In this article I will present you powerful architectural pattern that I have been using it a lot called Decorator.
1. What is Decorator
By definition Decorator is architectural pattern which allows us incorporating new services, behaviours to existing components when certain events occur or simply augmenting existing behaviours.
Main benefit of the pattern is that the change is transparent and used automatically. In essence, the rest of the system does not have to know something has changed and clients of augmented components can use them as before without the actual need of knowing that something has changed. Sounds great, right?
We will take a look how can we implement Decorators with Swift and Protocols with ease.
2. Example: Token expiration
2.1 Intro
One of the common example in mobile world is token expiration. When we use API that needs authentication it can happen that we receive 401 status code response which indicates that token has expired and we need to refresh to re-authenticate it if we want to receive successful response.
There are many ways how to implement this so let’s take a look how we can implement this in a clean and scalable way this by using decorators.
I will show you an example how we can tackle this issue, decisions and motivation behind final solution.
For start let’s make a simple example components that will help us with an example – RemoteNamesLoader which conforms to NamesLoader and has two dependencies – HTTPClient and URL. Our goal will be to implement token refresh mechanism in a scalable way.
protocol TokenRefresher {
func refresh() async -> Void
}
protocol HTTPClient {
func execute(request: URLRequest) async throws-> (Data, HTTPURLResponse)
}
protocol NamesLoader {
func load() async throws -> [String]
}
final class RemoteNamesLoader: NamesLoader {
let client: HTTPClient
let url: URL
enum Error: Swift.Error {
case invalidResponse
}
init(client: HTTPClient, url: URL) {
self.client = client
self.url = url
}
func load() async throws -> [String] {
let (data, response) = try await client.execute(request: URLRequest(url: url))
return try map(data, response: response)
}
private func map(_ data: Data, response: HTTPURLResponse) throws -> [String] {
/**
We will skip parsing data since it's not part of the topic
and just return array with random name
**/
if response.statusCode == 200 {
return ["AnyName"]
} else {
throw Error.invalidResponse
}
}
}
Starting implementation checks if status code represents success – status code 200 and returns an array of names otherwise it throw an error.
But what should we do when we receive 401?
2.2 Posible solutions
2.2.1 Throw new error
Easiest and most straightforward solution would be to throw new error so that clients of NamesLoader would know that token has expired and they need to do something regarding that.
final class RemoteNamesLoader: NamesLoader {
enum Error: Swift.Error {
case invalidResponse
case tokenExpired
}
func load() async throws -> [String] {
let (data, response) = try await client.execute(request: URLRequest(url: url))
if response.statusCode == 401 {
throw Error.tokenExpired
} else {
return try map(data, response: response)
}
}
private func map(_ data: Data, response: HTTPURLResponse) throws -> [String] {
/**
We will skip parsing data since it's not part of the topic
and just return array with random name
**/
if response.statusCode == 200 {
return ["AnyName"]
} else {
throw Error.invalidResponse
}
}
}
Doing so nothing really solves, it just delegates responsibility to someone else which is not desired result. Image that you have ten or more components which can all receive 401 and they all start to throw this new error. We would need to react on this error in all places (either presentation or UI components) where we use them. This adds new dependency to TokenRefresher which just adds additional layer of complexity.
class NamesListController: UIViewController {
let loader: NamesLoader
let tokenRefresher: TokenRefresher
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
try await loader.load()
} catch {
if let error = (error as? RemoteNamesLoader.Error), case .tokenExpired = error {
await tokenRefresher.refresh()
try await loader.load()
} else {
}
}
}
}
}
Looks like complex and lot of duplicate work, so there must be a better way.
2.2.1 Handle 401 inside of component
To resolve the problem of delegating error and introducing new dependencies to the clients we can react to 401 inside of components.
final class RemoteNamesLoader: NamesLoader {
let client: HTTPClient
let url: URL
let tokenRefresher: TokenRefresher
enum Error: Swift.Error {
case invalidResponse
case tokenExpired
}
init(client: HTTPClient, url: URL, tokenRefresher: TokenRefresher) {
self.client = client
self.url = url
self.tokenRefresher = tokenRefresher
}
func load() async throws -> [String] {
let (data, response) = try await client.execute(request: URLRequest(url: url))
if response.statusCode == 401 {
await tokenRefresher.refresh()
let (data, response) = try await client.execute(request: URLRequest(url: url))
return try map(data, response: response)
} else {
return try map(data, response: response)
}
}
private func map(_ data: Data, response: HTTPURLResponse) throws -> [String] {
/**
We will skip parsing data since it's not part of the topic
and just return array with random name
**/
if response.statusCode == 200 {
return ["AnyName"]
} else {
throw Error.invalidResponse
}
}
}
class MyViewController: UIViewController {
let loader: NamesLoader
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
try await loader.load()
} catch {
}
}
}
}
Going with this approach we resolve both issues – we can remove both dependency and additional handling on client side which is great.
But in reality we still didn’t find optimal solution. Why? As I said before, if we have many components where we can get 401 we would need to inject in all those components TokenRefresher and implement / duplicate logic the same way as in example bellow. This again leads to a lot of repetitive code, extra dependencies and adds another level of complexity to all components.
3. Solution: Decorator
We already took two attempts to refresh the token but we always ended up with result that didn’t provide simple neither scalable solution.
As stated in introduction, decorators adds new behaviours to existing components or augment existing ones. So how can we implement it in a scalable way?
Going back to components definition that we used in our example we can see that only component that we didn’t touch was HTTPClient. And since HTTPClient is the one that delivers 401 we can try to augment its behaviour.
Thankfully HTTPClient is defined as protocol so this will be really simple. Let me introduce you HTTPClientDecorator.
First we need to define concrete object and both inject instance of HTTPClient and conform to HTTPClient. At this step we only forward calls directly from decorator to instance of client.
final class HTTPClientDecorator: HTTPClient {
let client: HTTPClient
init(client: HTTPClient) {
self.client = client
}
func execute(request: URLRequest) async throws -> (Data, HTTPURLResponse) {
try await execute(request: request)
}
}
Next and the only thing that we need to do is to move implementation from RemoteNamesLoader to HTTPClientDecorator.
final class HTTPClientDecorator: HTTPClient {
let client: HTTPClient
let tokenRefresher: TokenRefresher
init(client: HTTPClient, tokenRefresher: TokenRefresher) {
self.client = client
self.tokenRefresher = tokenRefresher
}
func execute(request: URLRequest) async throws -> (Data, HTTPURLResponse) {
do {
let (data, response) = try await client.execute(request: request)
if response.statusCode == 401 {
await tokenRefresher.refresh()
return try await client.execute(request: request)
} else {
return (data, response)
}
} catch {
throw error
}
}
}
That was easy, right? You may wonder why is this better than having TokenRefresher in RemoteNameLoader.
Let’s revert changes and revisit RemoteNamesLoader. As we can see we can remove all logic and dependencies regarding refreshing token and still keep newly implemented behaviour.
final class RemoteNamesLoader: NamesLoader {
let client: HTTPClient
let url: URL
enum Error: Swift.Error {
case invalidResponse
}
init(client: HTTPClient, url: URL) {
self.client = client
self.url = url
}
func load() async throws -> [String] {
let (data, response) = try await client.execute(request: URLRequest(url: url))
return try map(data, response: response)
}
private func map(_ data: Data, response: HTTPURLResponse) throws -> [String] {
/**
We will skip parsing data since it's not part of the topic
and just return array with random name
**/
if response.statusCode == 200 {
return ["AnyName"]
} else {
throw Error.invalidResponse
}
}
}
We are able to revert all changes since RemoteNamesLoader depends on HTTPClient protocol. So we just need to inject HTTPClientDecorator and we are done.
And what is best is that we can also inject same instance into all other components that need token refreshing mechanism without touching them.
Conclusion
We have just saw what Decorator is through a real example and all its benefits. In example we showcased how it can help us adding new behaviours to the system in a scalable way without touching existing codebase.
I hope that you enjoyed this article and learnt something new!
In case of any questions or comments feel free to contact me or leave it comments section bellow.
If you want to learn more about modular design, clean architecture, TDD and software architecture best practices I invite you to join iOS Lead Essentials program where I work as Lead Instructor.
Detect user’s country without accessing their location
Sometimes we are faced with challenge where we would like to improve user experience of our app based on where in the world or better said in which country ...