Design patterns
Skill delphicleancode/delphi-spec-kit/.claude/skills/design-patterns
An opinionated ecosystem of rules, skills, and steerings to elevate Delphi development to a state-of-the-art level with Artificial Intelligence.
npx -y skills add delphicleancode/delphi-spec-kit --skill design-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Implementation of the 23 GoF (Gang of Four) patterns in Object Pascal / Delphi with interfaces, TInterfacedObject and SOLID principles. Covers Creational, Structural and Behavioral patterns.
SKILL.md
22.0 KB, as published. Nobody here has run it
Design Patterns GoF in Delphi — Skill
Use this skill when the user requests implementation of design patterns in Delphi. Always apply together with naming conventions (T/I/E/F/A/L) and memory management (try..finally).
When to Use
- Create
Factory,Abstract FactoryorBuilderfor creating complex objects - Implement
Strategyto vary algorithms (calculation of freight, taxes, export) - Use
Observerfor decoupled notifications (Domain Events) - Apply
Commandfor undo/redo, job queues or auditing - Use
Decoratorto add responsibilities without inheritance - Implement
Adapterfor integration with legacy systems - Use
Facadeto simplify complex subsystems (e.g. NFe emission) - Apply
Template Methodto algorithms with variations (reports, exports) - Use
Statefor behavior that changes depending on the state of the object - Use
Chain of Responsibilityfor validation or processing pipelines
🏗️ Creational Patterns
Singleton — Global Configuration
unit MeuApp.Infra.AppConfig;
interface
type
TAppConfig = class
private
class var FInstance: TAppConfig;
FDatabaseUrl: string;
FApiKey: string;
constructor Create;
public
class function GetInstance: TAppConfig;
class procedure ReleaseInstance;
property DatabaseUrl: string read FDatabaseUrl write FDatabaseUrl;
property ApiKey: string read FApiKey write FApiKey;
end;
implementation
constructor TAppConfig.Create;
begin
inherited Create;
FDatabaseUrl := 'localhost:5432/myapp';
end;
class function TAppConfig.GetInstance: TAppConfig;
begin
if not Assigned(FInstance) then
FInstance := TAppConfig.Create;
Result := FInstance;
end;
class procedure TAppConfig.ReleaseInstance;
begin
FreeAndNil(FInstance);
end;
initialization
finalization
TAppConfig.ReleaseInstance;
end.
Factory Method — Creation with Polymorphism
unit MeuApp.Domain.Report.Factory;
interface
uses
MeuApp.Domain.Report.Intf;
type
IReportExporter = interface
['{A1B2-...}']
procedure Export(const AData: TReportData; const AFilePath: string);
end;
//Factory method — each subclass decides which exporter to create
TReportExporterFactory = class abstract
public
function CreateExporter: IReportExporter; virtual; abstract;
//Template Method usando Factory Method
procedure ExportReport(const AData: TReportData; const AFilePath: string);
end;
TPdfReportFactory = class(TReportExporterFactory)
function CreateExporter: IReportExporter; override;
end;
TExcelReportFactory = class(TReportExporterFactory)
function CreateExporter: IReportExporter; override;
end;
implementation
procedure TReportExporterFactory.ExportReport(const AData: TReportData; const AFilePath: string);
var
LExporter: IReportExporter;
begin
LExporter := CreateExporter;
LExporter.Export(AData, AFilePath);
end;
Abstract Factory — Family of Related Objects
unit MeuApp.Infra.UI.Factory;
interface
type
IButton = interface ['{...}'] procedure Render; end;
IInputField = interface ['{...}'] procedure Render; end;
IDialog = interface ['{...}'] procedure Show(const AMsg: string); end;
//Abstract Factory
IUIComponentFactory = interface
['{B2C3-...}']
function CreateButton(const ACaption: string): IButton;
function CreateInputField(const APlaceholder: string): IInputField;
function CreateDialog: IDialog;
end;
TVCLComponentFactory = class(TInterfacedObject, IUIComponentFactory)
function CreateButton(const ACaption: string): IButton;
function CreateInputField(const APlaceholder: string): IInputField;
function CreateDialog: IDialog;
end;
TFMXComponentFactory = class(TInterfacedObject, IUIComponentFactory)
function CreateButton(const ACaption: string): IButton;
function CreateInputField(const APlaceholder: string): IInputField;
function CreateDialog: IDialog;
end;
Builder — Step by Step Construction
unit MeuApp.Infra.Query.Builder;
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections;
type
///<summary>
///Builder for fluent construction of parameterized SQL queries.
///</summary>
TQueryBuilder = class
private
FSelect: string;
FFrom: string;
FWheres: TStringList;
FOrderBy: string;
FLimit: Integer;
public
constructor Create;
destructor Destroy; override;
function Select(const AFields: string): TQueryBuilder;
function From(const ATable: string): TQueryBuilder;
function Where(const ACondition: string): TQueryBuilder;
function OrderBy(const AField: string; ADesc: Boolean = False): TQueryBuilder;
function Limit(ACount: Integer): TQueryBuilder;
function Build: string;
end;
implementation
constructor TQueryBuilder.Create;
begin
inherited Create;
FWheres := TStringList.Create;
FLimit := 0;
end;
destructor TQueryBuilder.Destroy;
begin
FWheres.Free;
inherited Destroy;
end;
function TQueryBuilder.Select(const AFields: string): TQueryBuilder;
begin
FSelect := AFields;
Result := Self;
end;
function TQueryBuilder.From(const ATable: string): TQueryBuilder;
begin
FFrom := ATable;
Result := Self;
end;
function TQueryBuilder.Where(const ACondition: string): TQueryBuilder;
begin
FWheres.Add(ACondition);
Result := Self;
end;
function TQueryBuilder.OrderBy(const AField: string; ADesc: Boolean): TQueryBuilder;
begin
FOrderBy := AField;
if ADesc then FOrderBy := FOrderBy + ' DESC';
Result := Self;
end;
function TQueryBuilder.Limit(ACount: Integer): TQueryBuilder;
begin
FLimit := ACount;
Result := Self;
end;
function TQueryBuilder.Build: string;
var
LSql: TStringBuilder;
begin
LSql := TStringBuilder.Create;
try
LSql.Append('SELECT ').Append(FSelect);
LSql.Append(' FROM ').Append(FFrom);
if FWheres.Count > 0 then
LSql.Append(' WHERE ').Append(String.Join(' AND ', FWheres.ToStringArray));
if not FOrderBy.IsEmpty then
LSql.Append(' ORDER BY ').Append(FOrderBy);
if FLimit > 0 then
LSql.Append(' LIMIT ').Append(FLimit.ToString);
Result := LSql.ToString;
finally
LSql.Free;
end;
end;
🔧 Structural Patterns
Adapter — Legacy Integration
unit MeuApp.Infra.Payment.Adapter;
interface
type
//Legacy system — cannot be changed
TLegacyPaymentGateway = class
procedure ProcessarPagamento(AValor: Double; ACodigoCartao: string);
end;
//Interface expected by the modern domain
IPaymentGateway = interface
['{C3D4-...}']
procedure ProcessPayment(AAmount: Currency; const ACardToken: string);
end;
//Adapter: translates the new call to the legacy one
TLegacyPaymentAdapter = class(TInterfacedObject, IPaymentGateway)
private
FLegacy: TLegacyPaymentGateway;
public
constructor Create(ALegacy: TLegacyPaymentGateway);
destructor Destroy; override;
procedure ProcessPayment(AAmount: Currency; const ACardToken: string);
end;
implementation
constructor TLegacyPaymentAdapter.Create(ALegacy: TLegacyPaymentGateway);
begin
inherited Create;
if not Assigned(ALegacy) then
raise EArgumentNilException.Create('ALegacy gateway cannot be nil');
FLegacy := ALegacy;
end;
destructor TLegacyPaymentAdapter.Destroy;
begin
FLegacy.Free; //The adapter has the legacy
inherited Destroy;
end;
procedure TLegacyPaymentAdapter.ProcessPayment(AAmount: Currency; const ACardToken: string);
begin
FLegacy.ProcessarPagamento(AAmount, ACardToken);
end;
Decorator — Extension without Inheritance
unit MeuApp.Infra.Logger.Decorators;
interface
type
ILogger = interface
['{D4E5-...}']
procedure Log(const ALevel, AMessage: string);
end;
TConsoleLogger = class(TInterfacedObject, ILogger)
procedure Log(const ALevel, AMessage: string);
end;
//Decorator: adds timestamp
TTimestampDecorator = class(TInterfacedObject, ILogger)
private
FInner: ILogger;
public
constructor Create(AInner: ILogger);
procedure Log(const ALevel, AMessage: string);
end;
//Decorator: filters by minimum level
TLevelFilterDecorator = class(TInterfacedObject, ILogger)
private
FInner: ILogger;
FMinLevel: string;
public
constructor Create(AInner: ILogger; const AMinLevel: string);
procedure Log(const ALevel, AMessage: string);
end;
implementation
procedure TConsoleLogger.Log(const ALevel, AMessage: string);
begin
Writeln(Format('[%s] %s', [ALevel, AMessage]));
end;
procedure TTimestampDecorator.Log(const ALevel, AMessage: string);
begin
FInner.Log(ALevel, Format('%s | %s', [FormatDateTime('yyyy-mm-dd hh:nn:ss', Now), AMessage]));
end;
procedure TLevelFilterDecorator.Log(const ALevel, AMessage: string);
begin
if ALevel >= FMinLevel then //DEBUG filter in production
FInner.Log(ALevel, AMessage);
end;
Facade — Simplifying Subsystems
unit MeuApp.Application.NFe.Facade;
interface
uses
MeuApp.Domain.NFe.Entity,
MeuApp.Domain.NFe.Repository.Intf;
type
///<summary>
///Facade that simplifies the complete process of issuing NF-e,
///hiding XML, digital signature and communication with SEFAZ.
///</summary>
TNFeFacade = class
private
FXmlBuilder: TNFeXmlBuilder;
FSigner: TDigitalSigner;
FTransmitter: TSefazTransmitter;
FRepository: INFeRepository;
procedure GenerateXml(ANFe: TNFe);
procedure SignXml(ANFe: TNFe);
procedure TransmitToSefaz(ANFe: TNFe);
procedure PersistResult(ANFe: TNFe);
public
constructor Create(ARepository: INFeRepository);
destructor Destroy; override;
///<summary>Issue NF-e: generates XML → signs → transmits → persists.</summary>
procedure EmitirNFe(ANFe: TNFe);
///<summary>Cancels NF-e already issued.</summary>
procedure CancelarNFe(const AChaveAcesso: string; const AMotivo: string);
end;
Proxy — Access Control
unit MeuApp.Infra.Customer.Repository.Proxy;
interface
uses
MeuApp.Domain.Customer.Entity,
MeuApp.Domain.Customer.Repository.Intf,
MeuApp.Domain.User.Entity;
type
///<summary>
///Security proxy that checks permissions before delegating to the real repository.
///</summary>
TSecureCustomerRepositoryProxy = class(TInterfacedObject, ICustomerRepository)
private
FReal: ICustomerRepository;
FCurrentUser: TUser;
procedure CheckPermission(const AAction: string);
public
constructor Create(AReal: ICustomerRepository; ACurrentUser: TUser);
function FindById(AId: Integer): TCustomer;
function FindAll: TObjectList<TCustomer>;
procedure Insert(ACustomer: TCustomer);
procedure Update(ACustomer: TCustomer);
procedure Delete(AId: Integer);
end;
implementation
procedure TSecureCustomerRepositoryProxy.CheckPermission(const AAction: string);
begin
if not FCurrentUser.HasPermission(AAction) then
raise EAuthorizationException.CreateFmt(
'User "%s" does not have permission: %s', [FCurrentUser.Login, AAction]);
end;
procedure TSecureCustomerRepositoryProxy.Delete(AId: Integer);
begin
CheckPermission('CUSTOMER_DELETE');
FReal.Delete(AId);
end;
🎭 Behavioral Patterns
Strategy — Variation of Algorithms
unit MeuApp.Domain.Tax.Strategies;
interface
type
ITaxStrategy = interface
['{E5F6-...}']
function Calculate(ABaseValue: Currency): Currency;
function GetDescription: string;
end;
TSimplesTaxStrategy = class(TInterfacedObject, ITaxStrategy)
public
function Calculate(ABaseValue: Currency): Currency;
function GetDescription: string;
end;
TLucroPresumidoStrategy = class(TInterfacedObject, ITaxStrategy)
public
function Calculate(ABaseValue: Currency): Currency;
function GetDescription: string;
end;
TIsencaoStrategy = class(TInterfacedObject, ITaxStrategy)
public
function Calculate(ABaseValue: Currency): Currency;
function GetDescription: string;
end;
implementation
const
SIMPLES_RATE = 0.06;
LUCRO_PRESUMIDO_RATE = 0.15;
function TSimplesTaxStrategy.Calculate(ABaseValue: Currency): Currency;
begin
Result := ABaseValue * SIMPLES_RATE;
end;
function TSimplesTaxStrategy.GetDescription: string;
begin
Result := 'Simples Nacional (6%)';
end;
function TIsencaoStrategy.Calculate(ABaseValue: Currency): Currency;
begin
Result := 0;
end;
function TIsencaoStrategy.GetDescription: string;
begin
Result := 'Isento de impostos';
end;
Observer — Domain Events
unit MeuApp.Domain.Order.Events;
interface
uses
System.Generics.Collections,
MeuApp.Domain.Order.Entity;
type
IOrderEventObserver = interface
['{F6A7-...}']
procedure OnOrderPlaced(AOrder: TOrder);
procedure OnOrderCancelled(AOrder: TOrder);
end;
TOrderEventPublisher = class
private
FObservers: TList<IOrderEventObserver>;
public
constructor Create;
destructor Destroy; override;
procedure Subscribe(AObserver: IOrderEventObserver);
procedure Unsubscribe(AObserver: IOrderEventObserver);
procedure NotifyOrderPlaced(AOrder: TOrder);
procedure NotifyOrderCancelled(AOrder: TOrder);
end;
//Observers concretos
TEmailOrderNotifier = class(TInterfacedObject, IOrderEventObserver)
procedure OnOrderPlaced(AOrder: TOrder); //send confirmation by email
procedure OnOrderCancelled(AOrder: TOrder); //sends cancellation notice
end;
TStockReservationObserver = class(TInterfacedObject, IOrderEventObserver)
procedure OnOrderPlaced(AOrder: TOrder); //reserve stock
procedure OnOrderCancelled(AOrder: TOrder); //releases stock
end;
implementation
constructor TOrderEventPublisher.Create;
begin
inherited Create;
FObservers := TList<IOrderEventObserver>.Create;
end;
destructor TOrderEventPublisher.Destroy;
begin
FObservers.Free;
inherited Destroy;
end;
procedure TOrderEventPublisher.NotifyOrderPlaced(AOrder: TOrder);
var
LObserver: IOrderEventObserver;
begin
for LObserver in FObservers do
LObserver.OnOrderPlaced(AOrder);
end;
Command — Wrapping Actions
unit MeuApp.Application.Commands;
interface
uses
MeuApp.Domain.Customer.Entity,
MeuApp.Domain.Customer.Repository.Intf;
type
ICommand = interface
['{A7B8-...}']
procedure Execute;
procedure Undo;
function GetDescription: string;
end;
TCreateCustomerCommand = class(TInterfacedObject, ICommand)
private
FRepository: ICustomerRepository;
FCustomerData: TCustomerDTO;
FCreatedId: Integer;
public
constructor Create(ARepository: ICustomerRepository; AData: TCustomerDTO);
procedure Execute;
procedure Undo;
function GetDescription: string;
end;
TDeleteCustomerCommand = class(TInterfacedObject, ICommand)
private
FRepository: ICustomerRepository;
FCustomerId: Integer;
FBackup: TCustomer;
public
constructor Create(ARepository: ICustomerRepository; ACustomerId: Integer);
destructor Destroy; override;
procedure Execute;
procedure Undo;
function GetDescription: string;
end;
//Command History (for Undo/Redo)
TCommandHistory = class
private
FHistory: TStack<ICommand>;
FRedoStack: TStack<ICommand>;
public
constructor Create;
destructor Destroy; override;
procedure Execute(ACommand: ICommand);
procedure Undo;
procedure Redo;
function CanUndo: Boolean;
function CanRedo: Boolean;
end;
Template Method — Algorithm Skeleton
unit MeuApp.Application.Report.Generator;
interface
type
TReportData = record
Title: string;
StartDate: TDate;
EndDate: TDate;
end;
///<summary>
///Abstract base that defines the skeleton of the report generation algorithm.
///Subclasses implement variable steps.
///</summary>
TReportGenerator = class abstract
protected
FData: TReportData;
procedure LoadData; virtual; abstract;
procedure ValidateData; virtual; //hook with default implementation
procedure ProcessData; virtual; abstract;
procedure FormatOutput; virtual; abstract;
procedure SaveOutput(const APath: string); virtual; abstract;
procedure SendNotification; virtual; //optional hook — default: noop
public
//Template Method — cannot be overwritten (final)
procedure Generate(AData: TReportData; const ASavePath: string);
end;
TSalesReportGenerator = class(TReportGenerator)
protected
procedure LoadData; override;
procedure ProcessData; override;
procedure FormatOutput; override;
procedure SaveOutput(const APath: string); override;
procedure SendNotification; override;
end;
implementation
procedure TReportGenerator.Generate(AData: TReportData; const ASavePath: string);
begin
FData := AData;
LoadData;
ValidateData;
ProcessData;
FormatOutput;
SaveOutput(ASavePath);
SendNotification;
end;
procedure TReportGenerator.ValidateData;
begin
if FData.Title.Trim.IsEmpty then
raise EValidationException.Create('Report title cannot be empty');
if FData.StartDate > FData.EndDate then
raise EValidationException.Create('StartDate must be before EndDate');
end;
procedure TReportGenerator.SendNotification;
begin
//Default hook: empty. Subclasses can override to send email etc.
end;
Chain of Responsibility — Validation Pipeline
unit MeuApp.Application.Validation.Chain;
interface
uses
MeuApp.Domain.Customer.Entity;
type
TValidationResult = record
IsValid: Boolean;
ErrorMessage: string;
class function Ok: TValidationResult; static;
class function Fail(const AMessage: string): TValidationResult; static;
end;
IValidationHandler = interface
['{B8C9-...}']
procedure SetNext(AHandler: IValidationHandler);
function Validate(ACustomer: TCustomer): TValidationResult;
end;
TBaseValidationHandler = class(TInterfacedObject, IValidationHandler)
private
FNext: IValidationHandler;
public
procedure SetNext(AHandler: IValidationHandler);
function Validate(ACustomer: TCustomer): TValidationResult; virtual;
end;
TNameValidationHandler = class(TBaseValidationHandler)
function Validate(ACustomer: TCustomer): TValidationResult; override;
end;
TCpfValidationHandler = class(TBaseValidationHandler)
function Validate(ACustomer: TCustomer): TValidationResult; override;
end;
TEmailValidationHandler = class(TBaseValidationHandler)
function Validate(ACustomer: TCustomer): TValidationResult; override;
end;
implementation
function TBaseValidationHandler.Validate(ACustomer: TCustomer): TValidationResult;
begin
//Delegates to the next handler if there is one
if Assigned(FNext) then
Result := FNext.Validate(ACustomer)
else
Result := TValidationResult.Ok;
end;
function TNameValidationHandler.Validate(ACustomer: TCustomer): TValidationResult;
begin
if ACustomer.Name.Trim.IsEmpty then
Exit(TValidationResult.Fail('Nome é obrigatório'));
if ACustomer.Name.Length < 3 then
Exit(TValidationResult.Fail('Nome deve ter ao menos 3 caracteres'));
Result := inherited Validate(ACustomer); //continue the chain
end;
State — Behavior by State
unit MeuApp.Domain.Order.States;
interface
uses
MeuApp.Domain.Order.Entity;
type
IOrderState = interface
['{C9D0-...}']
procedure Confirm(AOrder: TOrder);
procedure Ship(AOrder: TOrder);
procedure Deliver(AOrder: TOrder);
procedure Cancel(AOrder: TOrder);
function GetStatusDescription: string;
end;
//Condition: New (to be confirmed)
TNewOrderState = class(TInterfacedObject, IOrderState)
public
procedure Confirm(AOrder: TOrder);
procedure Ship(AOrder: TOrder); //throws EInvalidOperationException
procedure Deliver(AOrder: TOrder);
procedure Cancel(AOrder: TOrder);
function GetStatusDescription: string;
end;
//Status: Confirmed (awaiting shipment)
TConfirmedOrderState = class(TInterfacedObject, IOrderState)
public
procedure Confirm(AOrder: TOrder); //throws EInvalidOperationException
procedure Ship(AOrder: TOrder);
procedure Deliver(AOrder: TOrder);
procedure Cancel(AOrder: TOrder);
function GetStatusDescription: string;
end;
//Status: In transit
TShippedOrderState = class(TInterfacedObject, IOrderState)
public
procedure Confirm(AOrder: TOrder);
procedure Ship(AOrder: TOrder);
procedure Deliver(AOrder: TOrder);
procedure Cancel(AOrder: TOrder); //requires calling the carrier
function GetStatusDescription: string;
end;
implementation
procedure TNewOrderState.Confirm(AOrder: TOrder);
begin
AOrder.ConfirmedAt := Now;
AOrder.SetState(TConfirmedOrderState.Create); //transition
end;
procedure TNewOrderState.Ship(AOrder: TOrder);
begin
raise EInvalidOperationException.Create('Pedido ainda não confirmado. Confirme antes de enviar.');
end;
function TNewOrderState.GetStatusDescription: string;
begin
Result := 'Novo — aguardando confirmação';
end;
📌 Guide to Choosing the Right Pattern
| Need | Standard |
|---|---|
| Create objects with type variation | Factory Method / Abstract Factory |
| Create complex objects step by step | Builder |
| Ensure Global Single Instance | Singleton |
| Copy objects | Prototype |
| Adapt incompatible interface | Adapter |
| Add responsibilities dynamically | Decorator |
| Simplify complex system | Facade |
| Control access to an object | Proxy |
| Compose objects in tree structure | Composite |
| Vary algorithm without changing context | Strategy |
| Notify multiple objects about changes | Observer |
| Encapsulate actions with undo/redo | Command |
| Algorithm with variations | Template Method |
| Pass request through handler chain | Chain of Responsibility |
| Behavior that changes with the state | State |
✅ Final Checklist
- All dependencies injected via constructor (DIP)
- Every interface implementation uses
TInterfacedObject(ARC) - No
.Createwithouttry..finally(except on ARC interfaces) - Each class has a single responsibility (SRP)
- DUnitX tests cover each pattern with Fakes/Stubs
- XMLDoc in Portuguese in public methods
- Prefixes:
Tclass,Iinterface,Eexception,Ffield,Aparam,Llocation