Singleton, service locator and tests in iOS

    Hello, Habr! I am Bogdan, I work in the Badoo mobile team as an iOS developer.

    In this article, we will look at the use of the Singleton and service locator patterns in iOS and discuss why they are often called antipatterns. I will tell you how and where to use them, while keeping the code suitable for testing.



    Singleton


    A singleton is a class that has only one instance at a time.

    Even if you have just started to iOS-programming, you are likely to have been exposed to such singletons like UIApplication.sharedand UIDevice.current. These objects are entities in the real world that exist in a single copy, so it is logical that in the application they are one at a time.

    Singleton is pretty easy to implement 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 distribute a new instance of the class directly, like SomeManager(), and are required to 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 respect to its singletones, for example, it UIDevice()creates for you a new instance of a class that contains information about the same device (rather pointless), while UIApplication() throwing an exception in runtime at runtime .

    An example of lazy (pending) 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 lazy startup can affect the state of your application. For example, if your singletons are subscribed to notifications, make sure that the code does not contain such lines:

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

    This means that you rely on the nuances of implementation. Instead, I recommend making your singletones explicitly set and either allowing them to always exist or binding them to important application statuses like a user session.

    How to understand that an entity must 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 are creating an autopilot for a specific car, then this car is a singleton, since more than one particular car cannot exist. On the other hand, if we are making an application for a car factory, then the “Car” object cannot be a singleton, since there are a lot of cars in the factory, and all of them are relevant to our application.

    In addition to this, you should ask yourself the question: “Is there such a situation in which an application can exist without this object?”

    If the answer is yes, then even considering that the object is a singleton, storing it with a static variable can be a very bad idea. In the example of autopilot, this would mean that if information about a specific car comes from the server, then 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 that requires a User entity. Even if the application is useless until you log in to it, 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 .

    Singleton abuse


    Singletones, like ordinary objects, can be in different states. But singletones 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 level of the application violates the principle of minimum privileges and hinders our attempts to control dependencies.

    Consider this extension a UIImageViewcounterexample:

    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 it NetworkManageris a hidden variable that is not accessible from the outside. In this case, it NetworkManagerworks asynchronously in a separate thread of execution, but the method downloadImagedoes not have a completion closure, from which it can be inferred that the method is synchronous. So until you open the implementation, you won’t understand whether the image was loaded after the method was called or not. image already installed or not?

    imageView.downloadImage(from: url)
    print(String(describing: imageView.image)) //


    Singleton and Unit Testing


    If you conduct unit testing of the extension above, 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 introduce helper methods in NetworkManagerand call them in setUp()/tearDown():

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

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

    Instead, you can follow the principle of “tests outperform encapsulation” and create a public setter for the static variable holding the singleton. Personally, I think this is also a bad idea, because I don’t perceive environments that function only thanks to the promise of programmers to do nothing wrong.

    The optimal solution, in my opinion, is to cover the Network Service protocol and implement it as an explicit 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. And we can 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 object responsible for the execution of one business activity, which may have other services as dependencies.

    Also, a 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 (in a given period of time, only one instance can exist) and at the same time to other users of the system. Service is an excellent candidate for the role of 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 pass 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, the screens can change the current user in the UserService and listen to the user’s 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?
    }
    final class ServiceLocator: ServiceLocating {
        private lazy var services: Dictionary = [:]
        private func typeName(some: Any) -> String {
            return (some is Any.Type) ? "\(some)" : "\(some.dynamicType)"
        }
        func addService(service: T) {
            let key = typeName(T)
            services[key] = service
        }
        func getService() -> T? {
            let key = typeName(T)
            return services[key] as? T
        }
        public static let shared: ServiceLocator()
    }
    

    This might seem like a tempting replacement 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 the service:

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

    Get access to 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 provided services for testing:

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

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

    Service Locator Abuse


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

    Moreover, when you add or remove a service dependency (or deep dependencies) from an object, a compilation error does not appear in your tests, because of which you would have to update the test. Your test may not even start to crash right away, remaining “green” for some time, and this is the worst case scenario, since ultimately the tests begin to fail after some “unrelated” changes in the service.

    Running unsuccessful tests individually will lead to different results due to poor isolation caused by a 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 keep links in services, not pass them as transitive dependencies and avoid a bunch 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 service locator in the same way 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 pass our mock objects:

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

    Is there a way to improve everything?


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

    public static let shared: ServiceLocator()

    even the most illiterate novice developer will not be able to use our service locator directly and get around our formal requirement to introduce it as a permanent 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 the service locator to all factories as a necessary variable.

    All factories and router / flow controllers 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!)
        }
    }
    

    Thus, we get the code, perhaps less convenient, but much more secure. For example, it will not let us access the factory from the View layer, because the service locator is simply inaccessible from there, and the action will be redirected to the router / stream controller.

    Conclusion


    We looked at the problems that arise due to the use of the Singleton and Service Locator patterns. It became clear that the bulk of the problems are 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 is the time to reconsider whether singletons and services are used correctly in your project!

    Also popular now: