SOLID : Liskov Substitution Principle (Part 2)

In the first part of the post, we visited the core definition of Liskov Substitution principle and took time to understand the problem using an example. In this second part, we would take time to understand some of the governing rules of the principle.

The rules that needs to be followed for LSP compliance can be broadly categorized into mainly two categories.

  • Contract Rules
  • Variance Rules

Let us examine the Contract Rules first.

Contact Rules
The three contact rules are defined as follows.
* Preconditions cannot be strengthened in a subtype.
* Postconditions cannot be weakened in a subtype.
* Invariants—conditions that must remain true—of the supertype must be preserved in a subtype

Let us examine each of them breifly.

Preconditions cannot be strengthend in a subtype.

Preconditions are conditions that are necessary for a method to run reliably. For example, consider a method in a school application, which returns the count of students above specified age.

public virtual int GetStudentCount(int minAge)
{
if(minAge <=0)
throw new ArgumentOutOfRangeException("Invalid Age");
return default; // For demo purpose
}

As observed in the above method, a non-positive value for age needs to be flagged as an exception and method should stop executing. The method does this check as the first step before it executes any logic related to the funcationlity of the method and thereby preventing the method execution unless the parameters are valid. This is a typical example of preconditions. There are other ways to add preconditions, but that would be out of scope of the current context.

So what happens if the precondition is strengthed. Let us assume that the application for Kindergarden for the school derieves from the same class, but adds another precondition.

public override int GetStudentCount(int minAge)
{
if(minAge <=0) throw new ArgumentOutOfRangeException("Invalid Age"); if(minAge >=5)
throw new ArgumentOutOfRangeException("Invalid Age");
return default; // For demo purpose
}

This creates a conflict in the calling class. The caller was invoking the method from the base Class, it would not throw an exception if the value is greater then 5. However, if the caller was invoking the method from the Child Class, it would now throw an exception, thanks to the new precondition we have added. Let’s demonstrate the same using Unit Tests.

[Test]
[TestCaseSource(typeof(Preconditions), nameof(TestCases))]
public void GetStudentCount(School school,int age)
{
Assert.GreaterOrEqual(school.GetStudentCount(age), 0);
}

public static IEnumerable TestCases
{
get
{
yield return new TestCaseData(new School(), 7);
yield return new TestCaseData(new Kindergarden(), 7);
}
}
}

The Unit Tests show that the strengthening of Preconditions would now indirectly force the caller to violate the fact that it should not make any assumptions about the type it is acting on (and thereby violating Open Closed Principle).

Post Conditions cannot be weakend in a subtype

During the execution of the method, the state of the object is most likely to be mutated, opening the door of possiblity that the method might leave the object in an invalid state due to errors in the method. Post Conditions ensures that the state of the object is in a valid state when the method exits.

Much similiar to the Preconditions, the Post Conditions are commonly implemented using Guard clauses, which ensure the state is valid after the execution of logic (any code that might alter the state of the object) associated with the method. For the same reason, the Post Conditions are placed at the end of the method.

The behavior of Post Conditions in sub-classes is the complete opposite of what happens with Pre Conditions. The Post Conditions, if weakened, could l;ead to existing clients to behave incorrectly. Let us explore a scenario. We will write the Unit Tests first.

[Test]
[TestCaseSource(typeof(Postconditions), nameof(TestCases))]
public void TestLabFeeForSchool(School school, long studentId)
{
Assert.Greater(school.CalculateLabFees(studentId),0);
}

public static IEnumerable TestCases
{
get
{
yield return new TestCaseData(new School(), 7);
}
}

The above Test Case ensures that the CalculateLabFees method returns a positive and non-zero value as result. Let us assume that School.CalculateLabFees method was defined as following.

public class School
{
public virtual decimal CalculateLabFees(long studentId)
{
var labFee = MockMethodForCalculatingLabFees(studentId);

if (labFee <= 0)
return 1000;
return labFee;
}

private decimal MockMethodForCalculatingLabFees(long studentID)
{
return default;
}
}

