DEV Community

Ian MacCallum
Ian MacCallum

Posted on • Originally published at stablekernel.com

Swift Design Patterns in Practice: the Decorator Pattern

Welcome to the first publication of “Swift Design Patterns in Practice”, a series with the goal of going beyond abstract examples by providing concrete use-cases of design patterns in real-world iOS applications. Today we will cover the Decorator Pattern and how it may be used when implementing the repo layer of an iOS application to interact with a RESTful API and a caching mechanism. The repo layer is an implementation of the Repository Design Pattern and serves the purpose of abstracting all interactions with our entity layer. We will give a general overview of the decorator pattern, including its structure, applications, and benefits. Then we will walk through a repo layer implementation for a single resource type for a mock environment with mock implementations and a production environment with HTTP and caching layer implementations. Before we get started, I wanted to give a shoutout to Stable Kernel for giving me time to work on these blog posts. Okay, let's get started!

Overview

The decorator pattern allows you to dynamically and transparently add or extend functionality of an object. The main idea is to provide an interface for some object (component) that you “wrap” or decorate via constructor injection with objects that implement the same interface (decorators). You then use decorators in place of the original object because they share a common interface (yay polymorphism). This allows the decorators to modify the functionality of the component without changing any of its code at runtime. For people who like diagrams, here’s a diagram:

Decorator Pattern Diagram

Benefits

Alternative to inheritance

Inheritance creates unnecessary coupling between the subclass and superclass. When changes are made to the superclass, they can break expected behavior in the subclass. Inheritance tends to be a suboptimal solution in almost all scenarios. The decorator pattern mitigates the need to subclass by giving us the ability to inject the object to fall back on for functionality instead of being forced to use the superclass.

Decoupled and extensible code

The component is open for extension but closed for modification (Open Closed Principle), as functionality is added by decorating the component with a new object rather than modifying it directly. The decorators should serve a single purpose for extending the component’s functionality (Single Responsibility Principle). It is a small structural change to your code that ensures your components are decoupled and extensible.

Avoid class explosion from combinational domain problems

Class explosion occurs in problems where many customizations are possible, especially when the order in which these customizations are applied matters or when the customizations depend on other customizations for their functionality, causing the class hierarchy to grow exponentially for each new customization added. This post gives an in depth look at an exploding class hierarchy but that's not the focus of this post so we'll leave it at that.

Compiler-enforced type checking for objects that can change dynamically at runtime

The component is an interface. As a result, any component (decorated or not) may safely be used where this interface is used. When the component interface is changed, the compiler will yell at you until functionality is defined in all concrete components and decorators for any changes made.

Testing individual units of functionality

It becomes very easy to test individual concrete components or decorators because functionality is isolated. Mock concrete components can be used to test the functionality of a single decorator and tests may be reused for different combinations of decorated components.

Use Case: the Repo Layer

Previously, I would use a single object to hold all my network calls. Each function would be responsible for hitting the API, parsing the response, caching the data, and returning the result. This approach is rigid and scales horribly. As more calls are added this file grows and grows. You find yourself organizing these functions into clusters relating to the HTTP methods (GET, POST, PUT, DELETE, etc…) for a particular resource. This is a good sign that these functions actually belong in separate classes entirely. Enter the repo layer. In our implementation we will create a repo for accessing each resource type. A repo is meant to mirror the HTTP methods at a given URL path.

Implementation

We will look at an implementation for a single Post resource at the /posts[/:id] route. limiting our implementation to only creating and fetching post objects (GET and POST methods) for brevity's sake. Let's start with our Post struct:

// If we put our initializers in extensions
// this object will maintain its automatically 
// synthesized initializer
struct Post: Codable {
    let id: Int
    let title: String
    let body: String
}
Enter fullscreen mode Exit fullscreen mode

The decorator pattern starts with an interface. Our PostRepo protocol will define a getPost(id:completion:) and createPost(title:body:completion:), using a closure and a generic Result type to asynchronously handle both the success and error states for each request. This same concept applies using Observables, Futures, Promises, etc...

// Result.swift
enum Result<T> {
    case value(T)
    case error(Error)
}

// PostRepo.swift
protocol PostRepo {
    func getPost(id: Int, completion: (Result<Post>) -> Void)
    func createPost(title: String, body: String, completion: (Result<Post>) -> Void)
}
Enter fullscreen mode Exit fullscreen mode

