Dependency injection, also known as "inversion of control" (IoC), is the art of constructing modules in a way that allows them to receive input regarding their behavior. A compassionate and meticulously designed codebase is constructed so that developers can quickly dismantle, analyze, and understand the code base.
Inversion of control refers to the control that a class has over its dependents.
The inversion is taking that control away and granting it to the code integrating the class. Instead of strictly defining the behavior of the dependent, the class or module can simply define the abstract rules its dependents must follow.
💡Type interfaces like Prototypes, Interfaces, and Templates help to enforce the rules that make dependency injection productive.
Dependency injection is a powerful technique that helps improve code maintainability, testability, and overall design flexibility.
It allows developers to write more modular and scalable code by decoupling components and making them easier to understand and modify.
The adoption of dependency injection as a technique can also contribute to the realization of compassionate coding goals. By defining expectations, setting an example, and allowing others to make necessary decisions creates an expansive coding environment.
🧠Insufficient Facts Always Invite Danger
Often classes and other constructs are baked in or designed with integrated dependencies. Motivated by a sense of care, craftsmanship, and convenience the programmer does not want others to have to worry about the dependencies of his class.
The programmer's intention is to spare other developers the burden of dealing with the dependencies of their class.
But this baked in style embedded approach carries its own set of trade-offs.
Swift is popular with iPhone developers due to its performance and integration with Apple's ecosystem.
However, as a language it has limitations in terms of reflection capabilities necessary to build Inversion of Control (IoC) modules such as Windsor Castle or Dagger.
With that in mind let’s create dependency injection using very basic tools available to us.
In this example notice that newing
up an instance of Tribble
results in a dependent instance of Trouble
.
Whenever there are Tribbles, there will always be Trouble.
However, by changing this implementation slightly from let to var,
other programmers can replace the value of behavior with something similar.
👥The Needs of The Many
By defining a prototype called TribbleBehavior
, other team members are free to redefine the behavior of your tribbles.
The Swift code could now look something like this.
When no behavior is defined, The (!)
operator grants flexibility and immediate programmer feedback. The var
allows the behavior to be changed at any time.
The prototype definition of TribbleBehavior
provides abstract requirements.
Team members are now free to define new behaviors. In their tests, and in their implementations.
Flexibility not only for testing, but also for expanding your Tribble class
to do other things.
The multiply function can return 1 tribble, 0 tribbles, 100 tribbles.
The callback function can be time delayed or immediate. It could read from a file or go to the web.
All possibilities are open.
Team mates are free to expand on your design without needing to change your implementation. Helping to save time in debugging, and making it possible to quickly route out problems that surface under specific circumstances.
🧐Assumptions Are Made
It’s easy to make assumptions. It’s much more difficult to clear them.
Suppose our Tribble class had a complex implementation of a function called findFood. findFood
is an opaque function with no outputs. It will do some computation and then call eat()
once certain conditions are met. If eat()
returns false, then multiply()
is called.
The input for findFood
would be the different landscapes that the Tribble finds itself in. The goal is to test for the expected output of the Tribble in each landscape.
To test an opaque function like findFood
, a testable TribbleBehavior
class can be introduced. Specifically, to determine when and if the behavior functions in the abstract class are called.
The injected code inside the Tribble class
creates a window for tests to control the Tribble and find any unexpected behavior.
No code is perfect, it’s easy to make mistakes.
Transparency and humility help make it possible to find the small flaws that ruin good user experiences.
By redefining the behavior of the Tribble class,
the opportunity for increased test coverage enables a reduction in the number of assumptions made.
🖖Live Long and Prosper
Every language, whether it’s functional, object oriented, or JavaScript can handle dependency injection in some form or another. It’s just a matter of creativity and keeping an open attitude.
Programmers can sometimes get lost in the myriad of options modern languages like C# and Java provides. Taking a defensive posture to defend their code. Using unnecessary private variables, baking in concrete implementations, or providing no output.
Solidifying the code so that it can only follow one untestable path. Removing ones-self from this is a test in humility, and requires us to trust that we are not indispensable, and the code will continue to function even in our absence.
Come say hi 👋 I love to chat at LinkedIn
Isn't failing fast a bit unsafe here? Is there reason not to use an initializer? I can see an issue as more properties are added (by less knowledgeable engineers who are in a hurry) that this won't get caught by an error in the initializer.