The Post Condition in the base class ensures that if the labFee is less than or equal to zero, a default value of 1000 is returned. The Test Case would pass successfully with the instance of class. The CalculateLabFees method would return a positive, non-zero value as expected by the Unit Test.

Now, let us assume the Kindergarden Implementation. The Kindergarden doesn’t have Lab Fees and hence, would return a default value 0 for the CalculateLabFee method.

public class Kindergarden : School
{
public override decimal CalculateLabFees(long studentId)
{
return 0;
}
}

As you can notice the Post Condition has been weakened in the Child Class. This would cause issues with client which would expect a positive response, as with the Unit Test Case. This highlights another instance where significance of honoring LSP is highlighted.

Invariants must be maintained

An invariant describes a condition of a process that remains true before the process begins and after the process ends, until ofcourse the object is out of scope.

Consider the following base class.

public class Student
{
public string Name{ get; set; }

public void CheckInvariants()
{
if (string.IsNullOrEmpty(Name))
Name = "Name Not Assigned";
}
public virtual void AssignName(string name)
{
CheckInvariants();
Name = name;
CheckInvariants();
}
}

The class requires the Name property to be always have a value. The CheckInvariants method assigns a default value if the clause is violated. Let us assume we have an sub class NurseryStudent.

public class NurseryStudent : Student
{
public override void AssignName(string name)
{
Name = name;
}

}

The author of NurseryStudent forgots to ensure the Invariants holds true after the AssignName exits. This is a violation of LSP as a client using the SuperClass would expect the Name to always have a value. Consider the following code.

[Test]
[TestCaseSource(typeof(Invariants), nameof(TestCases))]
public void TestInvariants(Student student,string name)
{
student.AssignName(name);
Assert.Greater(student.Name.Length, 0);
}

public static IEnumerable TestCases
{
get
{
yield return new TestCaseData(new Student(), string.Empty);
yield return new TestCaseData(new NurseryStudent(), string.Empty);
}
}

The Unit Test Case would fail

Let’s write the Unit Test Case before writing the example for sub class.

[Test]
[TestCaseSource(typeof(Invariants), nameof(TestCases))]
public void TestInvariants(Student student,string name)
{
student.AssignName(name);
Assert.Greater(student.Name.Length, 0);
}

public static IEnumerable TestCases
{
get
{
yield return new TestCaseData(new Student(), null);
yield return new TestCaseData(new NurseryStudent(), null);
}
}

The Test Case would fail when an instance of NurserStudent is passed to the method as the Name property would have an unexpected value of null when leaving the AssignName method. This would cause exceptions in all clients which expects the behavior of invariants are retained by any subclass of the Student.

In this post we explored the Contract Rules for LSP. In the part, we would explore the variance rules involved for conforming with LSP. All code samples given in the post are available in my Github

SOLID : Liskov Substitution Principle (Part 1)

The “L” in SOLID stands for Liskov Substitution Principle, named after Barbara Liskov, who initially introduced in 1987 keynote address. LSP provides guidelines for creating inheritence heirachy where a client can use any class or subclass without breaking the expected behavior.

We will begin by looking at the official definition first.

If S is a subtype of T, then objects of type T may be replaced with objects of type S,without breaking the program.

It would be unwise to explain this theoratically, hence like always, let hit the Visual Studio and write some code. I would like borrow an example given by Tim Correy in his famous video.

Assume we have an Employee class defined as follows.

public class Employee
{
public string Name{get;set;}
public Employee AssignedManager{get;protected set;}
public virtual double GetPay()
{
var currentSalary = 500;
Console.WriteLine($"Current Pay {currentSalary}");
return currentSalary;
}

public virtual void AssignManager(Employee manager)
{
Console.WriteLine("Manager Assigned");
AssignedManager = manager;
}
}

Now let’s assume we have a Manager class, which inherits from Employee