We can mock this out with very little effort. We no longer have to wait around on the backend team to implement the endpoints we need to do our job.

// An in-memory mock repo
class MockPostRepo: PostRepo {

    private var id = 0
    private var posts: [Int: Post] = [:]

    func getPost(id: Int, completion: (Result<Post>) -> Void) {

        if let post = posts[id] {
            // The post exists in memory
            completion(.value(post))
        } else {
            let error = NSError(domain: "Network error", code: 1, userInfo: nil) // 404 presumably
            completion(.error(error))
        }
    }

    func createPost(title: String, body: String, completion: (Result<Post>) -> Void) {
        let post = Post(id: id, title: title, body: body)
        posts[id] = post
        id += 1 // Like a db would autoincrement unique identifiers
        completion(.value(post))
    }
}
Enter fullscreen mode Exit fullscreen mode

We can then initialize an instance of PostRepo to use in our application:

let postRepo: PostRepo = MockPostRepo()
Enter fullscreen mode Exit fullscreen mode

At this point our repo layer is functional. Our data is mocked and we are ready to build our UI. The postRepo can now be injected into a view controller, view model, coordinator, or any architecture component.

/// For displaying a single post by id
class PostViewController: UIViewController {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var bodyTextView: UITextView!

    private let postRepo: PostRepo
    private let postId: Int

    init(postRepo: PostRepo, postId: Int) {
        self.postRepo = postRepo
        self.postId = postId
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) { ... }

    override func viewDidLoad() {
        super.viewDidLoad()
        loadPost()
    }

    private func loadPost() {
        // We don't care if the post comes from a cache or
        // the network, only if we have one to display. Any
        // cache invalidation policy should be abstracted
        // away from us.
        postRepo.getPost(id: postId) { [weak self] result in
            switch result {
            case .value(let post):
                self?.update(with: post)
            case .error(let error):
                // Present error
            }
        }
    }

    private func update(with post: Post) {
        titleLabel.text = post.title
        bodyTextView.text = post.body
    }
}
Enter fullscreen mode Exit fullscreen mode

Our view controller is dependent on an abstraction, the PostRepo protocol, rather than any concrete implementation of it (Dependency Inversion Principle).

Now if we think about our caching layer, it is a fundamentally different entity than our repo layer. Our repo layer has one method to get a post asynchronously, but our caching layer needs to be able to store a post and fetch a cached post synchronously. We could easily make a concrete PostRepo decorator to perform caching, but we can abstract our caching layer even more by using a PostCache protocol to define an interface for our caching layer and a PostRepo decorator that is impartial to the particular cache that is used. Here is our PostCacheProtocol:

protocol PostCache {
    func getPost(id: Int) -> Post?
    func store(post: Post)
}
Enter fullscreen mode Exit fullscreen mode

Once again, this is very easy to mock out. With little to no effort we can make a concrete, in-memory implementation of PostCache:

// An in-memory post cache
class MockPostCache: PostCache {

    private var posts: [Int: Post] = [:]

    func getPost(id: Int) -> Post? {
        return posts[id]
    }

