Knowledge Base

SOLID principles in Delphi [5] – The Dependency Inversion Principle

It’s the last SOLID principle: the Dependency Inversion Principle:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

The general idea of this principle is simple; If high-level modules depend on low-level modules, we should change (invert) that, and both high level and low-level modules should use abstractions, not implementations.

Let’s start with the difference between high-level modules and low-level modules. High-level modules are modules that provide or use complex logic and are using other modules or classes. Low-level modules are more like ‘utility’ modules, not depending on or using other modules. Have a look at the following examples of low level and high-level code:

Low-level examples:

type
  TLogger = class
  //<…>
  Public
    //<…>
    procedure LogMessage(const AMessage: string);
  end;


implementation

{ TLogger }

procedure TLogger.LogMessage(const AMessage: string);
begin
  Writeln(AMessage);
end;

or

type
  TEmailer = class
  //<…>
  Public
    //<…>
    procedure SendMessage(const AMessage, ARecipient: string);
  end;

implementation

{ TEmailer }

procedure TEmailer.SendMessage(const AMessage, ARecipient: string);
begin
  Writeln('Dummy implementation of email, just sent ' + AMessage + ' to ' + ARecipient);
end;

 

High-level example:

type
  TTaskItem = class
  private
    FName: string;
    FDueDate: TDateTime;
    FIsFinished: Boolean;
    FUsername: string;
    procedure SetDueDate(const Value: TDateTime);
    procedure SetName(const Value: string);
    procedure SetUsername(const Value: string);
  public
    constructor Create;
    procedure CompleteTask;

    property Name: string read FName write SetName;
    property Username: string read FUsername write SetUsername;
    property DueDate: TDateTime read FDueDate write SetDueDate;
    property IsFinished: Boolean read FIsFinished;
  end;

  TTaskManager = class
  strict private
    FTasks: TList<TTaskItem>;
  public
    constructor Create;
    destructor Destroy; override;

    procedure AddTask(const ATask: TTaskItem);
    procedure CompleteTask(ATask: TTaskItem);
  end;

An example implementation of the TTaskManager class can be:

procedure TTaskManager.AddTask(const ATask: TTaskItem);
var
  Emailer: TEmailer;
begin
  FTasks.Add(ATask);

  Emailer := TEmailer.Create;
  try
    Emailer.SendMessage('Task ' + ATask.Name + ' added', ATask.Username);
  finally
    Emailer.Free;
  end;
end;

As you can see in this example, our high-level module TTaskManager depends on the low-level module TEmailer (because of the TEmailer.Create call). And that is a problem, so we should invert that dependency. Why? You’ll figure that out later on.

Both low-level and high-level modules should also depend on abstractions. How do we change this? The simplest way is to use interfaces. An interface is by definition an abstraction, so by using interfaces instead of classes, we remove the dependency on implementations. As an extra benefit, we meet the second requirement of the Dependency Inversion as well, because an interface will not have the knowledge of the details involved of how things get done in the actual implementation.

Since we are concentrating here on explaining the fifth Solid principle, I will not explain in detail how to add an interface to a class and how to use it. In the previous posts about the Solid principles, we have done that several times. In the end, we will have the following interfaces:

ILogger
IMessageSender
ITaskItem
ITaskManager

The implementation of classes becomes as follows:

TLogger = class(TInterfacedObject, ILogger)
TEmailer = class(TInterfacedObject, IMessageSender)
TTaskItem = class(TInterfacedObject, ITaskItem)
TTaskManager = class(TInterfacedObject, ITaskManager)

 

Ok, so let’s start our transition to meet the Dependency Inversion principle by changing this example of the TTaskManager. There is more than one way of doing this. One is by using Dependency Injection. Dependency Inversion is the principle, and Dependency Injection is a way of making this principle work. However, it’s not mandatory to use Dependency Injection. Let me explain this.

Think of the dependency of our TTaskManager on TEmailer. Where does this dependency actually happen? The easiest way of identifying dependencies is by looking for .Create calls. In this case, the line

Emailer := TEmailer.Create;

introduces a dependency on the TEmailer class. In essence, every .Create call is a dependency on another class. As we have said, we do not want high-level modules to depend on low-level modules. So, for this class, we can use Dependency Injection to solve this. It is very simple; just inject the dependency via the constructor of the TTaskManager class:

constructor TTaskManager.Create(AMessageSender: IMessageSender);
begin
  FMessageSender := AMessageSender;
  FTasks := TList<ITaskItem>.Create;
end;

 

We now have injected the dependency on the emailer (or actually, to the IMessageSender) class, so we can use it without any problem:

procedure TTaskManager.AddTask(const ATask: ITaskItem);
begin
  FTasks.Add(ATask);

  FMessageSender.SendMessage('Task ' + ATask.Name + ' added', 
    ATask.Username);
