This principle amazes me because, especially when working with dynamically typed languages, people can spend years using an object-oriented language without ever learning about it. This fundamentally inhibits their ability to progress toward becoming senior developers. So, in this article, we’ll examine Inversion of Control (IoC) in Duck-typed languages.
Software engineering’s “Inversion of Control” principle assigns control over certain program elements or objects to a container or framework. It is most frequently used in object-oriented programming.
With IoC, a framework can take over program flow and make calls to our custom code, unlike conventional programming, which requires our code to call libraries. Frameworks use abstractions with integrated additional behavior to make this possible. If we want to add behavior, we should either add our classes as plugins or extend the framework’s classes.
The advantages of IoC are as follows:
The resolution of dependencies is the most important aspect of IoC. It is widely acknowledged that the biggest issue that IoC is attempting to address is dependency mismanagement. The program’s control is inverted when control is transferred from a component (like a web form) to a framework.
Throughout the years, multiple ways have been created to achieve Inversion of Control. Examples include Strategy design pattern, Factory pattern, Service Locator pattern, and Dependency Injection (DI).
No one is forcing you to use IoC when writing your code. Nonetheless, it offers various advantages that cannot be ignored.
IoC is a pattern used to decouple system levels and components. The pattern is implemented by injecting dependencies into a component when it is constructed. These dependencies are usually provided as interfaces for further decoupling and to support testability.
Moreover, it offers many advantages, which include:
Before moving on to implementing IoC, we must understand Dependency
We can leverage the pattern of dependency injection to implement IoC. Instead of the individual objects connecting or “injecting” themselves into other objects, Dependency Injection uses an assembler.
IoC and DI are both straightforward ideas, but because they hugely impact how we build systems, it is crucial to understand them thoroughly.
Let’s look at an example to grasp better the DI pattern and how it achieves IoC.
Imagine you have a typical online application for managing notes. We want a class that shows us notes organized differently, such as notes that are still pending or previously completed notes.
This implementation brings on some issues. What if we wish to alter how NoteTextStorage is implemented? The issue is that the NotePresenter class and its underlying persistency are closely related. It would be much nicer if we could send our Storage to the presenter, and it would function as before rather than changing our NotePresenter.
Dependency injection acts as a runtime glue, connecting components (in this case, the presenter and the store). We get loose coupling between these parts as a result.
How then can dependency injection be implemented in Ruby? A simple approach would be to pass it in the constructor:
Naturally, for this to function, all storage objects supplied to the presenter must implement the same interface. This approach is straightforward and easy to follow and will often be sufficient. But what if other classes, in addition to our NotePresenter, use our storage solution? Here, a configuration-like strategy would be more appropriate. Most libraries for dependency injection operate in this manner. All the parts are configured centrally, along with instructions on assembling them.
Two gems for this strategy are provided by dry-rb and are called dry-container, and dry-auto inject.
Let’s use these two gems to put our example into practice:
We create an object here that will act as a container for our configuration (our gluing of components).
Here, we bound our NoteTextStorage program to a new configuration we registered with the keynote storage.
Using the upper line of code, we configured it once we were done. We are now prepared to add our dependencies to our classes:
Here, we insert the configuration for our note storage into our NotePresenter. Through this, the configuration instance supplied is reachable via the note storage method. Let’s imagine we wish to modify how we implement underlying storage. Then, simply develop a new implementation and pass the class in our container as the note storage dependent is all that is required.
Congratulations! Everything there is to know about inversion of control in programming languages with duck-types has now been taught to you. In summary, IoC makes our code more tested, flexible for modifications, and loosely coupled. You may enhance this much more with the dependency injection method. Additionally, DI offers a means to distinguish between how an object is made and how it is used. As a result, you should now try to implement IoC in your code and test out different IoC strategies.