Singletons vs. Dependency Injection with Swift

Singletons vs. Dependency Injection with Swift

Examining the pros and cons of using both key design patterns.

Go to the profile of  Jonathan Banks
Jonathan Banks
11 min read
Time
45mins
Platforms
iOS
Difficulty
Medium
Technologies
Design Patterns,Swift

This tutorial provides a detailed comparison of the classic Singleton and Dependency Injection design patterns. The guide starts with an introduction and overviews of each design pattern along with diagrams and code to flesh out the concepts. Concrete Swift code examples make useful examples of each pattern.

Overview

There has been a paradigm shift against the use of singletons in recent years.  It's a fair argument as singletons can create more problems than they're worth if they're not used carefully.  More problematic is searching the term "singleton vs dependency injection swift" and finding a significant number of iOS discussions centering on replacing singletons with dependency injection.

This is a false argument at its core.  Dependency injection cannot be a drop-in replacement for singletons. Conceptually, they are very different architectural ideas.The majority of people advocating dependency injection over singletons are either making unclear arguments, misunderstanding the design patterns, or doing both.  They adamantly believe singletons are a poor design choice. Unfortunately, they're offering dependency injection as an alternative architectural solution without being clear that it's not a one-to-one drop-in replacement for singletons.

This is resulting in a significant amount of confusion for developers trying to decide which pattern will help most.

What's wrong with singletons? What is dependency injection? Can one replace the other?  The article starts with a succinct guide on singletons and the use of dependency injection. A short-list of pros and cons, code examples, and guidance on when to use each design pattern will round things out.

Singleton

Singletons are single instances of an object that are globally available across the entire lifetime of an application. Here's a UML representation of the pattern:

Singleton_pattern_uml

Referring to the UML, singletons consist of:

  1. private constructor
  2. instance variable of its own type
  3. public getter method to grab a reference of the instance

What does that look like in practice using Swift code? Imagine using a singleton instance that will permit you to login to an external service and get details around a user's profile settings:


class MySingletonAPIManager{
    
    static let shared = MySingletonAPIManager()
    private let apiKey:String = "KGA-43F-554-FGT"
    private let baseURL:String = "http://my.base.url"
    
    private init(){}
    
    func login(username:String, password:String)->Bool{
        // call login
    }
    
    func getUserProfile(username:String, password:String, profileURL:String)->String{
        // call API
    }
}

let mySingleton = MySingletonAPIManager.shared
mySingleton.login(username:"user1", password:"password")
mySingleton.getUserProfile(username: "user1", password: "password", profileURL: "get_profile")

Breaking this class down to the key components:

  • Static instance: shared of type MySingletonAPIManager. The static instance allows access to the shared variable without directly calling the initializer of MySingletonAPIManager.
  • Private initializer: MySingletonAPIManager is instantiated on the first access of the static variable. Only one copy is created, the private initializer ensures no chance of accidentally creating multiple copies.
  • getUserProfile and login: Public methods that are globally accessible from the shared single instance.

Thread-Safe Singletons

It's important to ensure that local variables in the singleton are thread-safe. Remember, the singleton is globally accessible from anywhere in the application. That means any number of background threads could simultaneously access the single, shared instance.

How do you protect against race conditions?

The answer is to use DispatchQueue from Grand Central Dispatch. Synchronize access to susceptible components by using the DispatchQueue.sync method call to ensure thread-safe operations.

As a demonstration, add a usageCounter and an instance of DispatchQueue to the previous example:

class MySingletonAPIManager{
    
    static let shared = MySingletonAPIManager()
    private let apiKey:String = "KGA-43F-554-FGT"
    private let baseURL:String = "http://my.base.url"
    private var dispatchQueue = DispatchQueue(label: "my.singleton.queue.identifier") 
    private (set) var usageCounter: Int = 0
    
    private init(){}
    
    func login(username:String, password:String)->Bool{
        // call login
    }
    
