Dependency injection and Singleton. Everything you need to know and a little more

Ivan Fomenko
7 min readMay 14, 2023

--

Good afternoon, colleagues, in this article we will talk about such a phenomenon in design as DI (Dependency injection) and Singleton. I’ll try to explain what DI is and why you shouldn’t be afraid of Singleton.

The current article may not be of much interest to experienced developers, but should be useful for beginners. So, to the point.

Let’s start with the terminology.

What is a Singleton?

Singleton is a generative design pattern that aims to ensure that a class has only one instance and provides a global access point to it.

This pattern helps to solve the following problems:

  1. The need for only one instance of the class you need to exist at a time.
  2. The need to access the object from any thread and scope.
  3. It is necessary to hide the constructor from external access.

Let’s imagine a hypothetical situation when you have N-number of screens or other objects that need to receive the same information. How to guarantee equal and universal data delivery? For such a situation, the Singleton spawning pattern will come in handy.

A real-life example of this pattern is the main Core library of iOS itself. You’re guaranteed to come across NSNotificationCenter.default or any classes whose methods you call via .shared, .default, or .system. This is a Singleton — an object generated by a static initializer.

But if it is so freely and widely used by Apple, why do many authors warn against using this pattern?
Because Apple uses this pattern precisely to solve the three previously mentioned problems.

For example, in the case of NSNotificationCenter, they need to guarantee you the ability to send and receive messages from one center anywhere in your application. They also need to hide initialization details to ensure correct system operation. But this creates the need to constantly save this object in the application’s memory. This is where the main danger of comprehensive use of this pattern lies.

Abusing the use of Singleton objects will lead to a situation where your application will always keep everything in memory. After all, in the development process, you may have direct links or dependencies on an excessive number of singleton objects. Because of this, ARC simply won’t have the right to deallocate objects of your classes.

In addition, classes using Singleton are rather problematic to test and add in the form of abstractions.

That is, by itself, judicious use of Singleton does not lead to guaranteed disaster or problems. But if you abuse this tool, you are guaranteed to face the above difficulties.

Because of this, I advise you to use Singleton pointwise and only in the case of the need to solve access problems and the need for a single instance to exist.

What is Dependency Injection?

Dependency Injection is a design pattern in which an object or function inherits other objects or functions on which it depends.

DI aims to separate the processes of creating and using objects by other classes, resulting in a certain decentralization of your system.

In the example, you can see 2 initializers.

In the first case, a dependency is created in the middle of the class. That is, we mix the tasks of creation and use.

In the second case, our ViewModel expects to accept an external dependency from the outside. Whether it’s a factory, assembler, or DI container. In this case, it is not so important for us.

That is, DI — is not a way to build, but a way to manage your dependencies.

Why should I create objects outside the base class?

  1. To be able to fully test your code. After all, at any moment, I can replace the MyService implementation with any other one with the scripts and behaviors I need.
  2. In order to separate the tasks of creation and use.
  3. To control the type of dependencies provided to your class. After all, with the use of DI-containers, you can define the type of stored dependency and, if necessary, transfer it to a weak, direct or singleton type of link.

How and when to use DI Container?

If you start using any of the off-the-shelf DI frameworks, be it Swinject or Weaver, you’ll come across an entity like Container.

Next, I will look at examples using Swinject. This framework is the most popular, the most supported and the most convenient for me personally. You can consider custom implementations or other frameworks depending on your views and needs. In most cases, the main entities will match, but may cause differences in implementation.

Container — is a global repository and registry of your dependencies. Like any registry, it must also exist in a single instance. But this does not mean that you need to create a Singleton version of it. A single initialization point will be enough — in the “heart of your application”.

Creating a Container

The easiest way — create it as a private variable in AppDelegate, a reference to which you will pass later. It can look like this:

Registration of dependencies in Container

So next I will show several methods of creating dependencies inside a Container.

A Container, be it Swinject or another variant, will always have 2 main methods: register<T>(...) and resolve<T>(...) -> T

register<T>— method to register your dependency. That is, instructions (clouser) how to initialize it.

resolve<T>() -> T— method of issuing the object corresponding to the registered key.

Because Container is by its very nature a large hash table, you will always need a key to identify your dependency to register a dependency. It can be a simple self-generated key or the name of a class or protocol. For example, MyClass.self.

I recommend using class or protocol names as keys. This will relieve you of the need to remember, validate and generate these keys. Instead, a key bound to a specific class or protocol will give you readability, stability, and self-testing tools (eg if the class name changes, you can use the Rename IDE tool for updates, or else your project simply won’t compile and you can correct the error).

You can register a class as follows:

But registration by key is not very convenient, because you need to remember our Archie. So I suggest modifying the registration a little:

Swinject offers us the ability to register dependencies with parameters, that is, with the ability to pass parameters to the initializer. But be careful with this functionality — it is very picky about the type of data being transferred and is only suitable for something simple.

To be honest, I’ve never been able to find a good use for this functionality, so let’s rework our code a bit:

We added a generating pattern — Factory — and registered it. And now we have the opportunity to create any cats using the container. After all, in this case, our cat object — is not what Container should store. A container must store spawners, logic implementation classes, controller classes, but not entity classes or data types.

I suggest modifying our code a little more:

So we registered our Factory as an abstraction and made it impossible to access the implementation via the Container.

This approach is very useful and allows not only to limit access for users, but also to solve additional problems:

1. Issuance of various implementations depending on external conditions. For example, depending on the specific region, you need to provide a different implementation:

2. Return of mocked implementations in case of testing using Container. Due to the fact that we are not tied to a specific implementation — we can return any object, the main thing is that it meets the requirements of the protocol. This allows us to use the container as a source of mocked and fake objects during testing.

DI & Singleton

With the help of Dependency injection, it is also possible to issue Singleton entities to the user for his classes, without informing the classes themselves that they receive a Singleton.

That is, we are again not tied to a specific implementation, and can update the storage policy without involving our implementation. Example:

In the second example, I added registration using ObjectScope.

ObjectScope— is a configuration parameter that defines how the instance provided by the DI container is shared across the system.

Swinject features 4 Object scopes:

  1. .transient
    If ObjectScope.transient is specified, the container always creates a new instance when you call the .resolve() method. It is worth noting that for this type of objects, the automatic cyclical resolution of dependencies will not work properly.
  2. .graph (default)
    With ObjectScope.graph, an instance is always instantiated, just like with ObjectScope.transient, if you call the container’s resolve() method directly. But the created instances are shared when resolving the root instance to build the object graph (which is not the case with .transient).
  3. .container
    If ObjectScope.container is specified, then the first time you call the .resolve() method on that type, it is created by the container, and it is this instance that will be returned by the container on any subsequent resolve() calls on the type.
  4. .weak
    In ObjectScope.weak, an instance provided by a container is shared within the container and its child containers as long as there are other strong references to it. When all strong references to an instance are gone, it will no longer be shared (reuse in the middle of the container is not possible), and a new instance will be created the next time resolve() is called for the registered type.

So, I highly recommend using ObjectScope for storage type management, because it eliminates a lot of unnecessary steps on your part.

Final

We talked about Singleton, Dependency injection and ways of working with Container objects.

We learned about the main problems with Singleton elements and how to avoid them.

Furthermore, we talked about why we need to implement dependencies and how to create them.

We’ve covered several additional container capabilities and ways to manage stored class instances.

I hope you learned something or just found it interesting.

Useful links

--

--