Why I love Swift protocol extensions

black and white blank challenge connect

Photo by Pixabay on Pexels.com

Let’s develop a small framework, allowing client developers to use working schedule objects. Each schedule would be based on an interval of days: from a start day to a finish day (specified by numbers).

Optionally, client developers may configure specific days to be excluded from the working interval. Exclusion of day can be set either as an array or programatically by providing a predicate function.

Client developers would afterwards be able to obtain the day count for any schedule object that they have defined.

We don’t want client developers to need to pass a closure for excluding days because it would be necessarily @escaping (as it would be required when computing day count) and they should ensure they don’t forget to capture self as an unowned var within for their function body to avoid memory leaks (and some developers might not be aware of this and/or may have a difficult time doing it in the right way.) We don’t want to use weak container alternatives either, as explained below, because that would clutter the client side syntax too. But enough intro, I guess. Let’s start.

We’ll first provide a protocol that anyone could implement, even as a simple struct.

public protocol ScheduleObject {
    var startDay: Int { get }
    var finishDay: Int { get }
    func excluding(day: Int) -> Bool
}

Then, we can also provide a base class that defines fields for start and finish day, and further allows overriding the exclusion function only, for the client developer to have an easier start. (Usually we run away from inheritance but in my opinion here it has a good use.)

open class ScheduleBase: ScheduleObject {
    public init(startDay: Int, finishDay: Int) {
        self.startDay = startDay
        self.finishDay = finishDay
    }
    public var startDay, finishDay: Int
    open func excluding(day: Int) -> Bool {
        return false
    }
    
    deinit {
        print("ScheduleBase deinitialized")
    }
}

We can also offer an implementation that handles the exclusion by checking an optional array of excluded days, for the situation when the client developer already a-priori knows all excluded days.

public class Schedule: ScheduleBase {
    public init(startDay: Int, finishDay: Int,
                excludingDays: [Int] = []) {
        self.excludingDays = excludingDays
        super.init(startDay: startDay, finishDay: finishDay)
    }
    public var excludingDays: [Int]
    public override func excluding(day: Int) -> Bool {
        return excludingDays.contains(day)
    }
}

At this point, we might think that we could also allow client developers to use a struct with excluder closure; being a value type, there is no problem if their excluder definition uses self inside the code, right? Um… wrong! The closure is a reference type and if it captures a reference typed self from the client app it would create a reference cycle too! In my opinion, it’s easier to just let the developer define a plain old class/struct and OOP-ish overriding/implementing our function instead. (The struct is provided here just as an example of what it would mean on client side, thus the name.)

public struct BadScheduleStruct: ScheduleObject {
    public init(startDay: Int, finishDay: Int,
                excluder: @escaping (Int) -> Bool) {
        self.startDay = startDay
        self.finishDay = finishDay
        self.excluder = excluder
    }
    public var startDay, finishDay: Int
    public var excluder: (Int) -> Bool
    public func excluding(day: Int) -> Bool {
        return excluder(day)
    }
}

Furthermore, if we’d want to reuse the start day and finish fields that ScheduleBase offers, and any other logic we’d want to add to the base later (such as the computing of day count that was requested in the first place and which you might have aleady considered as candidate there), while still allowing client developers to use an excluder closure, it won’t work that smooth, we’d need a class to do inheritance! And still require @escaping closure and unowned self capture on client side. So I think we’d better not add this class to our framework either. (Again, it is provided here just as an example of what it would mean on client side, thus the name.)

public class BadScheduleClass: ScheduleBase {
    public init(startDay: Int, finishDay: Int,
                excluder: @escaping (Int) -> Bool) {
        self.excluder = excluder
        super.init(startDay: startDay, finishDay: finishDay)
    }
    public var excluder: (Int) -> Bool
    public override func excluding(day: Int) -> Bool {
        return excluder(day)
    }
    
    deinit {
        print("BadScheduleClass deinitialized")
    }
}

Anyway, we need to make sure that all schedule objects (our own Schedule class, Bad* types, classes defined by the client developer as inheriting from ScheduleBase, or simply types implementing the original ScheduleObject protocol), would somehow all support computing of the day counts: because it was in the requirements! 🙂

We could think about creating a new class – ScheduleComputer or whatever – to do this, initializing it with an instance of ScheduleObject. But there is a more simple solution! You don’t need a new class if you’re not going to store any new field! Protocol extensions can help us here instead! Usually extensions are needed when you have no access to the base type source code, but this time their use case is within the same framework!

This is why I absolutely love them!

public extension ScheduleObject {
    public var dayCount: Int {
        var count = 0
        for day in startDay...finishDay {
            if !excluding(day: day) {
                count += 1
            }
        }
        return count
    }
}

The framework is now complete! So let’s see the how the client side could look like. Here are 6 ways that client developers could use our framework, depending on their app requirements and/or personal preferences – the first 4 without needing any closures or [unowned self] captures.

We start with the safe ways for client developers. First 2 examples (schedule1 and -2) use Schedule class already provided by framework. The following 2 examples (-3 and -4) use custom class or struct, defined below (i.e. by the client app). Finally, the last 2 examples (-5 and -6) use closures and Bad* struct and class provided by the framework for example purposes only (but that I advise not to include in production frameworks). To demonstrate, feel free to remove each “[unowned self]” below separately, to see that at runtime the expected “App deinitialized” won’t be displayed anymore, regardless of which one you remove, i.e. the deinit of ClientApp isn’t called anymore, generating a memory leak!

class ClientApp {
    init() {
        schedule1 = Schedule(startDay: 2, finishDay: 17)
        schedule2 = Schedule(startDay: 2, finishDay: 17,
                             excludingDays: [4, 7])

        schedule3 = CustomClassSchedule(startDay: 1, finishDay: 16)
        schedule4 = CustomStructSchedule(startDay: 1, finishDay: 16)

        schedule5 = BadScheduleStruct(startDay: 1, finishDay: 16)
            { [unowned self] day in day % self.mod == 0 }
        schedule6 = BadScheduleClass(startDay: 1, finishDay: 16)
            { [unowned self] day in day % self.mod == 0 }
    }

    func run() {
        print("schedule1.dayCount:", schedule1!.dayCount)
        print("schedule2.dayCount:", schedule2!.dayCount)
        print("schedule3.dayCount:", schedule3!.dayCount)
        print("schedule4.dayCount:", schedule4!.dayCount)
        print("schedule5.dayCount:", schedule5!.dayCount)
        print("schedule6.dayCount:", schedule6!.dayCount)
    }
    
    var schedule1, schedule2, schedule3,
        schedule4, schedule5, schedule6: ScheduleObject?
    
    class CustomClassSchedule: ScheduleBase {
        public override init(startDay: Int, finishDay: Int) {
            super.init(startDay: startDay, finishDay: finishDay)
        }
        public override func excluding(day: Int) -> Bool {
            return day % 2 == 0
        }
    }
    struct CustomStructSchedule: ScheduleObject {
        var startDay, finishDay: Int
        func excluding(day: Int) -> Bool {
            return day % 3 == 0
        }
    }
    
    let mod: Int = 4
    
    deinit {
        print("App deinitialized")
    }
}

ClientApp().run()
Advertisements

About Sorin Dolha

My passion is software development, but I also like physics.
This entry was posted in iOS, macOS, Swift and tagged , , , , , . Bookmark the permalink.

Add a reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s