What is the Decorator pattern and how is it used in Swift?

What is the Decorator pattern and how is it used in Swift?

ยท

7 min read

I faced a unique problem recently when I was trying to make modifications to some legacy code. I was tasked with adding a small extra feature on top of a class that was first implemented probably 10 years ago.

All I had to do was go to the class definition and add a new method to achieve what I wanted to? Easy right? Right? ....Wrong.

Adding that small piece of code ended up breaking all of the functionality surrounding it.

That was perfectly normal to be very honest. My next best bet was to subclass the class in question and make changes to the new class in a way that didn't break the call-sites of the original class. Also easy right? RIGHT? ... Wrong.

The class was marked final. Which means subclassing was also not an option. Extending the type with some sort of type conformance from my caller would mean I would be exposing that new behaviour on all of my caller types. Which was also something I didn't want to do.

What I wanted was to very specifically add functionality to one particular use case that would probably never be used by any other class or use case.

Verbalising this use case before would have probably helped me arrive at the solution earlier. Because it was crystal clear that what I needed was a Decorator over that class.

What is a Decorator?

"[A Decorator is a means to] attach additional information to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality" - GO4

The key words in this definition are

  • "...to an object"
  • "dynamically"

Decorators apply to objects. Not Types.

Now read the above line 10 more times. And then some more.

Sometimes we want to add responsibilities to individual objects, not to an entire class. One way to add a responsibility is inheritance. However, this is inflexible because the client cannot control when it has to add that specific responsibility because all instances of that inherited type will now have this added capability.

A more flexible way to achieve this would to be wrap your component within another component of the same type which then implements the new responsibility.

The decorator should conform to the interface of the component it decorates so that its presence is transparent to the component's clients. The decorator forwards requests to the component and performs any additional actions before or after forwarding. This also lets you nest the decorators recursively, thereby allowing you unlimited number of added responsibilities.

Let's dive into some code to see how this all is practically possible. I will start off with a simple example and then we shall look at a more practical use case.

We've all been in situations where it's too hot in the afternoon for a jacket but as soon the sun sets we're shivering and cursing ourselves for not planning our outfits. Well, not this guy.

protocol Shirt {
    func getWarmth() -> Int
}

final class CottonShirt: Shirt {
    private var warmth: Int = 5

    init(warmth: Int) {
        self.warmth = warmth
    }

    func getWarmth() -> Int {
        return warmth
    }
}

protocol ShirtWearable {
    var shirt: Shirt { get set }
    func wearShirt()
}

class Person: ShirtWearable {
    var shirt: Shirt {
        didSet {
            wearShirt()
        }
    }

    init(shirt: Shirt) {
        self.shirt = shirt
        wearShirt()
    }

    func wearShirt() {
        print("My warmth is now \(shirt.getWarmth()) out of 10")
    }
}

let cottonShirt = CottonShirt(warmth: 5)
let johnDoe = Person(shirt: cottonShirt)

What is important to note in the above code is that the definition of CottonShirt does not allow it to be subclassed. Because the creator of that shirt believes that a cotton shirt cannot be subclassed to behave like something else. Had we chosen to extend the CottonShirt shirt class to add cold resistance, every instance of that shirt can be modified even when it isn't necessary.

extension Shirt {
    func addWarmth(by value: Int) -> Int {
        let newWarmth = getWarmth() + value
        return min(newWarmth, 10)
    }
}

let johnDoeInHawaii = Person(shirt: cottonShirt)
print("temperature increases by 3 degrees")
johnDoeInHawaii.shirt.addWarmth(by: 5)