    func getUserProfile(username:String, password:String, profileURL:String) -> String{
        // call API
        dispatchQueue.sync {
            usageCounter += 1
        }
    }
}

let mySingleton = MySingletonAPIManager.shared
mySingleton.login(username:"user1", password:"password")
mySingleton.getUserProfile(username: "user1", password: "password", profileURL

The (set) keyword in front of the usageCounter variable indicates to the compiler that the setter is private but leaves the getter public. This is desirable because random threads should not set the value.

The call to execute getUserProfile runs concurrently from any thread. Grand Central Dispatch automatically controls and queues access to the usageCounter using DispatchQueue.sync.

The following example illustrates the race condition to avoid, on a non-synchronized instance:

  • Thread 1 calls getUserProfile (concurrent to Thread 2)
  • Thread 2 calls getUserProfile (concurrent to Thread 1)
  • Thread 1 accesses the reference to usageCounter and retrieves the value 0
  • Thread 2 access the reference to usageCounter and retrieves the value 0
  • Thread 2 returns the value 1 to usageCounter
  • Thread 1 returns the value 1 to usageCounter.

The counter should have been incremented to 2 but instead stores. Write code that creates several threads that call getUserProfile both with and without synchronizing protections. It will demonstrate DispatchQueue.sync resolves this issue.

Dependency Injection

The singleton pattern specifies the functionality it offers through publicly accessible methods. On the other hand, dependency injection permits the caller to "inject" the functionality it desires into the object, whereby the object is "dependent" on the injected service(s).

Here's an excellent UML diagram from Wikipedia for a visual representation of that statement:

W3sDesign_Dependency_Injection_Design_Pattern_UML

In a dependency injection pattern, the caller instantiates the object that comprises the "service" and then injects that into the client. There are several ways to inject services:

  • Constructor: pass the services in the initializer
  • Setter: use a setter method to inject the services on the fly. For example, examine the setService() method.
  • Protocol: create a protocol, make the client conform to it by implementing its methods. One method acts as the setter to inject the service.

The correct choice depends on the architecture of the app. The safest of these options is the constructor method. Choose this method if the services are available upon client creation. Alternatively, using setters and protocols provide more flexibility but cannot guarantee all services are available when accessed. Mixing the options together provides yet another possibility.

Benefits of Dependency Injection

The main benefit of dependency injection is its incredible modularity. The building blocks of functionality are extracted outside the class using protocols for loose-coupling. Each source conforms to a standardized protocol.

Advantages of this pattern include:

  • Easier to inject different services into the same base class
  • Easier to test because you can inject simulated components and functionality as needed.
  • Code is reusable in other projects or dependent classes.

So far, nothing that has been discussed here indicates that dependency injection is a replacement for a single globally accessible object. Maybe taking a look at the implementation of dependency injection will reveal something.

Implementation

With dependency injection, we can interchange different services if they all conform to a base protocol. The sample code, demonstrated previously, permits logging into a single service and getting the user profile data.

Unfortunately, that singleton implementation only allows one such login into a single service. An option is to add variables and new functions to the singleton that would allow login to different services. This is also problematic because it will generate excessive and unmanageable code.

Instead, refactor the existing code and resolve this problem by creating a protocol for different login services to implement. The next step is designing the services that will implement that protocol.

Architecture Diagrams

The diagram of the protocol and redesigned API manager shows the changes that will be implemented. The refactored design will:

  • Create two services that implement the protocol APIProtocol
  • MyAPILoginManager will now have a class variable, named service, of that protocol type
  • Call the setService method and passing in one of the two new sample services, APIService and TotallyDifferentAPIService that implement the protocol

dependency_injection1

The sample code below will show the creation of an instance of MyAPILoginManager and one each of the two API services. The APIService is set first, its login and getUserProfile methods are called. Next,
the TotallyDifferentAPIService is injected into the same login manager and its methods are called.

Here's what that will look like visually:
dependency_injection2

Create a new file named APIProtocol and paste the following code:


protocol APIProtocol{
    func login() -> String
    func getUserProfile() -> String
}

Next, create the first test service that will implement the APIProtocol. The code to log in and get data is intentionally left mostly blank since it's for demonstration only. An actual implementation would have detailed proprietary login code that differentiates each service:

class APIService:APIProtocol{

    private let userName:String
    private let password:String
    private let apiKey:String
    private let baseURL:String
    
    init(uName:String, passwd:String, key:String, bURL:String){
        userName = uName
        password = passwd
        apiKey = key
        baseURL = bURL
    }
    
    
    func getUserProfile() -> String{
        // code to call specific API
        return ""
    }
    
    func login() -> String{
        // code to call login
       return ""
    }
}


The code for the next service is exactly the same but a different class name, TotallyDifferentAPIService. The implementations of the methods getUserProfile and login would be entirely different. The previous service might parse XML and the this one JSON with different API calls and key return values:

class TotallyDifferentAPIService:APIProtocol{

    private let userName:String
    private let password:String
    private let apiKey:String
    private let baseURL:String
    
    init(uName:String, passwd:String, key:String, bURL:String){
        userName = uName
        password = passwd
        apiKey = key
        baseURL = bURL
       
    }
    
    func getUserProfile() -> String{
        // code to call specific API
        return ""
    }
    
    func login() -> String{
        // code to call login
        return ""
    }
}

Now with the services complete, implement MyAPILoginManager, the central object the services just created will be injected into.

class MyAPILoginManager{
    
    var service:APIProtocol?
    var usageCounter:Int = 0
    var dispatchQueue:DispatchQueue?
    
    public init(queue:String){
        dispatchQueue = DispatchQueue(label: queue)
    }
    
    public init(svc:APIProtocol,queue:String ){
        service = svc
        dispatchQueue = DispatchQueue(label: queue)
    }
    
    func callLogin() -> String?{
       
        // make API call through service to log user in
        return service?.login()
    }
    
    func callGetUserProfile()-> String?{
        
        // protect the usage counter from thread manipulation
        dispatchQueue?.sync {
            usageCounter += 1
        }

        return service?.getUserProfile()

    }
    
    func setService(service: APIProtocol){
        self.service = service
    }
}

MyAPILoginManager adds some additional complexity and requires a more detailed breakdown:

  • service:APIProtocol?: the service variable is of the protocol type. That's so any service that conforms to that protocol can be set to the variable. Implementing a protocol makes the implementor the same type as the protocol.
  • initializers: the initializer without parameters shows injection of a different service on-demand with the setService method.
  • callLogin() and callGetUserProfile(): calls the injected service's implementation of login and getUserProfile, respectively.
  • setService(service:): used to inject services dynamically.

Create the objects and make the final test calls. Place some print statements into the different services and see how the calls are made when switching the service.


let loginService = APIService.init(uName:"user1", passwd:"password", key:"KGA-43F-554-FGT", bURL:"mybaseurl")
let myAPIManager = MyAPILoginManager.init(queue:"my.service.queue.identifier")
myAPIManager.setService(service: loginService)
myAPIManager.callLogin()
myAPIManager.callGetUserProfile()

let totallyDifferentService = TotallyDifferentAPIService.init(uName:"user243", passwd:"password1234", key:"F52-685-777-657-EFA", bURL:"totally_different_base_url")
myAPIManager.setService(service: totallyDifferentService)
myAPIManager.callLogin()
myAPIManager.callGetUserProfile()

Here's a breakdown of that code:

  • Create a single instance of MyAPILoginManager
  • setService method called to set the first service
  • callLogin and getUserProfile called.
  • Change to a different service
  • Call that service's unique callLogin and getUserProfile calls.

Based on the evidence from the sample code, dependency injection is a very modular design pattern. A singleton designed to log into many different services would be a single, enormous class that would be difficult to maintain.

You'd have to add many additional variables, api keys, URLs, and more. Alternatively, a new singleton could be created to make the other service's API calls. The problem is there would be two global variables to track and debug. Not recommended.

Singletons with Dependency Injection

Can dependency injection also be a globally accessible and single-source class? Look back at the code for dependency injection. Since the service dynamically uses the setService method of MyAPILoginManager the following is possible:

  • create an instance variable named shared that calls the empty initializer (removing the other initializer).
  • make the empty initializer private.
  • create a method setQueueName to set the dispatch queue on the fly
  • set the services when they're needed, as per standard dependency injection.

That will result in a globally accessible, dependency injectible, singleton.

The question that comes to mind is: what's the purpose? The advantage of the singleton is the ease of grabbing an instance and calling its functions without adding code to the calling class dynamically. Outside of threading issues, it is easy to debug.

Pros and Cons

Just because dependency injection is the hot item of the day doesn't mean immediately refactoring an entire apps' code to work with it. Nor should it be adopted if singletons are very familiar and comfortable.

On the other hand, all those people out there demonizing singletons aren't creating problems from thin air. Dependency injection is alluring with its modular features. So how does one decide which route to take?

First, always keep in the back of your mind that these are solutions to different problems. Examine the problem you're trying to solve and pick the solution that best suits it. It's not always an either/or proposition. The combination of both may be ideal.

Whatever you do, don't discount one or the other based on internet punditry.

So, given that, onto the pros and cons list.

Singletons

Using a singleton when making simple applications can work well:

Pros:

  • Quick, easy, instantly accessible everywhere.
  • Doesn't require a lot of code to implement
  • Single class holds a significant amount of the required code
  • Dependency injection splits functionality between multiple classes and is complicated

On the flip side, singletons are rightly derided for a host of reasons:

Cons:

  • Share global state between all instances. Bugs are difficult to debug
  • Encourage quick, unrelated, additions to the base code to resolve issues that arise. This may result in spaghetti code
  • All consumers of the singleton can access updated code.

The most common problem is feature creep. Before you know it, a singleton with a strict single task to communicate to the database is soon making network calls, attaching to services, and doing logging.

If you know this up front then you can plan in advance how to combat "singleton creep". Just don't do it! If you carefully design singletons and call them responsibly, they can be fine solutions.

Dependency Injection

The pros and cons list for dependency injection contains a convincing set of pros:

Pros:

  • Flexibility and Modularity. The pattern permits adding and removing new functionality in a straightforward and simple way
  • Dynamic addition of dependencies on an ad-hoc basis.

Cons:

  • Volume of code. Dependency injection requires a larger number of classes and protocols.
  • Complexity. the pattern and additional code and classes add complexity and can be difficult to comprehend

Consider the modularity of dependency injection if similar features to the current set are on a roadmap. Testing is another major consideration. Dependency injection permits easier testing by allowing easy substitution of stub testing frameworks into services.

Just remember, unless you are creating a global dependency injection per the code specified above, you're not dealing with the globally accessible issues that a singleton solves.

Summary

Singletons implemented without care are a bad idea but as long as you understand the pitfalls, you can be successful using them.

Dependency injection is a different creature. It solves a host of unique problems and decouples your code into modular classes. Unfortunately, it will also require more coding and may impact the complexity and readability of your code.

There's no silver bullet. It would be a mistake to state one pattern is "the one" over another since they're each unique.

First, learn and make sure to understand both patterns in more detail than presented here. Cursory knowledge won't help you make the right decision. If misinformed, it could lead to terrible results.

Second, understand that there are evangelists out there with honorable agendas but are facing different challenges that skew their opinion. Be twice as suspicious of those on the internet telling you that you're making a terrible mistake in judgment and must not ever use a singleton.

Knowledge will empower you and if you have a good grasp on both design patterns, you can make properly-educated decisions.