SOLID Principles – Liskov Substitution Principle

objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

I noticed that this principle is a mystery even for experienced software developers. The reason for this is that you run into it very rarely, but once you do you’ll understand all the issues that arise from it.

Let’s try to implement the GeoPaymentModifier based on what we decided in the previous posts in the SOLID series. To recap, we receive the requirement to add a shipping cost to the total amount, if the shipping location is further away than 20 km. Since our client has multiple warehouses we need the closest warehouse location. Our class will look something like this:

class GeoPaymentModifier : IPaymentCalcModifier
{
	public WarehouseInfo ClosestWarehouse { get; set; }

	public double Calculate(Order order, double currentPrice)
	{
		return GetDistance(order) > 20
			? currentPrice + 100
			: currentPrice;
	}

	private int GetDistance(Order order)
	{
		int warehouseLoc = ClosestWarehouse.Location;
		int shippingLoc = order.ShippingInfo.Location;
		return Math.Abs(warehouseLoc - shippingLoc);
	}
}

In doing so unfortunately you force a change in the PaymentCalculator also. Since you probably noticed that this code will result in an error, if there is no warehouse information provided, the PaymenyCalculator needs to change to something like this:

class PaymentCalculator
{
	public double Calculate(Order order, 
        IEnumerable<IPaymentCalcModifier> modifiers)
	{
		double total = 0;
		foreach (var modifier in modifiers)
		{
			if (modifier is GeoPaymentModifier geo)
			   geo.ClosestWarehouse = GetClosestWarehouse(order);
			total = modifier.Calculate(order, total);
		}

		return total;
	}
}

Since we needed to modify the PaymentCalculator we very likely violated the Open/Closed Principle. By introducing the knowledge that there is a GeoPaymentModifier, which needs to be treated differently, I would argue you are in violation of the Single Responsibility Principle too.

We need to search for a different solution which doesn’t change the PaymentCalculator. I propose to handle the selection of the warehouse for now in the GeoPaymentModifier, I know this is not a perfect solution but we will discuss this in a later post.

class GeoPaymentModifier : IPaymentCalcModifier
{
	private IEnumerable<WarehouseInfo> _warehouses;

	public GeoPaymentModifier(IEnumerable<WarehouseInfo> warehouses)
	{
		_warehouses = warehouses;
	}

	public double Calculate(Order order, double currentPrice)
	{
		int shippingLoc = order.ShippingInfo.Location;
		int minDistance = _warehouses
                    .Min(x => Math.Abs(x.Location - shippingLoc));
		return minDistance > 20
			? currentPrice + 100
			: currentPrice;
	}
}

In most of the real life cases, you need to implement abstractions from other libraries. Chances are that our IPaymentCalcModifier and PaymentCalculator would be defined in a assembly dedicated to payment, and we would expose a mechanism of registering modifiers. Modifying them would result in a circular reference, or would be impossible from the begging since they could also come from a 3rd party library.

Keep in mind that this principle is not only about abstractions. Through regular inheritance you can overwrite parts from the base class. Inside the base class there is functionality that relies on the fact that the overwritten function behaves in the same manner and has the same effects on the class as the original. You need to obey the same contracts as the base class.

For example, if you have a class that has a virtual function the by convention returns a positive value if all went well, and a negative value with an error code if there is an error. Your inheriting class should return the same error codes for the same issue, and negative values cannot represent positive outcomes in this case.

Hope this posts sheds some light on the mystery and see you soon!