    func store(post: Post) {
        posts[post.id] = post
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we will look into decorating a PostRepo with a caching layer. To do this, we will define our abstract decorator by implementing a protocol that extends the PostRepo protocol (Component) and holds a reference to some inner PostRepo object. This step is entirely optional when implementing the decorator pattern. It is not out of the ordinary to create concrete decorators that conform to PostRepo and have an inner PostRepo property but then we must implement every PostRepo function for every concrete decorator in place of an abstract decorator.

With Swift, we use protocol extensions to provide reasonable defaults so it is not necessary to reimplement the functions that are not applicable to a particular decorator. Our protocol extension will have the default functionality of proxying all the calls to to the inner Component object. This behaves similarly to providing non-abstract functions in languages that have abstract classes or using mix-ins.

// MARK: - Abstract Decorator
protocol PostRepoDecorator: PostRepo {
    var inner: PostRepo { get }
}

// Forward all calls to the inner object by default
extension PostRepoDecorator {
    func getPost(id: Int, completion: (Result<Post>) -> Void) {
        inner.getPost(id: id, completion: completion)
    }

    func createPost(title: String, body: String, completion: (Result<Post>) -> Void) {
        inner.createPost(title: title, body: body, completion: completion)
    }
}
Enter fullscreen mode Exit fullscreen mode
class PostRepoCacheDecorator: PostRepoDecorator {

    private let cache: PostCache
    let inner: PostRepo

    init(inner: PostRepo, cache: PostCache) {
        self.inner = inner
        self.cache = cache
    }

    func getPost(id: Int, completion: (Result<Post>) -> Void) {

        guard let cachedPost = cache.getPost(id: id) else {

            // Hit the network to get posts
            return inner.getPost(id: id) { result in

                // Cache posts from network for next time
                if case .value(let post) = result {
                    cache.store(post: post)
                }

                // Forward the result
                completion(result)
            }
        }

        completion(.value(cachedPost))
    }

    func createPost(title: String, body: String,
                    completion: (Result<Post>) -> Void) {

        inner.createPost(title: title, body: body) { result in

            // Cache the newly created post
            if case .value(let createdPost) = result {
                cache.store(post: createdPost)
            }

            // Forward the response
            completion(result)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We use this PostRepoCacheDecorator to dynamically attach the functionality of caching to any PostRepo type by wrapping an existing PostRepo object and some PostCache object. Now when we set up our post repo, we are indifferent to the PostRepo and the PostCache we are using:

let cache = MockPostCache()
let api = MockPostRepo()
let postRepo: PostRepo = PostRepoCacheDecorator(inner: api, cache: cache)
Enter fullscreen mode Exit fullscreen mode

We now have a post repo with a mocked out API and an in-memory cache. If we want to implement a non-mock cache and API repo, all we need to do is create more concrete implementations of PostRepo and PostCacheProtocol. The only existing code that will change is where we originally setup the PostRepo instance that we will use throughout our application. We can test the new implementations independently and we do not have to refactor the existing implementations to add the desired functionality.

Let’s make an HttpPostRepo to make our network calls and a UserDefaultsPostCache to persist data between app launches.

// Some abstraction around URLSession
protocol HttpClient {
    func get<T: Codable>(_ path: String, completion: (Result<T>) -> Void)
    func post<T: Codable>(_ path: String, body: [String: Any], completion: (Result<T>) -> Void)
    // put, delete...
}

class HttpPostRepo: PostRepo {

    private let http: HttpClient

    init(http: HttpClient) {
        self.http = http
    }

    func getPost(id: Int, completion: (Result<Post>) -> Void) {
        http.get("/posts/\(id)", completion: completion)
    }

    func createPost(title: String, body: String, completion: (Result<Post>) -> Void) {

        let httpBody: [String: Any] = [
            "title": title,
            "body": body
        ]

        http.post("/posts", body: httpBody, completion: completion)
    }
}
Enter fullscreen mode Exit fullscreen mode
class UserDefaultsPostCache: PostCache {

    private let defaults: UserDefaults

    init(defaults: UserDefaults) {
        self.defaults = defaults
    }

    // MARK: - PostCache

    func store(post: Post) {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(post) {
            defaults.set(encoded, forKey: keyForPost(with: post.id))
        }
    }

    func getPost(id: Int) -> Post? {
        let decoder = JSONDecoder()
        let data = defaults.object(forKey: keyForPost(with: id)) as? Data
        return data.flatMap { try? decoder.decode(Post.self, from: $0) }
    }

    // MARK: - Helpers
    private func keyForPost(with id: Int) -> String {
        return "kPostKey:\(id)"
    }
}
Enter fullscreen mode Exit fullscreen mode

This becomes especially useful when dealing with future change. We could implement concrete PostCache classes for Core Data, Realm, or SQLite and it would take one line of code to integrate them into our application.

let defaults: UserDefaults = .standard
let httpClient: HttpClient = // ...

lazy var postRepo: PostRepo = {
    let cache = UserDefaultsPostCache(defaults: defaults)
    let api = HttpPostRepo(http: httpClient)
    return PostRepoCacheDecorator(inner: api, cache: cache)
}()
Enter fullscreen mode Exit fullscreen mode

And that’s really all there is to it. We now have an implementation agnostic repo layer that does not tie us down to any framework, library, or dependency. The Xcode project containing mock and production scheme configurations, all this source code, and a sample playground can be found on GitHub.

Top comments (0)