📜 ⬆️ ⬇️

Singleton, service locator and tests in iOS

Hi, Habr! I am Bogdan, I work in the mobile team of a Badoo iOS developer.

In this article, we will look at using the Singleton and Service locator patterns in iOS and discuss why they are often called antipatterns. I will tell how and where they should be applied, keeping the code suitable for testing.



Singleton


Singleton is a class that has only one instance at a time.
')
Even if you have just begun iOS programming, you most likely have already encountered such singletones as UIApplication.shared and UIDevice.current . These objects are entities that exist in the real world in a single copy, so it is quite logical that in the application they are one by one.

Singleton is quite simply implemented in Swift:

 class SomeManager { static let shared = SomeManager() private init() { } } … let manager = SomeManager.shared manager.doWork() 

Note that the initializer is private, so we cannot allocate a new instance of the class directly, like SomeManager() , and must get access through SomeManager.shared .

let managerA = SomeManager.shared // correctly
let managerB = SomeManager() // wrong, compilation error

At the same time, UIKit is not always consistent with its singletons, for example, UIDevice() creates for you a new instance of a class that contains information about the same device (quite meaningless), while UIApplication() throws an exception at runtime during execution.

An example of a lazy (delayed) initialization of a singleton:

 class SomeManager { private static let _shared: SomeManager? static var shared: SomeManager { guard let instance = SomeManager._shared else { SomeManager._shared = SomeManager() return SomeManager._shared! } return instance } private init() { } } 

It is important to understand that a lazy launch can affect the state of your application. For example, if your singletons are subscribed to notifications, make sure that there are no similar lines in the code:

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

This means that you rely on implementation nuances. Instead, I recommend making your singletons explicitly defined and either allowing them to exist forever or linking important applications with status of a user session.

How to understand that an entity should be a singleton


In object-oriented programming, we try to divide the real world into classes and their objects, so if an object in your domain exists in the singular, it should be a singleton.

For example, if we create autopilot for a particular car, then this car is a singleton, since there can not be more than one specific car. On the other hand, if we make an application for a car factory, then the Automobile object cannot be a singleton because there are a lot of cars in the factory, and all of them are relevant to our application.

In addition to this, it is worth asking yourself the question: “Is there a situation in which an application can exist without this object?”

If the answer is positive, then even considering that the object is a singleton, storing it with a static variable can be a very bad idea. In the autopilot example, this would mean that if information about a particular car comes from the server, it will not be available when the application starts. Therefore, this particular car is an example of a singleton that is dynamically created and destroyed.

Another example is an application requiring the “User” entity. Even if the application is useless, as long as you are not logged in, it still works, even if you have not entered your data. This means that the user is a singleton with a limited lifetime. For more information read this article .

Abuse of singletons


Singletons, like ordinary objects, can be in different states. But singletons are global objects. This means that their state is projected onto all objects in the application, which allows an arbitrary object to make decisions based on the general state. This makes the application extremely difficult to understand and debug. Access to a global object from any application level violates the principle of minimal privileges and interferes with our attempts to control dependencies.

Consider this UIImageView extension a counterexample:

 extension UIImageView { func downloadImage(from url: URL) { NetworkManager.shared.downloadImage(from: url) { image in self.image = image } } } 

This is a very convenient way to load an image, but NetworkManager is a hidden variable that is not accessible from the outside. In this case, NetworkManager works asynchronously on a separate thread of execution, but the downloadImage method does not have a termination closure, from which it can be concluded that the method is synchronous. So until you open the implementation, you won’t understand in any way whether the image was loaded after the method was called or not.

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

Singletons and unit testing


If you conduct unit testing of the above extension, you will understand that your code makes a network request and that you can’t influence it in any way!

The first thing that comes to mind is to enter helper methods in NetworkManager and call them in setUp()/tearDown() :

 class NetworkManager { … func turnOnTestingMode() func turnOffTestingMode() var stubbedImage: UIImage! } 

But this is a very bad idea, since you have to write a production code that is only suitable for supporting tests. Moreover, you can accidentally use these methods in the production code itself.

Instead, you can follow the principle of "tests exceed encapsulation" and create a public setter for a static variable that holds a singleton. Personally, I think this is also a bad idea, because I do not perceive the environment, functioning only thanks to the promises of programmers not to do anything wrong.

The best solution, in my opinion, would be to cover the Network Service protocol and implement it as an obvious dependency.

 protocol ImageDownloading { func downloadImage(from url: URL, completion: (UIImage) -> Void) } extension NetworkManager: ImageDownloading { } extension UIImageView { func downloadImage(from url: URL, imageDownloader: ImageDownloading) { imageDownloader.downloadImage(from: url) { image in self.image = image } } } 