public class Manager:Employee
{
public override double GetPay()
{
var currentSalary = 750;
Console.WriteLine($"Current Pay {currentSalary}");
return currentSalary;
}

public override void AssignManager(Employee manager)
{
Console.WriteLine("Manager Assigned");
AssignedManager = manager;
}
}

Let’s write some client code now which consumes these classes.

var manager = new Manager();
manager.Name = "John Doe";
manager.GetPay();

var programmer = new Employee();
programmer.Name = "Roel Doel";
programmer.GetPay();
programmer.AssignManager(manager);

Output

Current Pay 750
Current Pay 500
Manager Assigned

As you can observe, the output is perfectly fine. Let us try making some changes to the Client Code now. Let us replace the Employee class (programmer instance) with Manager.

var manager = new Manager();
manager.Name = "John Doe";
manager.GetPay();

var programmer = new Manager();
programmer.Name = "Roel Doel";
programmer.GetPay();
programmer.AssignManager(manager);

Output

Current Pay 750
Current Pay 750
Manager Assigned

This has turned out fine as well. Yes the output is different, which is expected considering the Pay for Manager and Employee are different, but the client hasn’t behaved unexpectedly. Let’s write one more class, inheriting from Employee and call it CEO.

public class CEO:Employee
{
public override double GetPay()
{
var currentSalary = 750;
Console.WriteLine($"Current Pay {currentSalary}");
return currentSalary;
}

public override void AssignManager(Employee manager)
{
throw new Exception("Am the CEO, no Manager for me !!");
}
}

The difference this time is of course the CEO doesn’t have a Manager and it raises an exception in the AssignManager method. Let’s head back to out Client code and replace Employee with CEO in our original code.

var manager = new Manager();
manager.Name = "John Doe";
manager.GetPay();

var programmer = new CEO();
programmer.Name = "Roel Doel";
programmer.GetPay();
programmer.AssignManager(manager);

Output

Exception:Am the CEO, no Manager for me !!

Aha, we have an exception here. When replaced the Employee (Base type) with a subtype (CEO), the expected behavior of program has been broken. This is a violation of LSP.

Let’s examine the violation further. It is often said that Inheritence is the IS-A relation. However, if you truely examine CEO and Employee, do they follow a IS-A relation in every sense ? Not really, not according the current definition of Employee. Yes, that is right. The problem doesn’t quite lie in CEO, but it originate from the definition of Employee class. While designing the Employee class, we “assumed” that all Employees would have a manager.

So what is the best solution for this scenario ? What if we were to ensure the Employee class had only those functionalities which are common to every employee ? Obviously, the Manager is not applicable for everyone, so let’s remove it and create an interface which contains only the basic employee functionalities.

public interface IBaseEmployee
{
string Name{get;set;}
double GetPay();
}

public abstract class BaseEmployee:IBaseEmployee
{
public string Name{get;set;}
public virtual double GetPay(){ return 0;}
}

Remember, we still have to consider the Manager assignment. What we would do here is define another interface IHasManager, which would wrap the functionalities required for Employees who has Manager.

public interface IHasManager
{
IBaseEmployee AssignedManager{get;set;}
void AssignManager(IBaseEmployee manager);
}

Let’s now define our Employee and Manager classes.

public class Employee:BaseEmployee,IHasManager
{
public IBaseEmployee AssignedManager{get; set;}
public override double GetPay()
{
var currentSalary = 500;
Console.WriteLine($"Current Pay {currentSalary}");
return currentSalary;
}

public virtual void AssignManager(IBaseEmployee manager)
{
Console.WriteLine("Manager Assigned");
AssignedManager = manager;
}

}

public class Manager:BaseEmployee,IHasManager
{
public IBaseEmployee AssignedManager{get; set;}
public override double GetPay()
{
var currentSalary = 750;
Console.WriteLine($"Current Pay {currentSalary}");
return currentSalary;
}

public void AssignManager(IBaseEmployee manager)
{
Console.WriteLine("Manager Assigned");
AssignedManager = manager;
}
}

