Dependency Injection - Part Four -Embracing Abstraction

Introduction
 
Now that we have the answer to our question "why loosely coupled code?", let's try and solve the problems we found in a tightly coupled application. In this post, we will use constructor based dependency injection, however we are not going to use any DI container for now.
Dependency Injection
 
Dependency Injection (DI) is a technique that helps us achieve loose coupling between objects and their collaborators. In general, we refer to these collaborators as dependencies for an object. In order to achieve loose coupling, we must follow the golden rule of "not newing" our dependencies in the class, or refrain from having too many static references in our code.
 
With this in mind, classes will declare their dependencies via their constructor following the Explicit Dependency Pattern. This approach is known as "constructor injection".
 
Example
 
To demonstrate the concept, we will be using a very simple application with the following component.
  • Order - represents an order placed by the user
  • Commerce - accepts an Order and uses other components to process it
  • PaymentProcessor - processes the user payment
  • CurrencyConverter - converts the paid amount into local currency
  • NotificationManager - notifies the user about the order confirmation
  • Logger - logs errors (like transaction failure)
  1. public class Program  
  2. {  
  3.     Order customerOrder = new Order { Id = 1, ProductName = "Product", Quantity = 3, UnitPrice = 75 };  
  4.       
  5.     Commerce commerce = new Commerce(new CreditCardProcessor(new CurrencyConverter()),  
  6.                                      new EmailNotifier(), new TextLogger());  
  7.     commerce.ProcessOrder(customerOrder);  
  8. }  
  9.   
  10. public class Commerce  
  11. {  
  12.     private PaymentProcessor _paymentProcessor;  
  13.     private NotificationManager _notificationManager;  
  14.     private Logger _logger;  
  15.   
  16.     public Commerce(PaymentProcessor paymentProcessor, NotificationManager notificationManager, Logger logger)  
  17.     {  
  18.         _paymentProcessor = paymentProcessor;  
  19.         _notificationManager = notificationManager;  
  20.         _logger = logger;  
  21.     }  
  22.   
  23.     public void ProcessOrder(Order order)  
  24.     {  
  25.         decimal paidAmount = order.UnitPrice * order.Quantity;  
  26.         bool paymentSuccessful = _paymentProcessor.ProcessPayment(paidAmount);  
  27.               
  28.         if(paymentSuccessful)  
  29.         {  
  30.             _notificationManager.NotifyCustomer(notification: "payment successful");  
  31.         }  
  32.         else  
  33.         {  
  34.             _notificationManager.NotifyCustomer(notification: "payment failed");  
  35.             _logger.Log(errorMessage: "payment failed");  
  36.         }  
  37.     }  
  38. }  
In our application, the Commerce class has an Order from the customer. In order to process the Order, it requires three collaborators, that is to say, it has a dependency on three other classes.
 
As can be seen in the above code, the Commerce class exposes all its dependencies via its constructor. Consequently, at the time of instantiating its object, the User class will be aware of all the requirements of Commerce class. Moreover, this is known as Constructor Injection, as the User class will provide all the dependencies via a constructor.
 
The Problem
 
In this instance, if we look at our application, everything looks just fine. Why? Because we have our components well separated from each other. Also, all our classes follow the Single Responsibility Principle. None of the collaborators is tightly coupled with the Commerce class, rather all of them are injected via its constructor.
 
However, if we notice we are still instantiating the dependencies for Commerce class in the Program class. While it doesn't seem to be a problem in such a small application, we will surely end up in trouble as the level of dependencies increases.
 
Summary
 
In this article, we have seen how we can achieve loose coupling using interfaces in place of concrete classes. However, it gets difficult to maintain such an application as the level of dependencies increases. Instead of instantiating the dependencies on our own, we require someone else to do this for us and provide them whenever and wherever required. Thankfully, a DI Container does that for us. It is a DI container that instantiates and provides the dependencies.


Similar Articles