# 15. Interfaces and SOLID Design
The Foundation for Robust Architecture
Before diving into interface-based services, we need to understand the fundamentals of interfaces in Delphi and the SOLID design principles that guide their effective use. This chapter establishes the theoretical foundation; Chapter 16 covers the practical implementation of SOA services.
In Delphi's OOP model, an interface defines a type comprising abstract virtual methods. It declares "what" is available, not "how" it's implemented — this is the abstraction benefit of interfaces.
type
ICalculator = interface(IInvokable)
['{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}']
/// add two signed 32-bit integers
function Add(n1, n2: Integer): Integer;
end;
Key characteristics:
ICalculator starts with I (convention for interfaces, vs T for classes)Ctrl+Shift+G in the IDE to generate)IInvokable for RTTI supporttype
TServiceCalculator = class(TInterfacedObject, ICalculator)
public
function Add(n1, n2: Integer): Integer;
end;
function TServiceCalculator.Add(n1, n2: Integer): Integer;
begin
Result := n1 + n2;
end;
Notes:
TInterfacedObject and implements ICalculatorclass(TInterfacedObject, ICalculator, IAnotherInterface)Classic way (explicit class instance):
function MyAdd(a, b: Integer): Integer;
var
Calculator: TServiceCalculator;
begin
Calculator := TServiceCalculator.Create;
try
Result := Calculator.Add(a, b);
finally
Calculator.Free;
end;
end;
Interface way (reference-counted):
function MyAdd(a, b: Integer): Integer;
var
Calculator: ICalculator;
begin
Calculator := TServiceCalculator.Create;
Result := Calculator.Add(a, b);
end; // Calculator automatically freed when out of scope
Key benefits:
Interfaces are orthogonal to class implementations:
type
TOtherCalculator = class(TInterfacedObject, ICalculator)
public
function Add(n1, n2: Integer): Integer;
end;
function TOtherCalculator.Add(n1, n2: Integer): Integer;
begin
Result := n2 + n1; // Different implementation, same interface
end;
The client code doesn't need to change:
var
Calculator: ICalculator;
begin
Calculator := TOtherCalculator.Create; // Different class, same interface
Result := Calculator.Add(a, b);
end;
mORMot leverages interfaces for Client-Server communication:
The SOLID acronym represents five principles for maintainable OOP design:
| Principle | Summary |
|---|---|
| Single Responsibility | One reason to change per class |
| Open/Closed | Open for extension, closed for modification |
| Liskov Substitution | Subtypes must be substitutable for base types |
| Interface Segregation | Many specific interfaces over one general-purpose |
| Dependency Inversion | Depend on abstractions, not concretions |
"A class should have only one reason to change."
Bad — TBarcodeScanner handles both protocol and communication:
type
TBarcodeScanner = class
function ReadFrame: TProtocolFrame;
procedure WriteFrame(const Frame: TProtocolFrame);
procedure SetComPort(const Port: string); // Serial communication
procedure SetUsbDevice(DeviceID: Integer); // USB communication
end;
Good — Separated responsibilities:
type
// Connection abstraction
TAbstractBarcodeConnection = class
function ReadChar: Byte; virtual; abstract;
procedure WriteChar(aChar: Byte); virtual; abstract;
end;
// Protocol abstraction
TAbstractBarcodeProtocol = class
protected
fConnection: TAbstractBarcodeConnection;
public
function ReadFrame: TProtocolFrame; virtual; abstract;
procedure WriteFrame(const Frame: TProtocolFrame); virtual; abstract;
end;
// Composed scanner
TBarcodeScanner = class
protected
fProtocol: TAbstractBarcodeProtocol;
fConnection: TAbstractBarcodeConnection;
public
property Protocol: TAbstractBarcodeProtocol read fProtocol;
property Connection: TAbstractBarcodeConnection read fConnection;
end;
Smell in uses clause:
unit MyDataModel;
uses
Vcl.Forms, // BAD: Couples data to GUI framework
Windows, // BAD: Couples to operating system
mormot.orm.core;
Keep business logic units free of:
Windows, Posix)mormot.orm.core.pas has no GUI dependencies.
"Software entities should be open for extension, but closed for modification."
Guidelines:
protected (for inheritance) or private (hidden)type
TMyRestServer = class(TRestServerDB)
published
procedure MyCustomService(Ctxt: TRestServerUriContext);
end;
You extend by inheritance, not by editing mormot.rest.server.pas.
"Objects of a supertype should be replaceable with objects of any subtype."
mORMot Example:
var
Rest: TRest; // Abstract parent type
begin
// Either implementation works identically:
Rest := TRestServerDB.Create(Model, 'mydata.db3');
// OR
Rest := TRestHttpClientSocket.Create('server', '8080', Model);
// Same API regardless of implementation:
Rest.Orm.Add(MyRecord);
end;
Violations to avoid:
procedure TAbstractScanner.Process;
begin
// BAD: Type checking breaks substitutability
if Self is TSerialScanner then
// Serial-specific code
else if Self is TUsbScanner then
// USB-specific code
end;
"Many client-specific interfaces are better than one general-purpose interface."
Bad — Fat interface:
type
IEverything = interface
procedure DoThis;
procedure DoThat;
procedure DoSomethingElse;
// ... 50 more methods
end;
Good — Segregated interfaces:
type
IDoThis = interface
procedure DoThis;
end;
IDoThat = interface
procedure DoThat;
end;
This is especially important in SOA: define small, focused service interfaces rather than monolithic ones.
"Depend on abstractions, not concretions."
Bad — Direct dependency on implementation:
type
TOrderService = class
private
fDatabase: TSQLiteDatabase; // Concrete class
end;
Good — Dependency on abstraction:
type
TOrderService = class
private
fRepository: IOrderRepository; // Interface abstraction
public
constructor Create(const aRepository: IOrderRepository);
end;
This enables:
Interface reference counting can cause memory leaks with circular references:
type
IParent = interface
procedure SetChild(const Value: IChild);
function GetChild: IChild;
end;
IChild = interface
procedure SetParent(const Value: IParent);
function GetParent: IParent;
end;
If Parent.Child references Child, and Child.Parent references Parent, neither will ever be freed — both maintain a reference count ≥ 1 indefinitely.
mORMot provides SetWeak to bypass reference counting:
uses
mormot.core.interfaces;
procedure TChild.SetParent(const Value: IParent);
begin
SetWeak(@fParent, Value); // No reference count increment
end;
The child holds a reference to parent, but doesn't prevent parent's destruction.
For safer weak references that automatically become nil when the target is freed:
procedure TChild.SetParent(const Value: IParent);
begin
SetWeakZero(Self, @fParent, Value); // Auto-nils when parent freed
end;
When Parent is destroyed:
SetWeak: fParent becomes a dangling pointer (dangerous!)SetWeakZero: fParent automatically becomes nil (safe)The simplest and most explicit form:
type
IUserRepository = interface(IInvokable)
['{B21E5B21-28F4-4874-8446-BD0B06DAA07F}']
function GetUserByName(const Name: RawUtf8): TUser;
procedure Save(const User: TUser);
end;
ISmsSender = interface(IInvokable)
['{8F87CB56-5E2F-437E-B2E6-B3020835DC61}']
function Send(const Text, Number: RawUtf8): Boolean;
end;
TLoginController = class(TInterfacedObject, ILoginController)
private
fUserRepository: IUserRepository;
fSmsSender: ISmsSender;
public
constructor Create(const aUserRepository: IUserRepository;
const aSmsSender: ISmsSender);
procedure ForgotMyPassword(const UserName: RawUtf8);
end;
constructor TLoginController.Create(const aUserRepository: IUserRepository;
const aSmsSender: ISmsSender);
begin
fUserRepository := aUserRepository;
fSmsSender := aSmsSender;
end;
For automatic resolution of dependencies, inherit from TInjectableObject:
uses
mormot.core.interfaces;
type
TMyService = class(TInjectableObject, IMyService)
private
fCalculator: ICalculator; // Auto-resolved
published
property Calculator: ICalculator read fCalculator;
public
function DoWork: Integer;
end;
Published interface properties are automatically resolved when the object is created through the DI container.
For on-demand resolution:
procedure TMyService.DoSomething;
var
Repository: IOrderRepository;
begin
Resolve(IOrderRepository, Repository); // Resolve when needed
Repository.SaveOrder(Order);
end;
| Type | Purpose |
|---|---|
| Stub | Fake implementation returning pre-arranged responses |
| Mock | Fake that verifies interactions (method calls, parameters) |
uses
mormot.core.interfaces;
procedure TMyTest.TestForgotPassword;
var
SmsSender: ISmsSender;
UserRepository: IUserRepository;
begin
// Create stub that returns true for Send method
TInterfaceStub.Create(TypeInfo(ISmsSender), SmsSender)
.Returns('Send', [True]);
// Create mock that expects Save to be called once
TInterfaceMock.Create(TypeInfo(IUserRepository), UserRepository, Self)
.ExpectsCount('Save', qoEqualTo, 1);
// Run the test
with TLoginController.Create(UserRepository, SmsSender) do
try
ForgotMyPassword('testuser');
finally
Free;
end;
// Verification happens automatically when UserRepository goes out of scope
end;
Simple returns:
TInterfaceStub.Create(TypeInfo(ICalculator), Calc)
.Returns('Add', [42]); // Add always returns 42
Conditional returns:
TInterfaceStub.Create(TypeInfo(ICalculator), Calc)
.Returns('Add', [1, 2], [3]) // Add(1,2) returns 3
.Returns('Add', [10, 20], [30]); // Add(10,20) returns 30
Using a callback for complex behavior:
procedure TMyTest.SubtractCallback(Ctxt: TOnInterfaceStubExecuteParamsVariant);
begin
Ctxt['Result'] := Ctxt['n1'] - Ctxt['n2'];
end;
TInterfaceStub.Create(TypeInfo(ICalculator), Calc)
.Executes('Subtract', SubtractCallback);
TInterfaceMock.Create(TypeInfo(ICalculator), Calc, Self)
// Expect Multiply to be called exactly twice
.ExpectsCount('Multiply', qoEqualTo, 2)
// Expect Add to be called at least once
.ExpectsCount('Add', qoGreaterThan, 0)
// Expect specific call sequence
.ExpectsTrace('Add(10,20)=[30],Multiply(5,6)=[30]');
For "run then verify" testing:
procedure TMyTest.TestCalculator;
var
Calc: ICalculator;
Spy: TInterfaceMockSpy;
begin
Spy := TInterfaceMockSpy.Create(TypeInfo(ICalculator), Calc, Self);
// Run code under test
Calc.Add(10, 20);
Calc.Multiply(5, 6);
// Verify after execution
Spy.Verify('Add');
Spy.Verify('Multiply', [5, 6]);
end;
Register interfaces at initialization for cleaner code:
unit MyInterfaces;
interface
type
ICalculator = interface(IInvokable)
['{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}']
function Add(n1, n2: Integer): Integer;
end;
IUserRepository = interface(IInvokable)
['{B21E5B21-28F4-4874-8446-BD0B06DAA07F}']
function GetUserByName(const Name: RawUtf8): TUser;
end;
implementation
uses
mormot.core.interfaces;
initialization
TInterfaceFactory.RegisterInterfaces([
TypeInfo(ICalculator),
TypeInfo(IUserRepository)
]);
end.
After registration, you can use interface types directly (no TypeInfo()):
// Instead of:
TInterfaceStub.Create(TypeInfo(ICalculator), Calc);
// You can write:
TInterfaceStub.Create(ICalculator, Calc);
This chapter covered the foundations for interface-based development:
| Previous | Index | Next |
|---|---|---|
| Chapter 14: Client-Server Services via Methods | Index | Chapter 16: Client-Server Services via Interfaces |