As you can see, the Manager and Employee inherits from BaseEmployee and implements the IHasManager interface. The next step is to define our CEO Class, and this time, we know that he doesn’t need a Manager.

public class CEO:BaseEmployee
{

public override double GetPay()
{
var currentSalary = 750;
Console.WriteLine($"Current Pay {currentSalary}");
return currentSalary;
}

}

That’s it. Let’s rewrite our Client code now.

var manager = new Manager();
manager.Name = "John Doe";
manager.GetPay();

var programmer = new Employee();
programmer.Name = "Roel Doel";
programmer.GetPay();
programmer.AssignManager(manager);

The above code compiles and executes as expected. Let’s replace the Employee with a Manager first.

var manager = new Manager();
manager.Name = "John Doe";
manager.GetPay();

var programmer = new Manager();
programmer.Name = "Roel Doel";
programmer.GetPay();
programmer.AssignManager(manager);

That works fine as well. Let’s try again, and this time replace with CEO.

var manager = new Manager();
manager.Name = "John Doe";
manager.GetPay();

var programmer = new CEO();
programmer.Name = "Roel Doel";
programmer.GetPay();
programmer.AssignManager(manager);

Not surprisingly, that doesn’t compile at all. CEO doesn’t have the AssignManager method as it doesn’t implement the IHasManager interface. Your client no longer have unexpected behavior as such errors are caught during compilation.

That was pretty easy, isn’t it ? But we are not done yet. There is more to the definition of LSP. In the next part of this series, we will examine the Contact and Variance rules which must be followed for LSP compliance.

Patterns,Principles and Programming.

Have been thinking about putting together a collection of Jump Start Tutorials on some core concepts on Programming, finally getting everything together

Design Patterns

Design Princples

Code Smells and Refactoring Techniques

 

SOLID : Open / Closed Principle

The Open / Closed Principle focuses on one basic requirement, the classes should be Open for extensions, but closed for modification.

The better way to explain the principle would be to first express the issue in hand. Consider the below class.

image

The class itself looks good, but what if in future one needs to add another Shape, say Circle. This would raise need to modify our existing class which is violation of OCP.

The solution to problem lies in achieving extension by deriving from abstraction. Let’s rewrite the code.

image

As you can see in the above implementation, we have created an abstract class with the function we might require extension. The corresponding classes, Square and Circle, implement this abstract class and extend its functionality. So when the requirements changes, and you need to extend, you needn’t change the existing classes. This means the classes which are properly tested needn’t be touched again and thereby reduce chances of introducing bugs in existing classes.

The Chain Of Responsibility Pattern is yet another way of achieving Open Closed Principle

SOLID : Single Responsibility Principle

While OOPs is rich with features like inheritance, polymorphism, encapsulation and overloading enabling developers to extract more from modern day programming languages like C#, it is equally important to understand when to use these features based on design principles.

SOLID, is a set of 5 principles which when properly applied intend to guide a programmer to make systems that is scalable and maintainable with ease.

The first of SOLID Principles, (S) – Single Responsibility Principle (SRP) demands that a class should have only a single responsibility and that responsibility should be fully encapsulated by that a class.

Consider the following class. The class is intended to Book Movie Tickets.

image

If you observe the class, the method BookTicket is divided into two parts. First, it books the tickets and then it notify the User about the same. That is, the class has two responsibilities – Book Tickets and Notify User, in other words, there are two reasons for which the class can be changed.

Suppose few years later, the company decides to change the notification mechanism and include SMS support as well. This would mean our MovieTicket Class needs to be altered for a reason (user notification), which has nothing to do with the core functionality of the class (booking ticket).

This is violation of SRP and the obvious solution would be to separate the responsibilities to separate classes.

image

This ensures that each class has only one responsibility and there would be only reason to change it.