SOLID Principles – Part 04 – Liskov Substitution Principle

Principle

This principle is an extension of the Open Close Principle and was first introduced by Barbara Liskov during a conference keynote speech. We need to ensure that new derived classes are extending the base classes without changing their behaviour. You should be able to replace a class with its base type without altering the correctness of the program.

Scenario

To demonstrate LSP 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 however; the CalculateTotal method in the Order class has some logging added to it.

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 Logger();

   log.Log("Total Cost: " + total.ToString());

   return total;
}

The following Logger class is created which sends the output to the console and also simulates sending to an Oracle database.

public class Logger
{
   public void Log(string Message)
   {
      string output = string.Format("Log Message: {0}", Message);
      Console.WriteLine(output);
      WriteToDatabase(output);
   }

   public void WriteToDatabase(string Message)
   {
      // Simulate DB Write
      Console.WriteLine(string.Format("Database Message: {0}", Message));
   }
}

A new requirement comes in to send the logging to a SQL Server database instead. So the developer decides to create a class that inherits from the Logger class and create their own implementation of the WriteToDatabase method.

public class SQLServerLogger : Logger
{
   public void WriteToDatabase(string Message)
   {
      Console.WriteLine(string.Format("SQL Server Database Message: {0}", Message));
   }
}

The problem with this approach is that the logger class has not declared the WriteToDatabase as virtual and therefore the original method has been hidden. We have in affect changed the behaviour of our Logger class which breaks LSP. Visual Studio has warned us about this as you can see below.

After Code

The correct way of implementing the Logger class is as follows.

public abstract class Logger
{
   public void Log(string Message)
   {
      string output = string.Format("Log Message: {0}", Message);
      Console.WriteLine(output);
      WriteToDatabase(output);
   }

   public abstract void WriteToDatabase(string Message);
}

public class SQLServerLogger : Logger
{
   public override void WriteToDatabase(string Message)
   {
      Console.WriteLine(string.Format("SQL Server Database Message: {0}", Message));
   }
}

public class OracleLogger : Logger
{
   public override void WriteToDatabase(string Message)
   {
      Console.WriteLine(string.Format("Oracle Database Message: {0}", Message));
   }
}

We create an abstract class Logger (could be an Interface if we have no implementation in the base class) and create two derived classes for our databases. From the CalculateTotal method we can instantiate a Logger and pass in any one of our derived classes.

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;
}

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.