The Problems with Singletons and Why You Should Use DI Instead

fatihcyln
10 min readOct 4, 2022

--

Hello developers, in this article we are going to deep dive into Singleton and Dependency Injection. We will get the all answers at the end of the article.

What is Singleton?

Singleton is a design pattern that well known by all developers and also it loved by all new developers. I am saying all new developers because every course or tutorial series use this design pattern and it is okay to use till some point.

Wait, don’t you know how to use Singleton?
Don’t worry it is easiest thing to use.
You have to create an instance of that object using “static let” keywords in the scope of the object and then you have to make that class’ initializer private. If you don’t make its initializer private, you can create instances of that object as much as you want, and it is really really bad thing. You have to ensure that you only have one instance of that object.

You all see that it’s so easy to use but sometimes easiest path may cause some big problems. What problems?
Eventually you will get thread issues because Singletons are global and it is accessible by all threads, you will get non-customizable class, you will get non-testable class and you will not be able to swap out dependencies for testing purpose. It is not a big thing, eh? I am kidding!

Before jumping into problems with singleton, let’s take a look at the project that I made for this article and see how to use it.

Use of Singleton

I am using MVVM for this project. We will have service layer, view layer, view model layer and model layer.

I’m using some mock data from https://jsonplaceholder.typicode.com to make an API request in order to simulate real world application.

I created a service layer called RealDataService. It will be responsible for making API requests.

Notice that we have an instance called “shared” and it is “static let”, also our RealDataService’s initializer is private.
Result type, @escaping closure, and URLSession, etc. are not the scope of this article, I assume that you already know it.

Here our view model. We are reaching out our singleton in getPosts() function and using its downloadPosts function. If it succeed, our posts variable in HomeViewModel will send a notification to the view. If you’re confused of use of ObservableObject, don’t worry I will write an article about different MVVM approaches.

Here our view, aka ViewController. We’ve initialized our HomeViewModel and we’re listening the changes of posts variable in HomeViewModel.

That’s all, the use of singleton simply as it.
It’s time to talk about its downsides.

What is The Problems with Singleton

Singleton works pretty well, but it has 3 major problems.

1- Singletons are global.
2- You can’t customize the initializer of the class.
3- You can’t swap out dependencies.

Singletons Are Global

They are global which means they can be accessible everywhere in app. You can access a singleton, without creating an instance of it, directly from another class, class’ methods, or not being in a class’ scope.

Maybe you could say something like that, “So what? It’s perfect, isn’t it? I can access it anywhere I want.”

Since we are in multi-threaded environment in swift, we have bunch of threads to do different tasks at the same time.

Instance of classes store in Heap, since we are using singleton there is only one instance of our class and we can reach that instance from different threads at the same time.
When you reach that instance from different threads at the same time, you will get “swift access race” error and probably your app will crash.

Non-customizable Initializer

You are initializing your class within that class, because of this reason you can’t take parameters. This is a problem because maybe you want to initialize that singleton class with URL or something but you can’t do it.

You Can’t Swap Out Dependencies

Swapping out dependencies? What does it even mean?
In larger apps you probably have more than one service and you have to make tests for be sure that your class working well for different data services. Generally larger apps uses mock data service for testing purpose or in order to not mess with real data service. We use protocol to achive this. Protocol?

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

We’ve learned 3 major problems with singletons but we haven’t learned what we should use instead of singleton.

Dependency Injection

Dependency injection is a way to avoid use of singletons and make your code better for testing and easy to swapping out dependencies. It’s considering as so much complicated but it’s not. You just need to inject your dependency to all the views, view models which need that dependency. That’s all, it’s not complicated.

In our example, instead of initalize our dependency (in this case our dependency is going to be RealDataService) within the RealDataService we should initialize it pretty much early on in our app, almost at the beginning of our app and then inject it into the rest of our app.

We had initialized it as a singleton but we’re not going to do that anymore. We will initialize our dependency at the beginning of our app, I mean in the SceneDelegate.swift file.

We will need that dependency in view model, so we have to initialize it with needed dependency. Also we will initialize our view model within view, so we have to initialize our view with needed dependency too.

Let’s imagine we are in the classroom, and there are 3 desks in the classroom.
In our example let’s say pencil is our dependency.
First desk and second desk have no pencil, but as a 3rd person in the 3rd desk we are good boy and we have pencils. We will give 2 pencil to the 2rd person, and the 2rd person will give a pencil to the 1st person, as you can see we all have pencil.
The first desk is view model.
The second desk is view.
The third desk is SceneDelegate.
SceneDelegate(3rd) — > View(2nd) — > View Model(1st)

