For those less familiar with these diagrams, solid lines with solid arrow heads indicate source code dependencies, and dotted lines with open arrows indicate types that implement a protocol.
In this little app, we have three modules:
Weather Feature Module
Weather API Module
Weather Cache Module
Tightly Coupled Protocols
In the Weather Cache Module, we have a Protocol <WeatherStore> with a source code dependency on the WeatherReport type in a different module.
This presents a problem. Any type that implements the WeatherStore protocol will then also have a source code dependency on WeatherReport. If WeatherReport ever needs to change, we could be breaking any number of implementations of that protocol.
This kind of coupling can quickly become a major bottleneck. Let’s look at one way to handle this.
Data Transfer Objects to the Rescue
To decouple the WeatherStore protocol from the WeatherReport type, we can use a data transfer object, or a DTO.
Data Transfer Objects are typically simple types with no business logic. It’s just a structured data container that defines only the information that should be shared between different parts of your application.
We use Data Transfer Objects to create clear boundaries between our internal domain models and the data we expose externally.
To understand this better, let’s use one to decouple our code.
Decoupling WeatherStore With a DTO
Let’s get into the example:
This WeatherStore has an insert(:::) function that inserts an array of WeatherReport‘s into a local cache. This cache might use a variety of frameworks for persistence, such as SQLite, json files, etc. As it currently stands, each of those implementations would need to use the same WeatherReport model. This could easily cause problems. For example, the existing WeatherReport represents the data from the API response. But when we want to cache a WeatherReport, there might be other data we want to store in the model, such as a timestamp or file location. But those properties would only be relevant to a specific caching implementation.
In other words, we really have two different “WeatherReport” models:
The API’s model
The local cache’s model
This is the kind of problem that a Data Transfer Object was designed to solve. Let’s implement one and see how comes together.
First, we can essentially make a copy of the existing WeatherReport model, and then update the WeatherStore to reference that model instead:
Since the LocalWeatherLoader calls the updated .insert(:::) method on the WeatherStore, we need to update that as well. The cache(::) function on LocalWeatherLoader accepts an array of WeatherReport‘s and inserts them into the WeatherStore instance. Now that this instance accepts only LocalWeatherReport models, we need a way to map [WeatherStore] --> [LocalWeatherStore]:
We create a private extension on Array, scoped specifically to arrays where the elements are WeatherReport. In that extension, we define a toLocal() function that just maps the array to an array of LocalWeatherReport‘s.
Then we update the cache(::) function to use that new function:
Here’s how the change looks in our dependency diagram:
Wrap Up
Strong coupling can be hard to notice. So dependency diagrams can be a useful tool for finding couplings that can become bottlenecks.
Data Transfer Objects can be a useful tool when separate parts of your software depend on a shared model. But they aren’t always the right tool for the job. In a simple codebase, especially when it does nothing more than consume and display data from an external API, using a single shared model can work quite well.
But as soon as your app needs to do more, using Data Transfer Objects to decentralize the use of models can be a powerful tool for making your codebase more modular, more flexible, and more resilient to changes.
Conceptually, singletons are simple. They are a pattern for making sure that an object only has one instance, and provides a single point of access to that instance. The canonical definition is based on the mathematically definition of a singleton:
A singleton in mathematics, also known as a unit set or one-point set, is a set with exactly one element. For example, the set {0} is a singleton whose single element is 0.
The Singleton pattern in software design refines this definition in some important ways. A singleton object:
Ensures that only one instance can be created
Provides easy access to that instance
Controls its instantiation
In practice, this implies some specific behavior from our object:
It is responsible for keeping track of its sole instance
Ensures that no other instance can be created, by intercepting requests for creating new instances
Provides a way to access the sole instance
While Singletons have a reputation for being an anti-pattern, there are times when a Singleton pattern can be quite helpful. From the book Design Patterns: Elements of Reusable Object-Oriented Software:
Use the Singleton pattern when
there must be exactly one instance of a class, and it must be accessible to clients from a well-known access point.
when the sole instance should be extensible by subclassing, and clients should be able to use an extended instance without modifying their code.
There are some important benefits from using a Singleton:
Controlled access: since the Singleton class encapsulates its sole instance, it can strictly control how and when clients access it.
Reduced name space: since the Singleton class encapsulates its instance, it avoids polluting the namespace with global variables, making it an improvement over global variables.
Allows for a variable number of instances: Since all access to the Singleton is via a well-known access point, changing the number of underlying instances later is easy, as only the operation that grants access to the Singleton needs to change.
More flexible than class operations: While class operations (e.g. class func functions in Swift) can be used to package a singleton’s functionality, that technique can make it more difficult to change a design to allow for more instances.
Singletons with an uppercase S
Alright, that’s a lot of theory. Let’s use this definition to start building a Singleton. Let’s start simple:
import UIKit
final class ApiClient {
static let instance = ApiClient()
private init() {}
}
let client = ApiClient.instance
This is a Singleton. I’ve used an upper-case S for here, because this class adheres strictly to the canonical definition. As we’ll see later, there are other patterns that replicate this functionality, but deviate in interesting and useful ways.
This class checks all the boxes:
The init() method is marked private, so it cannot be called explicitly.
The only way to access this class’s instance is by calling its static let instance property.
The instance property is immutable, so it cannot be set to nil and then reinitialized
Marking the class final prevents us from subclassing the class. This is important because a subclass could be instantiated separately.
There’s a problem with this, however. The canonical definition also requires the object to be extensible. Our final declaration makes it impossible to subclass. But in Swift, we can use Extensions.
import UIKit
final class ApiClient {
static let instance = ApiClient()
private init() {}
}
extension ApiClient {
func foo() {}
}
let client = ApiClient.instance
client.foo()
There are limits to this approach to extending a Singleton. Specifically, we cannot override functionality with an Extension. We can only add behavior. So if you need to be able to override functionality, you will need to make your Singleton subclass-able, and remove the final declaration.
It’s a trade off:
If you want people to be able to change existing behavior of your singleton, then you’ll need to make it subclass-able, and deal with the consequences.
If not, make it final, which will limit the extensibility to only being able to add behavior.
Singletons with a lowercase S
Sometimes this strict Singleton pattern is too restrictive. A good example of this is the URLSession class:
URLSession has a singleton shared session (which doesn’t have a configuration object) for basic requests. It’s not as customizable as sessions you create, but it serves as a good starting point if you have very limited requirements. You access this session by calling the shared class method. For other kinds of sessions, you create a URLSession with one of three kinds of configurations…
URLSession provide access to a shared singleton object as a convenience. It implements all of the requirements of a Singleton (uppercase), with one exception: it allows you to instantiate your own objects for your own needs.
In the strictest sense, then, the URLSession class is not, itself, a singleton. But as part of its implementation, it provides a convenience shared singleton. Notably, this shared singleton does adhere to the definition of Singleton:
✅ Ensures that only one instance (of shared) can be created
✅ Provides easy access to that instance
✅ Controls its instantiation
Testing Singletons
Let’s add a ViewController to our example above, and see what happens when we try to make it testable.
import UIKit
struct LoggedInUser {}
final class ApiClient {
static let instance = ApiClient()
private init() {}
func login(completion: (LoggedInUser) -> Void) {}
}
let client = ApiClient.instance
class LoginViewController: UIViewController {
func didTapLoginButton() {
ApiClient.instance.login() { user in
// show next screen
}
}
}
Now let’s say we want to test the didTapLoginButton() with unit tests, and without actually making an API request. We would need to replace the ApiClient with a mocked version. But two aspects of our class conspire to make this impossible:
final means we cannot create a MockApiClient subclass
the static let instance property cannot be reassigned
This Singleton cannot be tested. Not in Swift, anyway. But there are other ways to do this.
Subclassing + Mock
One approach would be to make our ApiClient subclass-able. Then we can inject our ApiClient instance into our ViewController. That way, we can replace our ApiClient with a MockApiClient subclass during tests.
import UIKit
struct LoggedInUser {}
class ApiClient {
static let instance = ApiClient()
private init() {}
func login(completion: (LoggedInUser) -> Void) {}
}
let client = ApiClient.instance
class MockApiClient: ApiClient {}
class LoginViewController: UIViewController {
var api = ApiClient.instance
func didTapLoginButton() {
api.login() { user in
// show next screen
}
}
}
Global State Masquerading as a Singleton
There’s a subtle change we can make to this that seems to make it even easier to test. Let’s make the ApiClient.instance a var instead of a let:
import UIKit
struct LoggedInUser {}
class ApiClient {
static var instance = ApiClient()
private init() {}
func login(completion: (LoggedInUser) -> Void) {}
}
let client = ApiClient.instance
class MockApiClient: ApiClient {
init() {}
}
class LoginViewController: UIViewController {
func didTapLoginButton() {
ApiClient.instance.login() { user in
// show next screen
}
}
}
To make this work in unit tests, we’d need to make sure to:
setUp(): make sure to replace the instance with the MockApiClient instance
tearDown(): make sure to replace the original instance of the ApiClient
This seems to be cleaner, too. The LoginViewController no longer needs to have the ApiClient injected.
The challenge here is that the semantics seem to indicate that this is a Singleton pattern. The property named “instance” and the “private init()” are hallmarks of the Singleton pattern.
But this is no longer a Singleton (or even a singleton). This is mutable global state.
Fixing the Global State Problem
Let’s change this code so that we contain the global state within our instance, and go back to this being a singleton (lowercase s):
import UIKit
struct LoggedInUser {}
class ApiClient {
static let shared = ApiClient()
func login(completion: (LoggedInUser) -> Void) {}
}
class MockApiClient: ApiClient {}
class LoginViewController: UIViewController {
var api = ApiClient.shared
func didTapLoginButton() {
api.login() { user in
// show next screen
}
}
}
What did we do?
First we revert the static var to a static let. This gets rid of our mutable global state.
Then we removed out private init(). This let’s people instantiate their own instance, which will be useful for mocking.
Then we re-add our var api property in the ViewController. This let’s us inject an ApiClient, which will need when mocking during unit tests.
Lastly, we rename instance –> shared, to reflect the fact that we still provide a singleton instance.
Singletons and Dependencies
Let’s start growing our app. We’ll add a FeedViewController.
import UIKit
struct LoggedInUser {}
struct FeedItem {}
class ApiClient {
static let shared = ApiClient()
func login(completion: (LoggedInUser) -> Void) {}
func loadFeed(completion: ([FeedItem]) -> Void) {}
}
class MockApiClient: ApiClient {}
class LoginViewController: UIViewController {
var api = ApiClient.shared
func didTapLoginButton() {
api.login() { user in
// show feed screen
}
}
}
class FeedViewController: UIViewController {
var api = ApiClient.shared
override func viewDidLoad() {
super.viewDidLoad()
api.loadFeed { loadedItems in
//update UI
}
}
}
The FeedViewController will need to use the api to load the feed, which it will do in viewDidLoad(). That means our ApiClient will need a loadFeed() function of some kind.
But doing it this way presents a problem. Let’s look at a diagram:
Imagine if each of these were in its own module. The concrete type ApiClient needs to have functions for each of them. But Login doesn’t need loadFeed() or loadFollowers(), and the same is true for the others.
Plus, each time we make a change to ApiClient, everything that depends on it will need to be recompiled. We have source code dependencies between the Login modules and the ApiClient module. We cannot reuse any of these modules without bringing all of these modules with it.
All that the LoginViewController really needs to do is call a function. But right now, to do that, it needs to go through three “layers”:
Needs to know where to find the ApiClient instance
Needs to know the concrete type
Needs to invoke a function to call
Is this a problem?
Maybe. It will depend on our needs. There are tradeoffs. The global state of the ApiClient.shared instance is convenient, but the implicit dependencies it creates can cause problems down the road.
If we need to attain some level of reusability, then these tradeoffs will start to bite us. We’ll want to understand those tradeoffs, and what to do about it.
Extensions to the Rescue
We can use Extensions to solve this problem. Let’s take a look:
Now the ApiClient provides generic functionality, while each module adds its own functionality. Let’s look at the code:
import UIKit
// Api Module
class ApiClient {
static let shared = ApiClient()
func execute(_ : URLRequest, completion: (Data) -> Void) {}
}
class MockApiClient: ApiClient {}
// Login Module
struct LoggedInUser {}
class LoginViewController: UIViewController {
var api = ApiClient.shared
func didTapLoginButton() {
api.login() { user in
// show feed screen
}
}
}
extension ApiClient {
func login(completion: (LoggedInUser) -> Void) {}
}
// Feed module
struct FeedItem {}
class FeedViewController: UIViewController {
var api = ApiClient.shared
override func viewDidLoad() {
super.viewDidLoad()
api.loadFeed { loadedItems in
//update UI
}
}
}
extension ApiClient {
func loadFeed(completion: ([FeedItem]) -> Void) {}
}
In this configuration, the modules are still dependent on ApiClient. If we add some new module, and that module requires changes to the functionality of ApiClient, it could break other modules.
But even so, this is much more flexible. And it may suit our needs just fine, for quite some time.
Inverting the Dependencies
Eventually, we may require more flexibility or reusability. when that time comes, Dependency Inversion is the next step.
Instead of having the modules depend on the ApiClient, what if we have the ApiClient depend on the modules?
This inversion makes a lot of sense. The ApiClient is an implementation details. Most of the time, the business doesn’t care about implementation details. They care about features. That’s why it can be helpful to have logic contained in the modules, and have the implementation details depend on the modules.
Let’s look at an inverted diagram more closely:
Here we are making each of these classes dependent on some kind of interface for the modules – it could be a protocol or a closure, for example. Remember that each of these modules really just needs a function that it can call (closure), or a type that implements a function (protocol).
For the most modular approach, where every module is reusable in another application, we might even consider introducing “adapters”:
This opens up lots of possibilities. Reusability translates into testability.
Do I Need This Much Complexity?
You might not. It is entirely reasonable to progress through refactorings, like the ones above, as an application becomes more complex.
But this isn’t as complex as it looks. For one, it may not be easy, but it is simple:
Composing is simple
Changing is simple
Testing is simple
It’s also worth remembering that diagrams don’t convert directly into code. Just because this diagram looks complex doesn’t mean the code will be.
Let’s start refactoring this to see what it might look like:
import UIKit
// Main module
extension ApiClient {
func login(completion: (LoggedInUser) -> Void) {}
}
extension ApiClient {
func loadFeed(completion: ([FeedItem]) -> Void) {}
}
// Api Module
class ApiClient {
static let shared = ApiClient()
func execute(_ : URLRequest, completion: (Data) -> Void) {}
}
class MockApiClient: ApiClient {}
// Login Module
struct LoggedInUser {}
class LoginViewController: UIViewController {
var login: (((LoggedInUser) -> Void) -> Void)?
func didTapLoginButton() {
login? { user in
// show feed screen
}
}
}
// Feed module
struct FeedItem {}
class FeedService: UIViewController {
var loadFeed: ((([FeedItem]) -> Void) -> Void)?
func load() {
loadFeed? { loadedItems in
//update UI
}
}
}
We’ve done a few things here:
We added a Main module
We added properties to each class that contains a closure
We moved all of the Extensions to the Main module
There’s still more to do. We’d have to solve the problem of the various struct types in the modules. But as you can see, this refactoring isn’t complicated.
Conclusion
Singletons are tricky to use, and easy to abuse. But they can be powerful, flexible, and testable when set up properly. The structure of a Singleton can evolve to meet the demands and complexity of the app or SDK that uses it. Using techniques like the ones above, we can react to problems as our application evolves.
Surprisingly, some design patterns that seem complex might have implementations that are quite simple, and provide powerful flexibility, both in usage and in testing.
In the spring of 2018, in the span of only a few minutes, my two-decade career as a progressive Mac IT Engineer was razed to its very foundations. A mindless decision by some middling middle manager at an ancient internet company ended it faster – and more completely – than I would have thought possible.
They didn’t even fire me.
The few years leading up to that had been lightning in a bottle for me. After some soul searching, and despite my bitterness and outrage over this turn of events, I knew how unlikely it was to find that again.
The best way for me to grow from there was to change paths. My next path, I decided, was the one I’d been teasing myself with since the introduction of the App Store: I would transform myself into an iOS Developer.
A New Path
To get there, professionally, I needed to plot a course through more familiar territory. In the fall of 2018, then, I joined our Release Engineering team. For more than two years, I became our iOS Release expert, working closely with our iOS Core platform team to develop and improve our iOS release strategies and get to know the developers and the codebase intimately.
Then, in the late winter of 2021, I finally made the jump to full-time iOS development as part of the iOS Core platform team that I had been supporting. Since then, I’ve been able to learn iOS development at scale, on a massive codebase, all while bringing my extensive background in release engineering and automation, along with more than two decades of making sure people are at the center of everything I do.
A Detour
Only a couple of months after I made this transition, the Release Engineering team I had been part of disbanded. The vacuum this left in our development process was substantial, even if it wasn’t felt immediately, and I found that my old release engineering skills were once again in demand. Increasingly, my iOS developer colleagues were finding themselves blocked by ever-mounting issues with CI. This presents me with a difficult decision. I have an ability among the iOS developers at my company that is unique: I have the experience and administrator privileges to debug and resolve CI problems – I can unblock my colleagues.
The problem for me is that release engineering is “iOS development” adjacent. It demands a deep understanding of all kinds of things that are related to iOS development. It just doesn’t require any actual iOS development. I don’t have to debug any Swift, decipher any massive view controllers, rewrite Objective-C, or track down any memory leaks. It was Ruby, and Fastlane, and yaml files, and 20+ iOS developers that were dead in the water behind a problem I could probably sort out myself without waiting for our Infrastructure team.
This tug-of-war issue continues to this day, although to a lesser degree. Despite my best efforts to limit this cross-over time, and with the full support of my team lead in doing so, I have spent much more time than I’d meant to in areas that haven’t helped me develop my skills as an iOS developer.
Returning to My Path
Starting January 1, 2024, this tug-of-war ends. I will be joining a brand-new iOS developer team on a brand-new project. My institutional knowledge about CI automation is related to my current product, which I won’t be supporting. We won’t have a sprawling, million-line project, or any of the “scale issues” (and CI issues) that come with it.
In fact, for a variety of reasons it has already effectively ended. For the next few weeks, I will be wrapping up my time here and preparing for my new team.
It is in this moment, this moment of profound change and re-focus, that something perfectly serendipitous has happened to me. I have found an opportunity to focus my training through an online course that I’ve been curious about for months: the iOS Lead Essentials course from Essential Developers.
I’m signed up and ready to start. And I cannot wait.