Dependency injection (DI) is a tricky concept to understand, and applying it to new or existing applications is even more confusing. Jess Smith will show you how to implement dependency injection without an injection container in the C# and Java programming languages. In this article, I will show you how to implement dependency injection (DI) in .NET and Java applications. The concept of dependency injection first came to the attention of developers in 2000, when Robert Martin wrote the article "Design Principles and Patterns" (later known under the acronym
SOLID). The D in SOLID refers to Dependency of Inversion (DOI), which later became known as dependency injection. The original and most commonly used definition: dependency inversion is an inversion of the way a base class manages dependencies. Martin's original article used the following code to illustrate a class's dependency
Copy
on a lower-level class
WritePrinter
:
void Copy()
{
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
The first obvious problem is that if you change the list or types of a method's parameters
WritePrinter
, you must inject updates wherever there is a dependency on that method. This process increases maintenance costs and is a potential source of new errors.
Another issue is that the Copy class is no longer a potential candidate for reuse. For example, what if you need to output the characters you enter from the keyboard to a file instead of a printer? To do this, you can modify the class
Copy
as follows (C++ language syntax):
void Copy(outputDevice dev)
{
int c;
while ((c = ReadKeyboard()) != EOF)
if (dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
Despite the emergence of a new dependency
WriteDisk
, the situation did not improve (rather worse) because another principle was violated: "software entities, that is, classes, modules, functions, and so on, should be open for extension, but closed for change." Martin explains that these new if/else statements reduce the stability and flexibility of the code. The solution is to invert the dependencies so that the write and read methods depend on the
Copy
. Instead of "popping" dependencies, they are passed through the constructor. The modified code looks like this:
class Reader
{
public:
virtual int Read() = 0;
};
class Writer
{
public:
virtual void Write(char) = 0;
};
void Copy(Reader& r, Writer& w)
{
int c;
while((c=r.Read()) != EOF)
w.Write(c);
}
Now the class
Copy
can be easily reused with different implementations of class methods
Reader
and
Writer
. The class
Copy
does not have any information about the internal structure of the
Reader
and types
Writer
, which makes it possible to reuse them with different implementations. But if all this seems like some kind of gibberish to you, perhaps the following examples in Java and C # will clarify the situation.
Example in Java and C#
To illustrate the ease of dependency injection without a dependency container, let's start with a simple example that can be converted to use
DI
in just a few steps. Let's say we have a class
HtmlUserPresentation
that, when its methods are called, generates an HTML user interface. Here is a simple example:
HtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Any project class that uses this code has a dependency on the class
HtmlUserPresentation
, resulting in the usability and maintainability issues described above. An improvement immediately suggests itself: the creation of an interface with the signatures of all the methods currently available in the class
HtmlUserPresentation
. Here is an example of this interface:
public interface IHtmlUserPresentation {
String createTable(ArrayList rowVals, String caption);
String createTableRow(String tableCol);
}
After creating the interface, we modify the class
HtmlUserPresentation
to use it. Returning to type instantiation
HtmlUserPresentation
, we can now use an interface type instead of a base type:
IHtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Creating an interface allows us to easily use other implementations of the type
IHtmlUserPresentation
. For example, if we want to test this type, we can easily replace the base type
HtmlUserPresentation
with another type called
HtmlUserPresentationTest
. The changes made so far make the code easier to test, maintain, and scale, but do nothing for reuse, since all
HtmlUserPresentation
classes that use the type are still aware of its existence. To remove this direct dependency, you can pass the interface type
IHtmlUserPresentation
to the constructor (or method parameter list) of the class or method that will use it:
public UploadFile(IHtmlUserPresentation htmlUserPresentation)
The constructor
UploadFile
now has access to all the functionality of type
IHtmlUserPresentation
, but it does not know anything about the internals of the class that implements this interface. In this context, type injection occurs when an instance of a class is created
UploadFile
. An interface type
IHtmlUserPresentation
becomes reusable, passing different implementations to different classes or methods that require different functionality.
Conclusion and recommendations for consolidating the material
You learned about dependency injection and that classes are said to be directly dependent on each other when one of them creates an instance of another in order to access the target type's functionality. To decouple a direct dependency between two types, you must create an interface. An interface gives a type the ability to include different implementations, depending on the context of the required functionality. By passing an interface type to a constructor or class method, the class/method that needs the functionality doesn't know any details about the type that implements the interface. Because of this, an interface type can be reused for different classes that require similar but not the same behavior.
- To experiment with dependency injection, review your code from one or more applications and try to convert a heavily used base type into an interface.
- Modify the classes that directly instantiate this base type so that they use this new interface type and pass it through the constructor or parameter list of the class method that should use it.
- Create a test implementation to test this interface type. After refactoring your code,
DI
it will become easier to implement, and you will notice how much more flexible your application will become in terms of reuse and maintenance.