So we had injected our dependecy into the view and view model.

We removed singleton in RealDataService class.

We are initializing our view model with needed dependency (RealDataService).

Our view model has to be initialized with needed dependency but view doesn’t have that dependency, so we have to initialize our view with needed dependency too. (We are injecting our dependency into the view model.)

We are injecting our dependency into the view.

Here is the posts’ titles. We’ve got data successfully from api.

That’s all folks, we’ve injected our dependency into our view and view model. We’ve initialized our dependency in SceneDelegate but we can’t initialize all our dependencies in SceneDelegate. Let’s assume that we have second view and when we tap a button we will navigate to the second view. Also let’s assume that we have another data service for second view, if this is the case, we have to initialize that view with needed dependency and probably we should initialize our dependency within first view (HomeVC) and inject it into second view.

We’ve solved our first problem. We’re not using singleton so we fixed our “singletons are global” problem.
But what about others?
Did we customize the initializer of our dependency?
Did we swap out dependencies?
No we didn’t yet but we will.

Custom Initializer for Our Dependency

What if we want to initialize our dependency with some extra data, such as string for url? No problem, we can customize our dependency’s initializer and pass whatever data we want.

In our example let’s pass some string that holds url address to our dependency.

We’ve customized the initializer. We have to pass an string when we want to initialize RealDataService.

We’ve just solved our 2nd problem which is non-customizable initializer problem.

What About Protocols

Swapping out dependencies is a necessary for big apps. We’ve little bit talked why we need that. We want that our dependencies can be swappable because we want to test this app, we want to give another data service to view model and view rather than RealDataService, thus we can observe how our view model, view or other data layers react that dependency. There are lots of reasons to want our dependencies can be swappable.

Where protocols comes in.
Look at our example, we’re injecting RealDataService into view and view model. What would happen if we inject a protocol rather than one data service?

Let’s create our protocol and give it a name called “DataServiceProtocol”

Protocol will be kind of a blueprint for our class, when we conform DataServiceProtocol to our classes, we will have to implement downloadPosts() function.

We have created our protocol, let expect DataServiceProcotol as parameters and pass class that conforms DataServiceProtocol as argument.

Our RealDataService protocol conforms DataServiceProtocol and our class has downloadPosts method so it won’t give us an error.

Also we have to change our view model and view’s initializer parameter type to DataServiceProtocol.

dataService type changed to DataServiceProtocol in view model.

HomeVC is expecting a class that conforms DataServiceProtocol.

Notice that we are passing RealDataService as argument to HomeVC and HomeVC is expecting a class that conforms DataServiceProtocol.

Right now we have to inject a class that conforms DataServiceProtocol to our view. Since our RealDataService conforms DataServiceProtocol, there is no need to do extra work. All of our codes will work properly.

Let’s make a recap.
1- We’ve created a protocol and gave it a name called “DataServiceProtocol”.
2- We’ve put a function without body in our protocol.
3- Since we’re conforming DataServiceProtocol to our RealDataService we have to implement needed downloadPosts() function but we have that function in our RealDataService already so there is no need for extra work.
4- Expect DataServiceProtocol as parameter.
5- Inject your dependency that conforms DataServiceProtocol to your view and view model.

Swapping Out Dependencies

You can ask yourself “Okay but we haven’t swapped out our dependecy, when we will make it?”. This is the last step. Let’s create mock data service and inject that data service into our view and view model.

Let’s create our MockDataService class and conform DataServiceProtocol.

Xcode yells us because we haven’t implemented needed function.

Here our needed function. We will simulate downloading posts process in this function.

Our job has done with this MockDataService. We are expecting Post array as parameter and we will send that array to our view model with closure. Because of we are simulating downloading process we will execute completion block after one second.

There will be just one little step which is initializing our MockDataService with post array and inject that into our HomeVC.

We’ve created a post and initialized our MockDataService with that post.

Yep, that’s all. We’ve injected our MockDataService into rest of our app thanks to protocol. Let’s see if it actually works?

Yep, console is printing our post title.

The End

We’ve talked about singletons, protocols, and dependency injection.
We’ve learned 3 major problems with singletons and how to avoid that problems. We’ve talked about why we need dependency injection, and how to inject our dependencies. We’ve made our dependencies swappable and initialized with some data.

I hope you’ve learned something and I hope you’ve understood concepts of singletons and dependency injection.

If I made some mistakes, please reach me out.
LinkedIn: https://www.linkedin.com/in/fatih-kilit-10aa33203/
Source codes: https://github.com/fatihceyln/DependencyInjectionArticle

--

--