Knowledge Base

SOLID Principles in Delphi [3] – The Liskov Substitution principle

The Liskov Substitution Principle in Delphi! I start with the official definition:

Subtype Requirement: Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

Still here and wondering what that means? Good, I had some trouble understanding this too. 😊

Let us take a more practical approach to LSP: The principle defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. Or, to stay in Delphi terms: If TChild is of a subtype of TParent, then objects of type TParent may be replaced with objects of type TChild, without breaking the logic of the program.

This means that the underlying classes must work in approximately the same way as the parent class. In contrast to the previous SOLID principles we have dealt with, this one is more about the behaviour of classes, and not directly about the structure of these classes. Let’s get started again with an example.

TEmployee = class
  strict private
    FName: string;
    FManager: TEmployee;
    FSalary: Double;

    procedure SetName(const Value: string);
    procedure SetSalary(const Value: Double);
    function GetName: string;
    function GetSalary: Double;
  public
    procedure AssignManager(const AEmployee: TEmployee); virtual;

    property Name: string read GetName write SetName;
    property Salary: Double read GetSalary write SetSalary;
  end;

As you can see, this is a very simple Employee class, which we for example could use like this:

var
  Employee: TEmployee;
  Manager: TEmployee;
begin
  Manager := TEmployee.Create;
  Manager.Name := 'Jane Smith';
  Manager.Salary := 31000;

  Employee := TEmployee.Create;
  Employee.Name := 'John Smith';
  Employee.Salary := 22500;
  Employee.AssignManager(Manager);
end;

So far so good. But let’s say we want to add some functionality to the Manager, as he has to do an appraisal for an employee. Maybe you remember that I’ve mentioned the preference to program against interfaces, not implementation. But for the sake of this example, we’re going to override our TEmployee class to see what the Liskov Substitution principle is, before we refactor this again to interfaces. So, let’s create the following class:

  TManager = class(TEmployee)
  public
    procedure DoAppraisal(const AEmployee: TEmployee);
  end;

This is just fine, as we added some functionality. We can now simply change our implementation to this:

var
  Employee: TEmployee;
  Manager: TEmployee;
begin
  Manager := TManager.Create;
  Manager.Name := 'Jane Smith';
  Manager.Salary := 31000;

  Employee := TEmployee.Create;
  Employee.Name := 'John Smith';
  Employee.AssignManager(Manager);
  Manager.Salary := 22500;
end;

We changed the implementation of the Manager object to a TManager, and our program still functions as before. Let’s create another class, and now for our CEO:

  TCEO = class(TManager)
  public
    procedure AssignManager(const AEmployee: TEmployee); override;
    procedure ReviewCompany;
  end;

and the actual implementation:

{ TCEO }
procedure TCEO.AssignManager(const AEmployee: TEmployee);
begin
  raise Exception.Create('The CEO can''t have a manager!');
end;

procedure TCEO.ReviewCompany;
begin
  // Do the company review
end;

Our TCEO overrides from the TManager, implements a new procedure ReviewCompany, and overrides the AssignManager, as this won’t make sense to have a manager for a CEO. But now we have a problem. Let’s say we change our implementation to an instance of TCEO:

var
  Employee: TEmployee;
  Manager: TEmployee;
begin
  Manager := TManager.Create;
  Manager.Name := 'Jane Smith';
  Manager.Salary := 31000;

  Employee := TCEO.Create;
  Employee.Name := 'John Smith';
  Employee.AssignManager(Manager);
  Employee.Salary := 22500;
end;

Although our project still compiles, we now have a problem when we run this program, because we suddenly get an exception on the AssignManager call. As this breaks our project, this is clearly a violation of the LSP. We should be able to change our TParent to TChild without having a side effect on the functionality.

As you see, the implementation of the TCEO class is stricter than its parent class. The Liskov Substitution Principle states that you can implement less restrictive validation rules, but you are not allowed to enforce stricter ones in your child classes. Similar rules apply to the return value of a function. The return value of a function of the child class needs to comply with the same rules as the return value of the function of the parent.

So, how can we solve this?

The first thing is to ask ourselves; is a CEO really an Employee? Sort of; a CEO can have a salary, but to assign a manager won’t make any sense. Let’s try to solve this with the use of interfaces (again, program against interfaces, not implementation). We start with some interfaces:

type
  IBaseEmployee = interface
    procedure SetName(const Value: string);
    procedure SetSalary(const Value: Double);
    function GetName: string;
    function GetSalary: Double;

    property Name: string read GetName write SetName;
    property Salary: Double read GetSalary write SetSalary;
  end;

  IManagedEmployee = interface(IBaseEmployee)
    procedure AssignManager(const AEmployee: IBaseEmployee);
  end;

  IManager = interface(IBaseEmployee)
    procedure DoAppraisal(const AEmployee: IBaseEmployee);
  end;

  ICEO = interface(IManager)
    procedure ReviewCompany;
  end;

As you can see, we have an interface for the basic properties of an employee, we have created a specific interface for the manager and the managed employee, and one for the CEO. The implementations of our classes are now as follows:

 TBaseEmployee = class(TInterfacedObject, IBaseEmployee)
  strict private
    FName: string;
    FManager: IBaseEmployee;
    FSalary: Double;

    procedure SetName(const Value: string);
    procedure SetSalary(const Value: Double);

    function GetName: string;
    function GetSalary: Double;
  public
    property Name: string read GetName write SetName;
    property Salary: Double read GetSalary write SetSalary;
  end;

  TEmployee = class(TBaseEmployee, IManagedEmployee)
  strict private
    FManager: TEmployee;
  public
    procedure AssignManager(const AEmployee: IBaseEmployee);
  end;

  TManager = class(TEmployee, IManager)
  public
    procedure DoAppraisal(const AEmployee: IBaseEmployee);
  end;

  TCEO = class(TBaseEmployee, ICEO, IManager)
  public
    procedure ReviewCompany;
    procedure DoAppraisal(const AEmployee: IBaseEmployee);
  end;

And finally the changed calls to these classes:

var
  Employee: IBaseEmployee;
  Manager: IBaseEmployee;
begin
  Manager := TManager.Create;
  Manager.Name := 'Jane Smith';
  Manager.Salary := 31000;

  Employee := TCEO.Create;
  Employee.Name := 'John Smith';
  Employee.AssignManager(Manager);
  Employee.Salary := 22500;
end;

If you take a good look at the implementation, you can spot an error; the last Employee.AssignManager will give a compile error, because the TCEO class doesn’t have the AssignManager function anymore. So, our logic must change, reflecting the actual situation.

And although we do use inheritance with our classes, we still program against interfaces, as you can see in another code example below:

var
  CEO: ICEO;
  Manager: IManagedEmployee;
begin
  CEO := TCEO.Create;
  CEO.Name := 'John Smith';
  CEO.Salary := 75000;

  CEO.ReviewCompany;

  Manager := TManager.Create;
  Manager.Name := 'Jane Smith';
  Manager.Salary := 31000;
  Manager.AssignManager(CEO);
end;

The implementation of our classes is now also compliant to the Liskov Substitution principle, as we can now swap the TManager.Create with a TEmployee.Create  without affecting runtime behaviour.

So, to summarise the Liskov Substitution Principle again: If TChild is of a subtype of TParent, then objects of type TParent may be replaced with objects of type TChild, without breaking the logic of the program. Don’t enforce stricter behaviour in your child classes than defines in your parent classes.

This concludes our explanation of the third SOLID principle, the Liskov Substitution Principle.

Thanks for reading!

Contact

Let us help you to realise your ambitions