end;

 

And that is just how dependency injection works. As long as you consequently push the creation of classes back (i.e. do not use the .Create anywhere) you will automatically use Dependency Injection.

But where does this lead us? In the end you have to create a class somewhere, right?

If you push back the creation of classes, you will end up in the program files (the DPR). Have a look at my example for this TaskManager example program:

program Solid5;
{$APPTYPE CONSOLE}
uses
//<…>

var
  ATask: ITaskItem;
  ATaskManager: ITaskManager;
  AMessageSender: IMessageSender;
begin
  AMessageSender := TEmail.Create;

  ATaskManager := TTaskManager.Create(AMessageSender);

  ATask := TTaskItem.Create;
  ATask.Name := 'Develop module X';
  ATask.UserName := 'Marco Geuze';
  ATask.DueDate := Tomorrow;
  ATaskManager.AddTask(ATask);

  ATask := TTaskItem.Create;
  ATask.Name := 'Codereview module Y';
  ATask.UserName := 'Marco Geuze';
  ATask.DueDate := IncWeek(now, 2);
  ATaskManager.AddTask(ATask);

  ATask.CompleteTask;

  Readln;
end.

 

In the example code, we create two tasks, add the tasks to the Taskmanager and complete one task. We create only classes in the DPR right now. But isn’t the DPR in fact itself also a high-level module? I think it is, and again, we should not create a dependency in a high-level module.

I’ve shown you the way of doing this in classes via Dependency Injection. Here I’d like to show you how it’s done via a factory class. The basic role of a factory class is to provide instances of interfaces. And as we are at the top level of our program, this is the place where we will create our classes. Let’s create a new unit and create our factory class in this new unit. As this is a factory class, we’ll use class functions to give back instances of classes:

unit ClassFactory;

interface

uses
//<…>

type
  TClassFactory = class
  public
    class function CreateTaskManager: ITaskManager;
    class function CreateTask: ITaskItem;
    class function CreateLogger: ILogger;
    class function CreateMessageSender: IMessageSender;
  end;

Let’s look at the implementation of the CreateTaskManager function for example:

class function TClassFactory.CreateTaskManager: ITaskManager;
begin
  Result := TTaskManager.Create(CreateMessageSender);
end;

 

and for the CreateMessageSender function:

class function TClassFactory.CreateMessageSender: IMessageSender;
begin
  Result := TEmailer.Create;
end;

and lastly to the DPR file again:

program Solid5;
{$APPTYPE CONSOLE}
uses
//<…>

var
  ATask: ITaskItem;
  ATaskManager: ITaskManager;
begin
  ATaskManager := TClassFactory.CreateTaskManager;

  ATask := TClassFactory.CreateTask;
  ATask.Name := 'Develop module X';
  ATask.UserName := 'Marco Geuze';
  ATask.DueDate := Tomorrow;
  ATaskManager.AddTask(ATask);

  ATask := TClassFactory.CreateTask;
  ATask.Name := 'Codereview module Y';
  ATask.UserName := 'Marco Geuze';
  ATask.DueDate := IncWeek(now, 2);
  ATaskManager.AddTask(ATask);

  ATask.CompleteTask;

  Readln;
end.

 

To summarise what we have done so far: In order to comply with the fifth Solid principle, we first use dependency injection to bring up the dependencies as far as possible. Because of this, no create call is used in the classes anywhere. Secondly, we converted the code to interfaces, so that we are no longer dependent on details, but on abstractions. And finally, we created one factory class in which all objects are created.

Finally, let’s see what advantage this gives us. In the code above, we have an Emailer class that takes care of sending an email when a task is completed. Now suppose we don’t want to do this via email, but via a text message. In the old situation we would have had to change the TEmailer instance everywhere it’s used to TTexter. This is because high-level modules were dependent on these low-level modules. In the new structure, it is very easy to change this. We only need to change the CreateMessageSender class in the ClassFactory:

class function TClassFactory.CreateMessageSender: IMessageSender;
begin
  Result := TTexter.Create;
end;

 

and with just one line of code, the whole application is adapted to the new requirements. Taking the other Solid principles into account (Liskov Substitution, Open Closed), we can make this adjustment without any problems. But there are more benefits. Let’s say we want to add unit tests to our application. If you just create a unit test project and test this application 50 times a day, you would end up with 50 emails, or 50 text messages in your inbox. So, in your unit test project, you can now simply have another factory class, with a dummy or mock implementation of the MessageSender class. And that’s all, no more emails or text messages.

So, that is the last one of the five SOLID principles. I hope you got a lot of useful information which you can use when developing in Delphi. I am convinced that your applications will be more robust, more readable and more extensible if you apply the five SOLID principles consistently when developing. Next time I will give a short summary and refresher of the principles. Thanks, and see you soon!

Contact

Let us help you to realise your ambitions