Simple Guide To Improve Singleton Usage in iOS Legacy Codebase
Don’t get me wrong, Singleton is not that bad :]
What is Singleton?
Singleton is one of the popular creational design patterns where in a class only and must have 1 number of instance. Singleton provides a convenient access because it can be accessed globally and spoils engineers in using it in their daily work. Generally, the way to create a Singleton is to create a class with a static reference for the object of that class and use a private initializer to prevent this Singleton from being initialized from elsewhere.
In iOS legacy codebase, we often see the use of Singleton in many places. Starting from using it to fetch data from the API, implementing caching, or storing user profile data so can be accessed in many places without need additional effort. Convenience in using Singleton, of course, is accompanied by several weaknesses that we will discuss in this article.
Shared Mutable State
Since Singleton can be accessed from many places, the first thing we need to pay attention to is about the shared mutable state that the Singleton has. Shared mutable state can be interpreted as data that can be accessed and changed from anywhere.
Consider the following simple code snippet:
We can see that there is one shared mutable state in the code above, namely userProfile. The data can be accessed and changed its value from any place. Having a shared mutable state on a Singleton makes it not thread-safe, which means that if there is a read/write process on the data at the same time on a different thread it will cause a race condition or even crash.
Therefore the first step to improve Singleton on the legacy iOS codebase is to find out if there is a shared-mutable state, then we must provide a secure read/write mechanism so that it doesn’t cause our application to experience race conditions or crash.
There are many ways to make the read/write mechanism thread-safe, one of which is to use a custom serial DispatchQueue. Serial DispatchQueue allows to run tasks that run according to the queue, wait for the task to finish, and only then can run the next task. Serial DispatchQueue guarantee that only one task that can be executed at the given time.
In the improvement code above we add _userProfile as shadow data that is used for read and write processes. Then we use the serial DispatchQueue object which makes the read or write process run serially and synchronously so that it avoids race conditions or crashes.
Because Singleton provides convenient access, we encounter a lot of code like this in our daily work:
This is what is called Implicit Dependency, where actually the SomeViewController object has dependencies to HomeService, CartService, and ProfileService but we can still initialize SomeViewController and it can run smoothly without warnings or errors from the compiler. As a result, the SomeViewController object is very tight-coupled with those Singleton objects, making it more difficult for the SomeViewController object to maintain or implement unit tests for it.
Therefore the second step to improve Singleton usage on the iOS legacy codebase is to look for Implicit Dependencies and refactor them gradually to make the class easier to maintain, more flexible to changes, and maybe we can also implement unit tests for that class. The first step to fixing an implicit dependency is to extract it and implement property dependency injection
Property Dependency Injection is an implementation of dependency injection
the easiest for legacy classes that have lots of implementations
inside it. But to implement dependency injection in the new class,
it is recommended to use constructor dependency injection.
Ok, the changes above make SomeViewController a bit more flexible because we can replace homeService, cartService, and profileService with other objects, for example when implementing unit testing we need to replace these objects using spy, stub, or mock. To create a spy, stub or mock we can create a subclass of HomeService, CartService or ProfileService.
But what happens if those classes are made final so you can’t create subclasses from them? Then we can make the abstraction by using the protocol. But before creating the protocol, if we pay attention, each of HomeService, CartService, and ProfileService has the same function, which is to make an API request to receive card models. Then we can refactor like this:
Implementing the code above makes the SomeViewController object have a dependency on the CardModelService protocol which makes the object flexible and acts polymorphic so we don’t need to implement if-else anymore.
Apart from using protocol as an abstraction, we can also implement a Higher-Order Function approach where we can create a function as a parameter or as a return type.
In this case, we can implement the Higher-Order Function as follows:
Where to go from here
Dealing with legacy codebase is not easy, but in my opinion it will give us a lot of chance to improve it (so we will receive salary increment :]). One of the common thing is there are so many Singleton usage in our legacy codebase. There is nothing wrong with Singleton, but we need to encounter some of the weakness from Singleton so make our codebase become better. Thank you for reading my article, see you in another time.