SOLID Principles – Part 02 – Single Responsibility Principle

Principle

A class should only have a single responsibility. E.g. If it’s a logging class it should only do logging. If it’s a payments class it should only do payments, and so forth. A class should only have one reason to change.

Scenario

To demonstrate SRP I will use an E-Commerce example. 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

Look at the code below. It has three classes, Customer, Order and OrderItem. The Order class has a method CalculateTotal to calculate the total order.

public class Customer
{
   public string StateCode;
   public string ZipCode;
   public string County;
}

public class Order
{
   public List _orderItems = new List();

   public decimal CalculateTotal(Customer customer)
   {
      decimal total = _orderItems.Sum((item) =>
      {
         return item.Cost * item.Quantity;
      });

      decimal tax;
      if (customer.StateCode == "TX")
         tax = total * .08m;

      else if (customer.StateCode == "FL")
         tax = total * .09m;

      else
         tax = .03m;

      total = total + tax;
      return total;
   }
}

public class OrderItem
{
   public int Quantity;
   public string Code;
   public decimal Cost;
}

I have also created the following tests.

List oi;

[SetUp]
public void Setup()
{
   oi = new List()
   {
      new OrderItem {Quantity = 5, Cost = 1.00m, Code = "Apples"},
      new OrderItem {Quantity = 4, Cost = 0.90m, Code = "Oranges"},
   };
}

[Test]
public void TestTexasCustomerOrder()
{
   Customer c1 = new Customer { StateCode = "TX", County = "whocares", ZipCode = "zippy" };

   Order o = new Order();
   o._orderItems = oi;

   decimal cost = o.CalculateTotal(c1);

   Assert.AreEqual(9.288m, cost);   
}

[Test]
public void TestFloridaCustomerOrder()
{
   Customer c1 = new Customer { StateCode = "FL", County = "whocares", ZipCode = "zippy" };

   Order o = new Order();
   o._orderItems = oi;

   decimal cost = o.CalculateTotal(c1);

   Assert.AreEqual(9.374m, cost);
}

[Test]
public void TestOtherCustomerOrder()
{
   Customer c1 = new Customer { StateCode = "WA", County = "whocares", ZipCode = "zippy" };

   Order o = new Order();
   o._orderItems = oi;

   decimal cost = o.CalculateTotal(c1);

   Assert.AreEqual(8.63m, cost);
}

So how does this break SRP?

The Order class should only be doing things tightly related to orders. It should be totalling order items however; it should NOT be managing the tax routines on a state by state basis. This is multiple responsibilities.

After Code

Look at the refactored code below. We have moved the tax calculation to its own class Tax. The Order class is now only responsible for totalling order items cost. Changes to tax does not affect the code in this class. We can also test the Tax class independently. By moving the tax into its own class we have in fact implemented the Strategy Design Pattern.

public class Customer
{
   public string StateCode;
   public string ZipCode;
   public string County;
}

public class Order
{
   public List _orderItems = new List();

   public decimal CalculateTotal(Customer customer)
   {
      decimal total = _orderItems.Sum((item) =>
      {
         return item.Cost * item.Quantity;
      });

      Tax t = new Tax();
      total = total + t.CalculateTax(customer.StateCode, total);
      return total;
   }
}

public class OrderItem
{
   public int Quantity;
   public string Code;
   public decimal Cost;
}

public class Tax
{
   public decimal CalculateTax(string StateCode, decimal Total)
   {
      decimal tax;

      if (StateCode == "TX")
         tax = Total * .08m;

      else if (StateCode == "FL")
         tax = Total * .09m;

      else
         tax = .03m;

      return tax;
   }
}

We can run the before and after tests and it will still work fine.

Sidenote

The code so far actually does not conform to the full SOLID principles however; we will discuss this in the up and coming blogs.