Singleton, Service Locator and tests in iOS

Bohdan Orlov
Dec 5, 2017 · 8 min read

In this article, I want to review the usage of Singleton and Service Locator patterns in iOS and the reasons why they are often called anti-patterns. We’ll learn when and how to use them, while keeping our code testable.

Singleton

Singleton is a class which has only one instance existing at a time.

Even if you have just started iOS programming, it is very likely that you have already worked with singletons like UIApplication.shared and UIDevice.current. These objects represent entities which exist as one instance in the physical world, so it is natural to be able to have only one instance of those in the app.

Singleton implementation is straightforward in Swift:

class SomeManager {

Note that the initializer is private, so we can’t allocate a new instance directly like SomeManager() and must access it via SomeManager.shared.

let managerA = SomeManager.shared // valid
let managerB = SomeManager() // invalid, compilation error

At the same time UIKit is not always consistent about its singletons, for example UIDevice() creates a new instance for you on the same device (which doesn’t make much sense) while UIApplication() throws an exception in runtime.

It is possible to have lazy initialization of a singleton:

class SomeManager {

However, it is important to understand that lazy initialization might impact your application state e.g. if your singletons subscribe for notifications make sure you don’t have code like this:

_ = SomeManager.shared // initialization of a lazy singleton to achieve a side-effect

This means that you will have to rely on implementation details. Instead I recommend making singletons lifetime explicit and either making them live all the time or bind to important app states like state of a user session.

How to understand if an entity should be a singleton

In the object-oriented programming, we are trying to map the real world to classes and their objects, so if in your domain the object we are mapping exists in one instance then it might be a singleton.

For example, if we are making an autopilot for a car, then the current car is a singleton because no more than one current car can exist at a time. On the other hand, if we are making an app for the car factory then the Car entity can’t be a singleton because there are plenty of cars in the factory and all of them relevant to the app.

Additionally, we must to answer this simple question:

Can the application exist without this object at any moment?

If the answer is affirmative, then even if the object is a singleton, it might be a very bad idea to save it using a static variable. In the autopilot example, it means if the current car information comes from a server then it appears not to be available when the app starts. Thus, the current car is a singleton instance that is dynamically created and destroyed.

Another example is an app requiring a User entity. Even though the app is useless until you log in, the app works even if you haven’t logged in yet. Thus, a User is a singleton with a limited lifetime. For further reasoning read this article.

Singleton abuse

Singletons, as well as regular objects, can have some state or other. But singletons are global objects. Thus, this state is exposed to all objects in the app, allowing an arbitrary object to make decisions based on the global state. This makes the application extremely hard to debug and understand. The access to a global object from any layer of the app violates the principle of least privilege and our efforts to keep dependencies explicit.

As a counterexample consider this UIImageView extension:

extension UIImageView {

It is a very convenient way of downloading an image, but NetworkManager is a hidden dependency meaning that is not visible from outside. In our case, NetworkManager works asynchronously in a separate thread but the downloadImage method has no completion, and so a reader can make a wrong assumption that the method is synchronous. Thus unless you open the implementation there is no way to determine whether the image is set after the method call.

imageView.downloadImage(from: url)
print(String(describing: imageView.image)) // is an image set already or not?

Singletons and Unit Tests

When you decide to cover the extension above with Unit tests, you realize that your code will make a network request and you can’t prevent this!

The first thing that might come to mind is to introduce helper methods into NetworkManager and call them in setUp()/tearDown():

class NetworkManager {

func turnOnTestingMode()
func turnOffTestingMode()
var stubbedImage: UIImage!
}

This is very bad idea because you end up writing production code which is only there to support tests. And moreover, nothing prevents you from accidentally using them in the production code.

Alternatively, you can follow the idea that “test trumps encapsulation” and make a public setter for the static variable holding the singleton. I personally think that this is a bad idea because I don’t like environments that are functioning only because developers keep their promises not to do anything wrong.

A better solution is to cover the Network Service with a protocol and inject it as an explicit dependency.

protocol ImageDownloading {
func downloadImage(from url: URL, completion: (UIImage) -> Void)
}

This allows us to use a mock implementation and do unit tests. As a bonus we can provide a different implementation and exchange implementations easily. A step-by-step guide is available here.

Service

A service is a self-contained object which is responsible for performing a single business activity and can have other services as dependencies.

Additionally, a service is a great way to make business logic independent of UI elements (screens/UIViewControllers).

A good example is a UserService (or Repository) which keeps reference to the current user, which is unique (and only one instance can exist at a time), and to other users in the system at the same time. A service is a perfect candidate to be a source of truth for your application.

Services are an excellent way to decouple screens from each other. Imagine you have a User entity. You can manually pass it as a parameter to the next screen, and if the user change in the next screen, you would get it via a callback:

Alternatively, screens can change the current user in a User Service, and listen to user changes from the service:

Service locator

Service locator is an object retaining and providing access to services.

It’s implementation can look like this:

protocol ServiceLocating {
func getService<T>() -> T?
}

It might look like a tempting replacement for Dependency Injection because you don’t have to pass dependency explicitly:

protocol CurrentUserProviding {
func currentUser() -> User
}

Register the service:

...

Access the service from the service locator:

override func viewDidLoad() {
super.viewDidLoad()

And you can still replace provided services for testing:

override func setUp() {
super.setUp()
ServiceLocator.shared.addService(MockCurrentUserProvider() as
CurrentUserProviding)
}

But in fact, it will cause you more trouble than good if you use it like this. The problem with this approach is that from outside of the service user you can’t tell which services are being used. Thus, the dependencies are implicit. Now imagine that a class you have written is a public component of a framework. How can a user of the framework figure out that he is supposed to register a Service?

Service locator abuse

If you have thousands of tests and suddenly they start to fail it might take a while to realise that a system under test has a service with a hidden dependency.

Furthermore, when you add or remove a service dependency from an object (or its deep dependencies) there is no compile time error in your tests forcing you to update the test. Your test might not even fail immediately but stay green for some time, and this is the worst scenario, because eventually the tests start to fail after some “unrelated” change in the service.

Running failed tests separately is going to produce different results, due to bad isolation caused by usage of the shared service locator.

Service locator and Unit Tests

Consequently, the first reaction might be not to use a Service Locator at all, but in fact it is very convenient to keep references to Services and not pass them as transitive dependencies and avoid explosion of parameters for factories. Instead let’s just forbid the use of Service Locator in code which we want to test!

Thus, I suggest using Service Locator at the level of factories, in the same way as you’d inject a singleton. So a typical factory of a screen might look like this:

final class EditProfileFactory {

In unit tests, we wouldn’t use the Service Locator but we would pass our mocks explicitly:

...

Can we do better?

What if we pledge to not use static variables for singletons in our own code? This will allow us to make our code foolproof. Also, if we forbid this statement:

public static let shared: ServiceLocator()

then even the most ignorant junior developer won’t be able to use our service locator directly and bypass our formal requirement of injecting it as the explicit dependency.

Consequently, this will force us to store the explicit reference to the service locator (e.g. as a property of the app delegate) and pass the service locator as a required dependency to all the factories.

All factories and Routers / Flow Controllers will have at least one dependency if they need any service:

final class EditProfileFactory {

In this way we achieve code that is safer but maybe a bit less convenient. For example, it prevents us from calling the factory from a View layer, because service locator is simply not available there, and instead the action must be forwarded to a Router/Flow Controller.

Conclusion

We highlighted and addressed problems which arise from the usage of Singleton and Service locator patterns. Now it’s time to assess how they are used in your project!

Bumble Tech

This is the Bumble tech team blog focused on technology and…

Bumble Tech

We’re the tech team behind social networking apps Bumble and Badoo. Our products help millions of people build meaningful connections around the world.

Bohdan Orlov

Written by

iOS head. ex @IGcom 📈, @MoonpigUK 🐽, @Badoo 💘 and @chappyapp 🖤 Lets' grow together 🌱@bohdan_orlov http://arch.guru

Bumble Tech

We’re the tech team behind social networking apps Bumble and Badoo. Our products help millions of people build meaningful connections around the world.