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:
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
}
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)
}
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))
}
}
We can then initialize an instance of PostRepo
to use in our application:
let postRepo: PostRepo = MockPostRepo()
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
}
}
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)
}
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
}
}
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)
}
}
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)
}
}
}
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)
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)
}
}
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)"
}
}
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)
}()
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)