This will allow us to use a fake implementation (mock implementation) and conduct unit testing. We can also use different implementations and easily switch between them. Walkthrough: medium.com/flawless-app-stories/the-complete-guide-to-network-unit-testing-in-swift-db8b3ee2c327

Service


A service is an autonomous entity responsible for executing one business activity that may have other services as dependencies.

Also, the service is a great way to ensure the independence of business logic from UI elements (screens / UIViewControllers).

A good example is the UserService (or Repository), which contains a link to the current unique user (only one instance can exist in a specific period of time) and simultaneously to other users of the system. Service is an excellent candidate for the role of the source of truth for your application.

Services are a great way to separate screens from each other. Suppose you have a user entity. You can manually transfer it as a parameter to the next screen, and if the user changes on the next screen, you get it in the form of feedback:



Alternatively, screens can change the current user in the UserService and listen for user changes from the service:



Service locator


A service locator is an object that holds and provides access to services.

Its implementation may look like this:

 protocol ServiceLocating { func getService<T>() -> T? } final class ServiceLocator: ServiceLocating { private lazy var services: Dictionary<String, Any> = [:] private func typeName(some: Any) -> String { return (some is Any.Type) ? "\(some)" : "\(some.dynamicType)" } func addService<T>(service: T) { let key = typeName(T) services[key] = service } func getService<T>() -> T? { let key = typeName(T) return services[key] as? T } public static let shared: ServiceLocator() } 

This may seem like a tempting substitute for dependency injection, since you don’t have to explicitly pass the dependency:

 protocol CurrentUserProviding { func currentUser() -> User } class CurrentUserProvider: CurrentUserProviding { func currentUser() -> String { ... } } 

Register service:

 ... ServiceLocator.shared.addService(CurrentUserProvider() as CurrentUserProviding) ... 

Access the service through the service locator:

 override func viewDidLoad() { … let userProvider: UserProviding? = ServiceLocator.shared.getService() guard let provider = userProvider else { assertionFailure; return } self.user = provider.currentUser() } 

And you can still replace the services provided for testing:

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

But in fact, if you use the services locator in this way, it can bring you more trouble than good. The problem is that, outside the user service, you cannot understand which services are being used at the moment, that is, the dependencies are implicit. Now imagine that the class you wrote is a public component of the framework. How does the framework user understand that he should register the service?

Service Locator Abuse


If you have thousands of tests running and suddenly they start to fail, you can not immediately understand that the system under test has a service with a hidden dependency.

Moreover, when you add or remove a dependency on a service (or deep dependencies) from an object, a compilation error does not appear in your tests, due to which you would have to update the test. Your test may not even start failing at once, remaining “green” for a while, and this is the worst scenario, since eventually the tests begin to fail after some “unrelated” changes in the service.

Running the failed tests separately will lead to different results due to poor isolation caused by the common service locator.

Service Locator and Unit Testing


The first reaction to the described scenario may be the refusal to use service locators, but in fact it is very convenient to retain links in services, not transfer them as transitive dependencies, and avoid heaps of parameters for factories. Instead, it’s better to ban the use of the service locator in the code that we are going to test!

I suggest using the factory-level services locator in the same way that you would enter a singleton. A typical screen factory would then look like this:

 final class EditProfileFactory { class func createEditProfile() -> UIViewController { let userProvider: UserProviding? = ServiceLocator.shared.getService() let viewController = EditProfileViewController(userProvider: userProvider!) } } 

In the unit test, we will not use the service locator. Instead, we will constantly send our mock objects:

 ... EditProfileViewController(userProvider: MockCurrentUserProvider()) ... 

Is there a way to improve everything?


What if we decide not to use static variables for singletons in our own code? This will make the code more reliable. And if we ban this expression:

 public static let shared: ServiceLocator() 

then even the most illiterate novice developer will not be able to use our service locator directly and circumvent our formal requirement of introducing it as a constant dependency.
Therefore, we will be forced to store explicit references to the service locator (for example, as a property of the application delegate) and pass all services to the service locator as a necessary variable.

All factories and flow controllers / routers will have at least one dependency if they need any service:

 final class EditProfileFactory { class func createEditProfile(serviceLocator: ServiceLocating) -> UIViewController { let userProvider: UserProviding? = serviceLocator.getService() let viewController = EditProfileViewController(userProvider: userProvider!) } } 

In this way, we get a code that is perhaps less convenient, but much more secure. For example, it will not allow us to access the factory from the View layer, since the service locator is simply not available from there, and the action will be redirected to the router / flow controller.

Conclusion


We have analyzed the problems arising from the use of the Singleton and Locator of Services patterns. It became clear that the main part of the problems arises due to implicit dependencies and access to the global state. Introducing explicit dependencies and reducing the number of entities that have access to the global state improves the reliability and testability of the code. Now it's time to reconsider whether singltons and services are used correctly in your project!

Source: https://habr.com/ru/post/344506/


All Articles