Dependency Injection is explained in this article in a very simple manner for all levels of developers to understand it properly.
Introduction
Dependency Injection is a concept that most of the developers struggle to grasp at first. Honestly, it is kind of intimidating. I surely took a big chunk of time to understand it properly. Here, in this article, I’ll break down the concept to help my fellow developers to understand DI and why we need it.
Points to Focus
- What is dependency injection?
- Why do we need DI? What problem does it solve?
- How do we implement it?
I could go on with more questions but, I won’t, because I don’t want to complicate things. Let’s learn things clearly in the simplest way possible.
Sample Project: Initial Steps
Let’s write a simple console application program without dependency injection with one rule in order to understand the problem.
Rule: The application code must be testable. (Testable = The code must facilitate Unit Test implementation)
Step 1: Open Visual Studio.
Step 2: Create a new console project. Name the project DependencyInjection
.
Step 3: Add a new code file to the project. Name the file OrderProcessor.cs.
Think of OrderProcessor
class that provides functionality to process Orders
by having them as argument.
So far, simple enough.
(Imagine it as a very core level implementation of Amazon Order Management system. Most of us have placed orders in the Amazon shopping platform.)
In order to have an OrderProcessor
mechanism, we need to have an Order
at first. So, we need two classes now.
Classes to Implement
Order
: a class with properties of an order. In other words, it is a model. (Model = Class with just properties, commonly used to resemble a table in the database). OrderProcessor
: a class with an Order
object processing mechanism.
namespace DependencyInjection
{
public class Order
{
public int Id {get;set;}
public int CustomerId {get; set;}
public decimal TotalAmount {get;set;}
public int Status {get;set;}
public List<OrderDetail> Details {get;set;}
}
}
Hold on, what is that OrderDetail
? Introducing new class: OrderDetail
.
OrderDetail
: a class that contains product information placed within that a order.
Now, Products
are also individual objects, so we need to create a model for Product
as well.
Product
: a class with properties of a regular product. (example, Pen
is a product).
using System.Collections.Generic;
namespace DependencyInjection
{
public class OrderDetail
{
public int OrderDetailId {get;set;}
public int OrderId {get;set;}
public Product Item{get;set;}
public int Quantity {get;set;}
}
}
namespace DependencyInjection
{
public class Product
{
public int Id{get;set;}
public string ProductName {get; set;}
public decimal Price {get;set;}
}
}
Now, our basic models are ready. Let’s picture them.
Step 4: Create a folder named Models in the project and add the above devised models inside that folder.
Step 5: Let’s implement OrderProcessor
class.
Requirement:
OrderProcessor
should take an Order
object as an input and process it. OrderProcessor
should validate the order
details using a ValidateOrder
class. OrderProcessor
should have a capability to print processed order details using PrintOrder
class.
Introducing new classes: ValidateOrder
, PrintOrder
ValidateOrder
: a class with validating the order mechanism with having Order
object as input. PrintOrder
: a class with capability to print the detailed information about the given Order
object.
using System;
namespace DependencyInjection
{
public class OrderProcessor
{
Order order;
public OrderProcessor(Order order)
{
this.order = order;
}
public void ProcessOrder()
{
Console.WriteLine("Process Started.");
ValidateOrder validateOrder = new ValidateOrder();
if (validateOrder.CheckAllDetails(order))
{
foreach (var orderDetail in order.Details)
{
order.TotalAmount += orderDetail.Item.Price *
orderDetail.Quantity;
}
PrintOrder printOrder = new PrintOrder();
printOrder.PrintAllDetails(order);
Console.WriteLine($"Order : {order.Id} has been processed.");
}
else
{
Console.WriteLine($"Order : {order.Id} processing failed.");
}
Console.WriteLine("Process Ended.");
}
}
}
namespace DependencyInjection
{
public class ValidateOrder
{
public bool CheckAllDetails(Order order)
{
return order.Id > 0;
}
}
}
using System;
namespace DependencyInjection
{
public class PrintOrder
{
public void PrintAllDetails(Order order)
{
Console.WriteLine($"OrderID : {order.Id}");
Console.WriteLine($"CustomerID : {order.CustomerId}");
foreach(var orderDetail in order.Details)
{
Console.WriteLine($"Product Name : {orderDetail.Item.ProductName}
(Price : {orderDetail.Item.Price})(Quantity : { orderDetail.Quantity })");
}
Console.WriteLine($"TotalAmount : {order.TotalAmount}");
}
}
}
Let’s picture our worker
classes.
The Main Program
Let’s take a look at our Main
program:
using System;
using System.Collections.Generic;
namespace DependencyInjection
{
class Program
{
static void Main(string[] args)
{
var productPen = new Product()
{ Id = 1, ProductName = "Pen", Price = 10 };
var productNotebook = new Product()
{ Id = 2, ProductName = "Notebook", Price = 35 };
List<OrderDetail> orderDetails = new List<OrderDetail>();
orderDetails.Add(new OrderDetail()
{
OrderDetailId = 1,
OrderId = 1,
Item = productPen,
Quantity = 1
});
orderDetails.Add(new OrderDetail()
{
OrderDetailId = 2,
OrderId = 1,
Item = productNotebook,
Quantity = 1
});
Order order = new Order();
order.Id = 1;
order.Details = orderDetails;
order.CustomerId = 1001;
OrderProcessor orderProcessor = new OrderProcessor(order);
orderProcessor.ProcessOrder();
}
}
}
Simply put, in our main program, a list of OrderDetail
object is created and it is given to an Order
object.
And by passing that order
with details to the constructor of OrderProcessor
, we’re processing the order
s.
Output of this simple application looks like this:
Are we done yet? Nope. Remember the rule.
Rule: The application code must be testable. (Testable = The code must facilitate Unit Test implementation.)
We have created an application which works fine as per the requirement. But in order to confirm our code quality and ensure that it won’t break on different scenarios, we need to write UnitTest
.
Tools are out there to apply test project in our solutions, but there is a catch. To apply UnitTest
, the targeted code must be testable.
Is our code testable? Nope.
Why is it not a testable code? Because OrderProcessor
class contains tightly coupled code.
What is a tightly coupled code? In simple terms, Code that contains direct instantiation of its dependency classes directly are called tightly coupled.
We say OrderProcessor
contains tightly coupled code because of the following block of code it holds.
ValidateOrder validateOrder = new ValidateOrder();
PrintOrder printOrder = new PrintOrder();
OrderProcessor
’s ProcessOrder
method uses validateOrder
and printOrder
object.
Dependency Injection is a concept that helps us to convert the tightly coupled code to loosely coupled code and doing so it makes our solution, untestable to testable.
So far, we have successfully implemented a sample project to work on.
In Part II, the exact problem that DI helps us to resolve will be explained in detail. Be on the lookout for that.
At the top of this post, I've attached the full source code file for this article to play with. The same code will be updated in the next article, so just be aware that it is not the final version of the code for this article.
Thanks for reading. Never stop learning!
History
- 27th February, 2021: Initial post