John Doe would John Die in Hawaii with this kind of code governing his life :(

But John Doe knows how to decorate his shirt with a jacket when the weather changes. Be like John Doe.

Here's how he does it.

final class JacketDecorator: Shirt {
    private var warmth: Int = 8
    private var decoratee: Shirt

    init(decoratee: Shirt) {
        self.decoratee = decoratee
    }

    func getWarmth() -> Int {
        return decoratee.getWarmth() + 3
    }
}

So now when the temperature drops, all he has to do it put his jacket on.

print("temperature drops by 5 degrees")
johnDoe.shirt = JacketDecorator(decoratee: johnDoe.shirt)

Points to note:

  • We decorated a concrete object cottonShirt with added capabilities and NOT its type.
  • Both the decorator and decoratee conform to the same interface Shirt
  • JacketDecorator can also be decorated the same way as it decorates CottonShirt
  • JacketDecorator forwards to request to the decoratee and applies a transformation after.

I know this seems like an awful lot of code to achieve something so trivial, but let's look at a more realistic example of how we can apply this to solve actual problems.

Something as basic a making an HTTP request from an app can benefit immensely from this concept. We all know that URLSession runs on a background thread by default and sometimes we want to call the result of the completion on a UI Thread (aka the Main thread).

Here I've implemented a simple method to make a GET call to an api, along with its invocation. (Pardon my usage of force-unwraps)

protocol HTTPClient {
    func getData<T: Codable>(url: URLRequest, expecting: T.Type, completion: @escaping (T) -> Void)
}

final class URLSessionHTTPClient: HTTPClient {
    func getData<T>(url: URLRequest, expecting: T.Type, completion: @escaping (T) -> Void) where T : Decodable, T : Encodable {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if error == nil, let data = data {
                let result = try! JSONDecoder().decode(T.self, from: data)
                completion(result)
            } else {
                print(error)
            }
        }
        task.resume()
    }
}

struct ResponseModel: Codable {
    var response: String
}

let urlRequest = URLRequest(url: URL(string: "https://justanendpoint.free.beeceptor.com")!)
let apiClient = URLSessionHTTPClient()

let completion: (ResponseModel) -> Void = { result in
    print("Completed with \(result) on \(Thread.current) thread\n")
}

apiClient.getData(url: urlRequest, expecting: ResponseModel.self, completion: completion)

I could just ask my URLSessionHTTPClient to blindly return its result on the main thread. But that would mean that my HTTPClient instance is now responsible for both network calls and thread management, which is a violation of the Single Responsibility principle. By doing that, your code cannot be changed easily, cannot be tested in isolation, and cannot be composed or replaced very easily.

One can see how easy it is to add simple one-liners and increase the maintenance effort required for that piece of code.

The best way to achieve this, albeit with slightly more code, would be to decorate this api call with the extra responsibility of thread management. And invoke it like so,

final class MainThreadHTTPClient: HTTPClient {
    private let decoratee: HTTPClient

    init(decoratee: HTTPClient) {
        self.decoratee = decoratee
    }

    private func dispatchToMainThread(completion: @escaping () -> Void) {
        guard Thread.isMainThread else {
            return DispatchQueue.main.async(execute: completion)
        }
        completion()
    }

    func getData<T>(url: URLRequest, expecting: T.Type, completion: @escaping (T) -> Void) where T : Decodable, T : Encodable {
        decoratee.getData(url: url, expecting: expecting) { [weak self] result in
            self?.dispatchToMainThread {
                completion(result)
            }
        }
    }
}

let mainThreadAPIClient = MainThreadHTTPClient(decoratee: apiClient)
mainThreadAPIClient.getData(url: urlRequest, expecting: ResponseModel.self, completion: completion)

So this way, clients that don't need their results on the main thread can simply use the URLSessionHTTPClient instance, and the others can use the MainThreadHTTPClient instance.

And using the magic of generics, it gets even better to make the decorator even more reusable.

final class MainQueueDispatchDecorator<T> {
    let decoratee: T

    init(decoratee: T) {
        self.decoratee = decoratee
    }

    func dispatchToMainThread(completion: @escaping () -> Void) {
        guard Thread.isMainThread else {
            return DispatchQueue.main.async(execute: completion)
        }
        completion()
    }

}
extension MainQueueDispatchDecorator: HTTPClient where T == HTTPClient {
    func getData<T>(url: URLRequest, expecting: T.Type, completion: @escaping (T) -> Void) where T : Decodable, T : Encodable {
        decoratee.getData(url: url, expecting: expecting) { [weak self] result in
            self?.dispatchToMainThread {
                completion(result)
            }
        }
    }
}

AAAAND we are done!

This lesson ended up being longer than I initially planned it to be. But I hope you liked and have understood the content. All feedback is appreciated :)

Don't forget to Get Swifty ๐Ÿ‘‰๐Ÿป๐Ÿ˜Ž๐Ÿ‘‰๐Ÿป with me and the iOS Community on Twitter and LinkedIn! Tschรผss!

ย