Dependency injection (DI) is not an easy concept to understand, and applying it to new or existing applications is even more confusing. Jess Smith shows you how to do dependency injection without an injection container in the C# and Java programming languages. In this article, I'll 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 by the acronym
SOLID ). The D in SOLID refers to Dependency of Inversion (DOI), which later became known as dependency injection. The original and most common definition: dependency inversion is an inversion of the way a base class manages dependencies. Martin's original article used the following code to illustrate the dependency of a class
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 parameter list or types of a method
WritePrinter
, you need to implement updates wherever there is a dependency on that method. This process increases maintenance costs and is a potential source of new errors.
Another problem: the Copy class is no longer a potential candidate for reuse. For example, what if you need to output characters entered from the keyboard to a file instead of to 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 introduction of a new dependency
WriteDisk
, the situation did not improve (but rather worsened) because another principle was violated: “software entities, that is, classes, modules, functions, and so on, should be open for extension, but closed for modification.” Martin explains that these new conditional 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 types
Reader
and
Writer
, making it possible to reuse them with different implementations. But if all this seems like some kind of gobbledygook 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 customized for 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's a simple example:
HtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Any project using this class code will have a dependency on the class
HtmlUserPresentation
, resulting in the usability and maintainability issues described above. An improvement immediately suggests itself: creating an interface with 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 instantiating the type
HtmlUserPresentation
, we can now use the interface type instead of the 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
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 using the type are still aware of its existence. To remove this direct dependency, you can pass an interface type
IHtmlUserPresentation
to the constructor (or list of method parameters) of the class or method that will use it:
public UploadFile(IHtmlUserPresentation htmlUserPresentation)
The constructor
UploadFile
now has access to all the functionality of the type
IHtmlUserPresentation
, but knows nothing about the internal structure of the class that implements this interface. In this context, type injection occurs when an instance of the class is created
UploadFile
. An interface type
IHtmlUserPresentation
becomes reusable by 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 directly depend on each other when one of them instantiates another to gain access to the functionality of the target type. To decouple the direct dependency between the two types, you should 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 class constructor or method, the class/method that needs the functionality does not know any details about the type implementing the interface. Because of this, an interface type can be reused across different classes that require similar, but not identical, behavior.
- To experiment with dependency injection, look at your code from one or more applications and try converting a heavily used base type into an interface.
- Change the classes that directly instantiate this base type to use this new interface type and pass it through the constructor or parameter list of the class method that will use it.
- Create a test implementation to test this interface type. Once your code is refactored,
DI
it will become easier to implement, and you will notice how much more flexible your application becomes in terms of reuse and maintainability.
GO TO FULL VERSION