Knowledge Base

SOLID Principles in Delphi [4] – the Interface Segregation Principle

SOLID principle 4: Interface Segregation. “Clients should not be forced to depend upon interfaces that they do not use.”

The interface segregation principle is about finding the most appropriate abstractions in your code. Wikipedia has a nice description of this principle: “ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces “.

Let’s have a look at our previous Delphi interfaces from the Liskov Substitution Principle:

type
  IBaseEmployee = interface
<getters and setters>
    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 know, projects that are maintained and expanded over a long period of time inevitably receive requests that need to be implemented. Suppose you get a request to also store hired employees, for which external company they work and the cost of hiring. The simplest way of solving this would be to simply create two new fields in the IBaseEmployee: “HiringCosts” and “Company” of type “TCompany”. In this way, you still comply with the Liskov Substitution Principle, because you can simply replace an IBaseEmployee with an IManager and all the code will continue to work functionally as well.

IBaseEmployee = interface
{<'Implementations'>}
    property Name: string read GetName write SetName;
    property Salary: Double read GetSalary write SetSalary;

    property HiringCosts: Double read GetHiringCosts write SetHiringCosts;
    property Company: TCompany read GetCompany write SetCompany;
  end;

My experience is that you see this happen a lot in projects that have been around for a long time. It is in fact the quickest way to simply place the new functionality in the basic interface, or one of the derived interfaces. This makes the new functionality immediately available at the place in the implementation where you need it. It also complies with the Single Responsibility Principle; from the perspective of an HR employee, this functionality also belongs in the IBaseEmployee interface.

However, now the Interface Segregation Principle comes into play. The risk of this is that you not only make the interface (and therefore the class itself) larger and larger, but you also introduce functionality where it is not needed at all.

If we think about the right level of abstraction, it makes much more sense to put separate functionality for the hired workers in a separate interface. And while we’re at it, why not modify the Manager interface as well? If we look at the ISP, a Manageable employee is actually a separate role.

If we go through all this, we end up with the following interfaces:

  IBaseEmployee = interface
  <getters and setters>
    property EmployeeID: Integer read GetEmployeeID write SetEmployeeID;
    property Name: string read GetName write SetName;
    property Salary: Double read GetSalary write SetSalary;
  end;

  IHireable = interface
  <getters and setters>
    property HiringCosts: Double read GetHiringCosts write SetHiringCosts;
    property Company: TCompany read GetCompany write SetCompany;
  end;

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

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

  ICEO = interface
    procedure ReviewCompany;
  end;

Because we have moved the specific functionality to a role interface, our classes should reflect these changes. We have also added the TExternalEmployee. Shown are just the class headers:

  TBaseEmployee = class(TInterfacedObject, IBaseEmployee)
<>
  TExternalEmployee = class(TBaseEmployee, IHireable)
<>
  TManager = class(TBaseEmployee, IManaging)
<>
  TCEO = class(TBaseEmployee, ICEO, IManaging)
<>

Have your interfaces and classes designed like this, and you can use the functionality using the interface support call:

var
  Employee: IBaseEmployee;
begin
  Employee := TExternalEmployee.Create;
  Employee.Name := 'Jack Smith';

  if Supports(Employee, IHireable) then
    (Employee as IHireable).HiringCosts := 3750;

Using the Interface Segregation Principle makes your interfaces small, manageable, and easy to use. If you want to hire a CEO, just add the IHireable to the class and you are good to go. In other parts of your application, let’s say where you want to calculate total hiring costs, you only have to provide a list of IHireable items to do that (TList<IHireable>). You don’t have to know what type of employee it is (or even if it is an employee, it could be of a completely different type if you want).

So, you might wonder; what is the difference between the Single Responsibility Principle and the Interface Segregation Principle? I think they are different sides of the same coin; the SRP looks at a class or interface from a design perspective, where you would group together the things that change for the same reason and separate the ones that differ. ISP looks at a class or interface from the users or consumers perspective; you should only see what you actually need. One last example to show the difference:

SRP valid, but ISP invalid:

  IUserManagement = interface
    function GetUsers: TList<TUser>;
    function SaveUser(AUser: TUser);
  end;

ISP valid, but SRP invalid:

  IObjectManagement = interface
    function SaveUser(AUser: TUser);
    function SaveProduct(AProduct: TProduct);
  end;

ISP and SRP valid:

  IUserProvider = interface
    function GetUsers: TList<TUser>;
  end;

  IUserSaver = interface
    function SaveUser(AUser: TUser);
  end;

(And the same for the TProduct of course).

Thanks for reading, and just one principle to go! See you next time. 😊

Written by Marco Geuze
Director

Contact

Let us help you to realise your ambitions

GDK Software UK

(+44) 20 3355 4470

GDK Software USA

+1 (575) 733-5744