TL;DR
No language accurate of a framework for Dependency Injection (DI - Dependency Injection). A framework is only interesting when it saves your work.
No one accurate of a framework
According to the DI concept presented in the question, it is true that "in Ruby, such frameworks are unnecessary", but also in any other language.
As we will see below there are several simple and direct techniques that allow to do Dependency Injection or some type of Inversion Control (Ioc - Inversion of Control) no need for a framework in any Object-Oriented language.
DI techniques without frameworks
Making Dependency Injection without frameworks is relatively simple.
Builder
As in the example presented in the question, the object simply receives the type or instance it depends on in its constructor. This is one of the simplest ways to do this without the need for a framework, being easily implemented in any language.
Method Setter
An alternative would be to have a method Setter to receive the dependencies, which is bad in the sense of being less intuitive to the developer, but would allow to meet the case of a circular reference.
Class Attribute
Another possibility, which is usually implemented by framerworks, is to inject the dependency directly into the attributes of a class, even if they are private. This is done using some kind of reflection on the objects managed by the framework.
What is meant by Addiction Injection
In particular, I would classify the question example as some specific type of Inversion of Control, but not purely an Injection of Dependence. At best, I believe that the example stands at a certain threshold between being and not being.
Different from how the official definition of DI, the example code does not provide the class B
a ready-to-use component, but a type.
Although you did take A
the responsibility of discover what is the correct implementation, A
remains responsible for creating your own dependency.
What if B
have an addiction C
? What if C
have an addiction D
?
Finally, the example does not satisfy some requirements that gave rise to the concept of DI, such as the case of a complete dependency hierarchy.
Control Inversion vs. Dependency Injection
DI is a type of Ioc, related to dependencies.
However, Inversion of Control is a more abstract way of taking a responsibility from certain classes and leaving it in charge of other more specialized.
For example, any framework with controllers (controllers) where the name of the methods matches the Urls that meet or that are mapped using routes. These frameworks generally implement the standard Front Controller, which is a "master" controller that reads the HTTP parameters and directs the request to the appropriate controller.
Another example is entities mapped to the database. You simply define attributes and certain properties and some other class of a framework is responsible for reading and writing from a table.
Note that you do Control Inversion whenever you implement a "passive" class, that is, it is not executed directly, but through a manager or indirectly.
DI is more than just passing objects
On the other hand, an Ioc Container, or Inversion Control framework, such as Spring in Java, is responsible for more than just instantiating objects and passing by parameter.
Declarative notation
Use annotations as @Inject
or @Autowired
is a declarative way of defining dependencies and delegating the injection to methods, attributes or constructors.
You do not need to manually link components or have instantiation code within your classes. The framework implements the Assembler, that will create and link your components.
Less code to maintain, less maintenance and less headache.
Manage the object graph
Most real cases of dependencies are not as simple as the example.
As already mentioned, let A
is responsible for creating or even managing the dependencies of B
would be a clear breach of the DI principle.
A DI framework will instantiate all your objects and place them in a special context, determine their dependencies that are properly declared and try to fill in the necessary links. It will warn you if there are problems creating one of the objects or if any connection is missing.
Inversion of Configuration Control
The problem of making DI with code is that you keep coupling the form dependencies hard-coded.
There are many components that can be reused in different contexts with some parameter with different configuration. For example, a component that monitors directories and copies files could be instantiated several times pointing to different directories and then could be linked to different file import components.
Configuring dependencies via XML or other external file increases reuse and allows different application settings without modifying the code.
Although no one likes XML very much nowadays, I also don’t like code with ifs
to check if you are in a test or production environment or a large amount of meta information "soiling" my classes.
Scopes
It is not always desirable to create a new instance of dependencies with each injection performed. It is not always desirable to inject the same instances everywhere.
The scope of each component may vary.
Imagine the case you want to inject a data source for connection to the database that depends on the user currently logged in to the system. It would be a session scope.
Or else you want to inject the system settings into all your classes. We have a global scope (Singleton).
Of course you can do this without a framework, but in your example you would have to refactor the code if you decided to change from one scope to another, while with a framework you would just change a simple configuration.
I can well understand why many people have an aversion to frameworks or anything that is very "heavy".
You see, there are two main approaches that we can use when the architecture of an application is going to be defined:
Complete architecture
We can start with an architecture complete, as a Java EE application server, and then select the Components that are needed.
This option is considered the most "secure" by many developers. Even you carrying a "truck" of unnecessary things, the day you need something, will be there.
However, things are not great. Development is slower and it is more difficult to debug the system in a more complex environment.
Minimalist architecture
We can still start with an architecture minimalist, adding then only what we need.
This is the most flexible option, but requires deeper knowledge about the platform, technologies and how to create a quality implementation, otherwise you end up with a tangle of gambiarras.
What does this have to do with DI?
In the first approach, the developer can already use an DI framework from the start, without thinking if he really needs it. In the other approach, he will have to think about whether he will need and then add in his architecture.
Both approaches can result in a good or bad product, depending on several factors, but mainly on the experience and knowledge of the team.
Personally I prefer the second option, especially if working alone. However, if not all your colleagues have the same ease of quickly changing the code, refactoring and restarting parts of the application from scratch, then the first approach becomes more attractive. After all, when you have hundreds or thousands of people working in your company, it’s easier to teach you how to use a framework than all the concepts of good design so they can do it for themselves.
Considerations
No language needs a DI framework, but knowing how to use them correctly can free the developer from writing and maintaining good amount of code, and prevent it from re-inventing certain wheels.
Many problems with DI frameworks are related to:
- Not understanding how the framework works
- Not properly performing framework configuration
- Having a bad (very coupled) or complex object model in itself
- Not knowing how to properly debug problems (logs, error messages)
In addition, there are all kinds of flavors of DI framerworks: simple, complex, small and large.
I know it’s not easy. For those who are starting it’s hard to understand what the framework does. It’s hard to understand design patterns. It is difficult to simply divide the implementation into classes and methods.
I’ve gone back and forth on this matter of whether or not to use frameworks, whether for DI or some other application. Sometimes I stopped using frameworks because I didn’t understand how they worked. So I improved my code and came to the conclusion that I had done the same as the framework already did. Then it is easier to choose to use them or not.
I found a very interesting topic about different approaches to implement Ruby dependency injection in the link below. It’s the second part of 3 posts addressing Object-Oriented Design tips with Ruby. It’s worth checking out. - http://shipit.resultadosdigitais.com.br/blog/dicas-de-design-orienteda-objetos-com-ruby-parte-2/ I hope it helps.
– Samuel Rodrigues