Principle
Classes that depend on other classes should depend on abstractions rather than concrete implementations. This makes the classes much more flexible to changing implementations.
Scenario
To demonstrate ISP I will continue using the E-Commerce example from the previous blog. A customer has an order which includes multiple order items. Given an order we calculate the total cost and the tax due based on US state.
Before Code
We will use the code from the previous blog unchanged however; I want to point out two issues. The Order class has a dependency on the InternetCustomer and SQLServerLogger concrete classes. This makes the Order class very brittle if either of those two classes change. It also makes the class inflexible when it comes to handling different types of customers and different types of loggers.
public class Order { public List _orderItems = new List(); private InternetCustomer _internetCustomer; public Order() { _internetCustomer = new InternetCustomer(); } public Customer GetCustomerById(int Id) { return _internetCustomer.GetCustomerById(Id); } public decimal CalculateTotal(Customer customer) { decimal total = _orderItems.Sum((item) => { return item.Cost * item.Quantity; }); ITax tax = new TaxFactory().GetTaxObject(customer.StateCode); total += tax.CalculateTax(total); Logger log = new SQLServerLogger(); log.Log("Total Cost: " + total.ToString()); return total; } }
After Code
To fix the DIP issue we need to add our Order class dependencies using Dependency Injection. There are many ways to achieve this. In the example below I will use Constructor Injection.
public class Order
{
public List _orderItems = new List();
private readonly IInternetCustomerRead _internetCustomer;
private readonly Logger _logger;
public Order(IInternetCustomerRead InternetCustomer, Logger Log)
{
this._internetCustomer = InternetCustomer;
this._logger = Log;
}
public Customer GetCustomerById(int Id)
{
return _internetCustomer.GetCustomerById(Id);
}
public decimal CalculateTotal(Customer customer)
{
decimal total = _orderItems.Sum((item) =>
{
return item.Cost * item.Quantity;
});
ITax tax = new TaxFactory().GetTaxObject(customer.StateCode);
total += tax.CalculateTax(total);
_logger.Log("Total Cost: " + total.ToString());
return total;
}
}
Our Order class no longer has a dependency on the concrete classes only the IInternetCustomerRead interface and Logger abstract class. This allows any classes that derive from those to be instantiated outside of the Order class and passed in through the constructor. The following test shows how this is done.
[Test] public void TestCustomerOrderWithSQLServerLogger() { IInternetCustomerRead _internetCustomer = new InternetCustomer(); Logger _logger = new SQLServerLogger(); Order o = new Order(_internetCustomer, _logger); Customer cust = o.GetCustomerById(5); o._orderItems = oi; decimal cost = o.CalculateTotal(cust); Assert.AreEqual(8.63m, cost); }
We can run the before and after tests and it will still work fine.
A word about Dependency Injection
Injecting dependencies into a class is something that is done all the time and keeping track of what dependencies are required can be hard to track in a large application. As a result of this, software known as “Inversion of Control (IOC) Containers” were created to enable dependencies to be centrally managed. These require a whole blog series on their own and therefore I will just list the popular ones.
- Microsoft Unity
- Castle Windsor
- Ninject
- Lamar. The successor to StructureMap