
Última atualização em 3 de junho de 2026 por Willian Tuttoilmondo
Você já olhou para aquele emaranhado de blocos try..finally, variáveis globais e heranças complexas no seu projeto e pensou: “Sério que a gente ainda codifica assim em Object Pascal?” Se a sua resposta foi sim, você está prestes a virar o jogo. No desenvolvimento moderno, a busca por legibilidade, produtividade e manutenabilidade nos levou a resgatar e refinar conceitos poderosos. Hoje, vamos falar sobre como implementar o Callback Pattern em Delphi integrado a uma Fluent Interface (Interface Fluida) com gerenciamento automático de memória utilizando o mecanismo de contagem de referências de interfaces.
Imagine disparar uma rotina complexa de envio de e-mails, configurar parâmetros como se estivesse escrevendo uma frase e gerenciar o sucesso ou a falha do processo na mesma linha, usando funções anônimas. É exatamente essa elegância sintática e eficiência que a arquitetura moderna nos proporciona. Neste artigo, você vai aprender a estruturar do zero uma DSL (Domain-Specific Language) em Delphi, entender o ciclo de vida de objetos baseados em escopo e dominar o uso de callbacks para transformar seu código legado em uma solução mais elegante, legível, fluida e sustentável.
Embora o Callback Pattern seja o elemento central da comunicação entre a aplicação e as operações executadas, a arquitetura apresentada vai além de sua implementação isolada. Ela combina Fluent Interface, Abstract Factory, Facade, Singleton e princípios do SOLID para construir uma API fluida e expressiva, capaz de simplificar significativamente o consumo das regras de negócio.
O que aprenderemos neste artigo
- Interfaces;
- Fluent Interface;
- Callbacks;
- Abstract Factory;
- Facade;
- Singleton;
- Liskov Substitution;
- Interface Segregation;
- Contagem de referências de interfaces.
O que é a arquitetura Callback Pattern
Nascida para simplificar o consumo de componentes de alta complexidade, a arquitetura baseada em Callback Pattern traz consigo uma marca muito difícil de ser superada: a legibilidade. Enquanto o desenvolvimento tradicional em Delphi foca em blocos try..except e try..finally para marcar os tratamentos de exceção e garantir que objetos criados sejam efetivamente destruídos, demandando linhas e linhas de código, controle estrito dos marcadores e, com isso, um código de leitura difícil e extensa, usar o Callback Pattern faz com que praticamente toda a sua rotina se resolva em um único fluxo fluido de execução.
Mesmo que, por trás do seu componente, as mesmas estruturas ainda sejam utilizadas, o componente final, o qual será consumido pelo desenvolvedor na aplicação ao desenvolver a regra de negócio, será muito mais simples e direto, demandando poucas linhas para resolver um problema. Assim, o código que passará por manutenções mais frequentes estará sempre limpo, simples e de fácil leitura.
Como implementar o Callback Pattern em Delphi
Ao contrário do que muitos podem pensar, a linguagem Delphi implementa um vasto conjunto de recursos que podem ser utilizados em conjunto para a obtenção de um código moderno e limpo. Um desses recursos já vimos antes, que são as interfaces fluidas – o conceito arquitetural Fluent Interface. Isso sempre esteve ali, com o Delphi, há muito tempo, mas pouquíssimos desenvolvedores sabiam o que estavam fazendo e, na imensa maioria dos casos, não usar este recurso acabava por deixar o código, que hoje chamamos de legado, sujo e com manutenção muito difícil.
Outro recurso que sempre esteve disponível eram os mecanismos de callback. Mesmo que de uma forma diferente, event notifiers declarados como procedure of object funcionavam como callbacks. O modelo de callback como conhecemos, utilizando métodos anônimos por reference to procedure, veio até nós com a versão 2009.
Por último, um recurso importantíssimo para implementarmos o Callback Pattern em Delphi também sempre esteve ali: a contagem de referências para interfaces. Ela nos permite instanciar objetos apenas pelo tempo necessário, sem que precisemos controlá-lo diretamente.
Quando trabalhamos com interfaces, o compilador utiliza o mecanismo de contagem de referências no Delphi para rastrear o ciclo de vida dos objetos de forma totalmente determinística. A cada nova atribuição ou passagem do objeto como parâmetro por meio de uma interface, o compilador gera automaticamente chamadas ao método _AddRef, incrementando o contador interno; assim que essa referência sai de escopo – o que acontece imediatamente quando a referência de objetos temporários e anônimos deixa de existir -, o método _Release é acionado de forma automática para decrementar o valor. Quando esse contador finalmente zera, o próprio ecossistema do Object Pascal se encarrega de destruir a instância e liberar a memória imediatamente após a última referência deixar de existir, eliminando drasticamente a necessidade do clássico bloco try…finally e reduzindo significativamente a possibilidade de memory leaks relacionados ao gerenciamento manual de objetos.
Sendo assim, com a união destes três “super poderes”, podemos implementar o Callback Pattern em Delphi tranquilamente.
A base de tudo está na interface
Como já vimos em artigos anteriores, usar interfaces nos traz vantagens imensas, como a delimitação mínima do escopo de uma classe, a segmentação de uma interface com muitas atribuições em interfaces menores e mais concisas e, principalmente, o controle automático do ciclo de vida da instância por meio da contagem de referências de interfaces. Então, para que qualquer projeto baseado em Callback Pattern em Delphi comece, precisamos estabelecer as interfaces que servirão de base.
Implementando as bases do projeto
Para começar, precisamos instituir a interface base para as operações, que ficará desta forma:
1 2 3 4 5 6 7 8 9 10 | IOperation = interface ['{BD5131FE-BED6-4E8C-9407-FFE8348F8DC2}'] procedure Calculate(out AResult: Double); function GetA: Double; function GetB: Double; function SetA(const AValue: Double): IOperation; function SetB(const AValue: Double): IOperation; property A: Double read GetA; property B: Double read GetB; end; |
Já vimos isso antes quando estudamos Fluent Interface para nossos projetos. Além do que já vimos, mais nada precisará ser acrescentado. Portanto, se você se sentir perdido, explore os artigos sobre interfaces aqui no blog para obter mais informações.
Definida a interface, a unit que conterá tanto esta interface quanto sua aplicação, ficará definida desta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | unit Devspace.Classes.Operation; interface type IOperation = interface ['{BD5131FE-BED6-4E8C-9407-FFE8348F8DC2}'] procedure Calculate(out AResult: Double); function GetA: Double; function GetB: Double; function SetA(const AValue: Double): IOperation; function SetB(const AValue: Double): IOperation; property A: Double read GetA; property B: Double read GetB; end; TOperation = class abstract(TInterfacedObject, IOperation) strict private function GetA: Double; function GetB: Double; strict protected FA: Double; FB: Double; FLastError: string; FResult: Double; FSuccess: Boolean; procedure DoCalculate(out AResult: Double); virtual; abstract; public constructor Create; virtual; procedure Calculate(out AResult: Double); function SetA(const AValue: Double): IOperation; virtual; function SetB(const AValue: Double): IOperation; virtual; end; TOperationClass = class of TOperation; implementation uses System.SysUtils, Devspace.Consts.Operation, Devspace.Exceptions.Operation; { TOperation } constructor TOperation.Create; begin FA := 0; FB := 0; end; procedure TOperation.Calculate(out AResult: Double); begin DoCalculate(AResult); end; function TOperation.GetA: Double; begin Result := FA; end; function TOperation.GetB: Double; begin Result := FB; end; function TOperation.SetA(const AValue: Double): IOperation; begin Result := Self; if FA = AValue then begin Exit; end; FA := AValue; end; function TOperation.SetB(const AValue: Double): IOperation; begin Result := Self; if FB = AValue then begin Exit; end; FB := AValue; end; end. |
Agora, precisaremos instituir uma Abstract Factory e seu Singleton, os quais já vimos no artigo sobre padrões de projeto. A seguir, veremos a definição de sua interface e sua implementação diretamente na unit que as abriga.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | unit Devspace.Classes.Operation.Factory; interface uses System.Generics.Collections, Devspace.Classes.Operation; type TOperations = TArray<string>; IOperationFactory = interface ['{16609C69-1A6D-4365-8203-EB15F8AAF682}'] function GetOperation(const AName: string): IOperation; function GetOperations: TOperations; function RegisterOperation(const AName: string; const AOperation: TOperationClass): IOperationFactory; property Operation[const AName: string]: IOperation read GetOperation; property Operations: TOperations read GetOperations; end; TOperationFactory = class(TInterfacedObject, IOperationFactory) strict private FOperations: TOrderedDictionary<string, TOperationClass>; function GetOperation(const AName: string): IOperation; function GetOperations: TOperations; private class var FInstance: IOperationFactory; constructor Create; public destructor Destroy; override; class function Instance: IOperationFactory; function RegisterOperation(const AName: string; const AOperation: TOperationClass): IOperationFactory; end; implementation { TOperationFactory } constructor TOperationFactory.Create; begin FOperations := TOrderedDictionary<string, TOperationClass>.Create; end; destructor TOperationFactory.Destroy; begin FOperations.Free; inherited; end; class function TOperationFactory.Instance: IOperationFactory; begin if not Assigned(FInstance) then begin FInstance := Create; end; Result := FInstance; end; function TOperationFactory.GetOperation(const AName: string): IOperation; begin Result := nil; if not FOperations.ContainsKey(AName) then begin Exit; end; Result := FOperations[AName].Create; end; function TOperationFactory.GetOperations: TOperations; begin Result := FOperations.Keys.ToArray; end; function TOperationFactory.RegisterOperation(const AName: string; const AOperation: TOperationClass): IOperationFactory; begin Result := Self; FOperations.AddOrSetValue(AName, AOperation); end; initialization TOperationFactory.FInstance := nil; finalization TOperationFactory.FInstance := nil; end. |
Agora, vamos implementar as quatro operações básicas, como de costume.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | unit Devspace.Classes.Operation.Basics; interface uses Devspace.Classes.Operation; type TAddition = class(TOperation) strict protected procedure DoCalculate(out AResult: Double); override; end; TSubtraction = class(TOperation) strict protected procedure DoCalculate(out AResult: Double); override; end; TMultiplication = class(TOperation) strict protected procedure DoCalculate(out AResult: Double); override; end; TDivision = class(TOperation) strict protected procedure DoCalculate(out AResult: Double); override; end; implementation uses System.SysUtils, Devspace.Classes.Operation.Factory, Devspace.Exceptions.Operation; { TAddition } procedure TAddition.DoCalculate(out AResult: Double); begin AResult := FA + FB; end; { TSubtraction } procedure TSubtraction.DoCalculate(out AResult: Double); begin AResult := FA - FB; end; { TMultiplication } procedure TMultiplication.DoCalculate(out AResult: Double); begin AResult := FA * FB; end; { TDivision } procedure TDivision.DoCalculate(out AResult: Double); begin if FB = 0 then begin EDivByZero.RaiseCannotBeZeroException('B'); end; AResult := FA / FB; end; initialization TOperationFactory.Instance .RegisterOperation('Addition', TAddition) .RegisterOperation('Subtraction', TSubtraction) .RegisterOperation('Multiplication', TMultiplication) .RegisterOperation('Division', TDivision); end. |
A parte interessante desta implementação é que, como todas as operações derivam de TOperation, elas podem ser registradas na factory sem qualquer alteração no restante do sistema. Isso representa uma aplicação prática do princípio da substituição de Liskov (LSP), segundo o qual qualquer implementação compatível com o contrato definido por uma interface deve poder substituir outra sem impactar o código consumidor. Dessa forma, a factory pode fornecer uma operação de soma (TAddition), uma operação de subtração (TSubtraction) ou qualquer outra implementação de IOperation, alterando apenas o comportamento da operação executada, mas sem exigir modificações na facade, na própria factory ou na camada de apresentação.
Um ponto importante a ser notado: ao invés de estendermos as classes modificando o método Calculate, criamos um método protegido chamado DoCalculate para esta extensão. Esta é uma questão de boas práticas, uma vez que, se necessário, podemos implementar mecanismos de validação ou qualquer outro processamento que seja necessário sem precisar repeti-lo nas extensões. Assim, apenas a parte que realmente for diferente entre as classes precisará ser escrita sem qualquer necessidade de reimplementar códigos definidos em outras classes.
Implementando as units auxiliares
A unit Devspace.Classes.Operation.Basics.pas usa em sua seção de implementação a unit Devspace.Exceptions.Operation.pas, a qual é definida desta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | unit Devspace.Exceptions.Operation; interface uses System.SysUtils; type EDivByZeroHelper = class helper for EDivByZero public class procedure RaiseCannotBeZeroException(const AProperty: string); end; EOnErrorException = class(Exception) public class procedure RaiseOnErrorCallbackNotAssignedException; end; EOnSuccessException = class(Exception) public class procedure RaiseOnSuccessCallbackNotAssignedException; end; implementation uses Devspace.Consts.Operation; { EDivByZeroHelper } class procedure EDivByZeroHelper.RaiseCannotBeZeroException(const AProperty: string); begin raise EDivByZero.Create(Format(ValueCannotBeZeroException, [AProperty])); end; { EOnErrorException } class procedure EOnErrorException.RaiseOnErrorCallbackNotAssignedException; begin raise EOnErrorException.Create(ErrorCallbackNotAssignedMessage); end; { EOnSuccessException } class procedure EOnSuccessException.RaiseOnSuccessCallbackNotAssignedException; begin raise EOnSuccessException.Create(SuccessCallbackNotAssignedMessage); end; end. |
Ela implementa um class helper, algo que também já vimos anteriormente, para a classe de exceção EDivByZero. Além disso, há a implementação de uma nova classe de exceção, à qual demos o nome de EOnErrorException. Notem que, em ambos os casos, temos um class procedure para usarmos na elevação da exceção. Isso deixa nosso código mais limpo e é mais um dos inúmeros itens a considerar no nosso manual de boas práticas.
E, seguindo nosso manual de boas práticas, esta unit usa a unit Devspace.Consts.Operation.pas para abrigar suas constantes e resource strings. Então, esta unit fica definida desta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 | unit Devspace.Consts.Operation; interface resourcestring ValueCannotBeZeroException = 'Value for %s cannot be zero.'; DefaultExceptionMessage = '[%s] %s'; ErrorCallbackNotAssignedMessage = 'The callback method to treat errors was not assigned.'; SuccessCallbackNotAssignedMessage = 'The callback method to treat success was not assigned.'; implementation end. |
Implementando o Callback Pattern em Delphi
Agora que nossa base foi definida e implementada, vamos explorar o desenvolvimento do Callback Pattern em Delphi. Começaremos pela definição dos tipos e interfaces e, no final, colocaremos as definições e implementações na mesma unit.
Definindo o formato dos métodos de callback
A forma mais simples de definir um método de callback é usarmos a estrutura TProc. Nela, as definições de ponteiros para métodos estão todas estabelecidas e isso nos permite escrever qualquer método para ser usado como callback. Contudo, às vezes precisamos de coisas mais específicas, como parâmetros estritamente definidos para que nosso método de callback possa utilizá-lo em seu processamento.
Então, para resolver essa questão, o Delphi implementa um recurso de referência ao ponteiro, o qual pode ser definido usando a expressão reservada reference to procedure. Vale lembrar que, para callbacks, funções não devem ser utilizadas pois não há como capturar seu retorno. Portanto, quando implementarmos o Callback Pattern em Delphi, utilizaremos apenas procedures como callback.
Sendo assim, definiremos os métodos de callback da seguinte maneira:
1 2 | TErrorCallback = reference to procedure(const AError: string); TSuccessCallback = reference to procedure(const AResult: Double); |
Assim, nosso callback de erro nos trará a mensagem de erro em seu parâmetro e nosso callback de sucesso nos trará o resultado da operação.
Definindo as interfaces usadas pelo Callback Pattern
Uma vez definidos os métodos de callback, definiremos as interfaces que irão compor este recurso, começando pela interface IOperationExecutor, a qual é responsável por prover a chamada do método Calculate.
1 2 3 4 | IOperationExecutor = interface ['{082073C3-3F8E-4C74-A9F8-E66A4207BED6}'] procedure Calculate; end; |
E agora, a definição da interface da nossa facade, a qual será consumida na aplicação para a execução da operação em si.
1 2 3 4 5 6 7 8 | IOperationFacade = interface ['{D135285E-3755-4094-B416-7BCD228E06D0}'] function Command: IOperationExecutor; function OnError(const ACallback: TErrorCallback): IOperationFacade; function OnSuccess(const ACallback: TSuccessCallback): IOperationFacade; function SetA(const AValue: Double): IOperationFacade; function SetB(const AValue: Double): IOperationFacade; end; |
As definições são simples e não implementam nenhuma complexidade. Contudo, é importante notar a separação destas interfaces pois, assim, o componente nos obrigará a setar todas as definições de parâmetros da classe antes de efetuarmos o cálculo. Isso garante que todos os parâmetros e callbacks sejam definidos antes da execução da operação.
Um ponto a ser observado é que a separação das interfaces – IOperationExecutor e IOperationFacade – não é apenas técnica, com a implementação do conceito I (Interface Segregation) do SOLID, mas também prática, uma vez que, estando separadas, a implementação me obrigará a definir totalmente a parametrização do objeto antes de efetuar a operação, garantindo estabilidade ao código e que os mecanismos de tratamento de erro sejam explicitamente definidos antes da execução.
O Callback Pattern em Delphi acontecendo
Definidos os métodos de callback, a interface com a chamada de callback de operação bem sucedida e da nossa facade, vamos juntar tudo e implementar o Callback Pattern em Delphi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | unit Devspace.Classes.Operation.Facade; interface type TErrorCallback = reference to procedure(const AError: string); TSuccessCallback = reference to procedure(const AResult: Double); IOperationExecutor = interface ['{082073C3-3F8E-4C74-A9F8-E66A4207BED6}'] procedure Calculate; end; IOperationFacade = interface ['{D135285E-3755-4094-B416-7BCD228E06D0}'] function Command: IOperationExecutor; function OnError(const ACallback: TErrorCallback): IOperationFacade; function OnSuccess(const ACallback: TSuccessCallback): IOperationFacade; function SetA(const AValue: Double): IOperationFacade; function SetB(const AValue: Double): IOperationFacade; end; TOperationFacade = class(TInterfacedObject, IOperationFacade, IOperationExecutor) strict private FA: Double; FB: Double; FOnError: TErrorCallback; FOnSuccess: TSuccessCallback; FOperation: string; FResult: Double; private constructor Create(const AOperation: string); public class function New(const AOperation: string): IOperationFacade; procedure Calculate; function Command: IOperationExecutor; function OnError(const ACallback: TErrorCallback): IOperationFacade; function OnSuccess(const ACallback: TSuccessCallback): IOperationFacade; function SetA(const AValue: Double): IOperationFacade; function SetB(const AValue: Double): IOperationFacade; end; implementation uses System.SysUtils, Devspace.Classes.Operation.Factory, Devspace.Consts.Operation, Devspace.Exceptions.Operation; { TOperationFacade } constructor TOperationFacade.Create(const AOperation: string); begin FOnError := nil; FOnSuccess := nil; FOperation := AOperation; end; function TOperationFacade.Command: IOperationExecutor; begin Result := Self; end; class function TOperationFacade.New(const AOperation: string): IOperationFacade; begin Result := TOperationFacade.Create(AOperation); end; procedure TOperationFacade.Calculate; begin try TOperationFactory.Instance .Operation[FOperation] .SetA(FA) .SetB(FB) .Calculate(FResult); case Assigned(FOnSuccess) of True : FOnSuccess(FResult); False: EOnSuccessException.RaiseOnSuccessCallbackNotAssignedException; end; except on E: Exception do begin case Assigned(FOnError) of True : FOnError(Format(DefaultExceptionMessage, [E.ClassName, E.Message])); False: raise; end; end; end; end; function TOperationFacade.OnError(const ACallback: TErrorCallback): IOperationFacade; begin Result := Self; if not Assigned(ACallback) then begin EOnErrorException.RaiseOnErrorCallbackNotAssignedException; end; FOnError := ACallback; end; function TOperationFacade.OnSuccess(const ACallback: TSuccessCallback): IOperationFacade; begin Result := Self; if not Assigned(ACallback) then begin EOnSuccessException.RaiseOnSuccessCallbackNotAssignedException; end; FOnSuccess := ACallback; end; function TOperationFacade.SetA(const AValue: Double): IOperationFacade; begin Result := Self; FA := AValue; end; function TOperationFacade.SetB(const AValue: Double): IOperationFacade; begin Result := Self; FB := AValue; end; end. |
Notem três pontos muito importantes nesta implementação:
- O construtor da classe TOperationFacade está em uma seção privada;
- A classe TOperationFacade possui uma class function chamada TOperationFacade.New que retorna uma instância de IOperationFacade; e
- O método TOperationFacade.Command retornará uma referência para IOperationExecutor.
A razão pela qual o construtor da classe foi colocado em uma seção privada está intimamente ligada ao método TOperationFacade.New. Como queremos que a instância da facade seja criada com este método e não pelo construtor, devemos ocultá-lo para chamadas externas à unit, mas permitir que ele seja acessado internamente. Para isso, usamos uma caracteŕistica de escopo do Delphi que permite que seções privadas de uma classe sejam acessadas diretamente em qualquer lugar da unit onde se encontram. Assim, externamente, apenas o método TOperationFacade.New permanecerá visível, com ele podendo acessar o construtor da classe diretamente.
Notem, também, que o método TOperationFacade.Calculate invoca a instância da Abstract Factory para obter a operação desejada, passada por parâmetro no método TOperationFacade.New e armazenada na variável FOperation. Isso faz com que nossa facade “encapsule” uma instância baseada em IOperation, permitindo que a operação seja realizada internamente. Ele ainda é responsável pelo tratamento de qualquer exceção que venha a ser elevada, retirando do desenvolvedor a responsabilidade de fazê-lo na camada de visão (formulário), por exemplo.
Tá, mas, e na prática, como fica?
Como não poderia deixar de ser, vamos gerar uma aplicação prática para exemplificar o consumo de um componente construído com Callback Pattern em Delphi. Ela será a clássica calculadora que implementa operações por meio de um componente TComboBox e exibe o resultado em um componente TLabel.
Sendo assim, a unit do formulário ficará desta forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | unit Devspace.Forms.Calculator; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Mask; type TCalculator = class(TForm) AEdit: TLabeledEdit; BEdit: TLabeledEdit; CalculateButton: TButton; ResultDescLabel: TLabel; ResultLabel: TLabel; OperationComboBox: TComboBox; procedure FormCreate(Sender: TObject); procedure CalculateButtonClick(Sender: TObject); procedure AEditKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); strict private procedure ComboBoxLoadItems; procedure GetOperationAndDoMath; procedure FillEditsIfEmpty; public class procedure Open; end; implementation uses Devspace.Classes.Operation.Factory, Devspace.Classes.Operation.Facade, Devspace.Helpers.TStrings; {$R *.dfm} procedure TCalculator.AEditKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); begin case Key of VK_RETURN: begin Key := 0; Perform(WM_NEXTDLGCTL, 0, 0); end; end; end; procedure TCalculator.CalculateButtonClick(Sender: TObject); begin FillEditsIfEmpty; GetOperationAndDoMath; end; procedure TCalculator.ComboBoxLoadItems; begin OperationComboBox.Items.FromArray(TOperationFactory.Instance.Operations); end; procedure TCalculator.FormCreate(Sender: TObject); begin ComboBoxLoadItems; OperationComboBox.ItemIndex := 0; end; procedure TCalculator.GetOperationAndDoMath; begin TOperationFacade.New(OperationComboBox.Text) .SetA(StrToFloat(AEdit.Text)) .SetB(StrToFloat(BEdit.Text)) .OnError(procedure(const AError: string) begin Application.MessageBox(PWideChar(AError), PWideChar(Application.Title), MB_OK or MB_ICONWARNING); end) .OnSuccess(procedure(const AResult: Double) begin ResultLabel.Caption := FloatToStr(AResult); end) .Command .Calculate; end; class procedure TCalculator.Open; var Instance: TCalculator; begin Application.CreateForm(TCalculator, Instance); end; procedure TCalculator.FillEditsIfEmpty; const DefaultValueA = '0'; DefaultValueB = '1'; begin if AEdit.Text = EmptyStr then begin AEdit.Text := DefaultValueA; end; if BEdit.Text = EmptyStr then begin BEdit.Text := DefaultValueB; end; end; end. |
Esta unit usa, em sua implementação, uma unit chamada Devspace.Helpers.TStrings.pas, a qual implementa um class helper para o tipo TStrings – o que pode ser visto no método TCalculator.ComboBoxLoadItems, na instrução OperationComboBox.Items.FromArray(TOperationFactory.Instance.Operations);.
Esta unit é definida da seguinte maneira:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | unit Devspace.Helpers.TStrings; interface uses System.Classes; type TStringsHelper = class helper for TStrings public function FromArray(const AValues: TArray<string>): TStrings; end; implementation { TStringsHelper } function TStringsHelper.FromArray(const AValues: TArray<string>): TStrings; var Value: string; begin Result := Self; Result.Clear; for Value in AValues do begin Result.Add(Value); end; end; end. |
Usando o Callback Pattern em Delphi para simplificar o código
Vamos observar o código abaixo para entendermos melhor como o Callback Pattern deixa nosso código mais simples e limpo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | procedure TCalculator.GetOperationAndDoMath; begin TOperationFacade.New(OperationComboBox.Text) .SetA(StrToFloat(AEdit.Text)) .SetB(StrToFloat(BEdit.Text)) .OnError(procedure(const AError: string) begin Application.MessageBox(PWideChar(AError), PWideChar(Application.Title), MB_OK or MB_ICONWARNING); end) .OnSuccess(procedure(const AResult: Double) begin ResultLabel.Caption := FloatToStr(AResult); end) .Command .Calculate; end; |
Percebam que, para questões de leitura, este código pode ser lido quase como uma linguagem natural. Quando isso acontece, mesmo que a sintaxe da linguagem aponte para outra direção – uma leitura mais “quebrada” -, forma-se uma “nova” linguagem, específica daquele domínio e não da linguagem de programação utilizada. Esta linguagem de domínio específico, ou DSL, permite que, ao invés de múltiplas linhas de codificação, foquemos apenas no necessário, facilitando a compreensão da rotina como um todo.
Sendo assim, ao ler esta parcela de código, naturalmente compreendemos que:
- Usaremos um componente TOperationFacade;
- O método New gera um novo componente a partir da operação selecionada no componente TComboBox;
- Atribuiremos ao valor A aquilo que estiver contido no componente chamado AEdit, convertendo seu valor para Double;
- O mesmo acontece para o valor B, por meio do componente chamado BEdit;
- O método OnError executará o método de callback em caso de exceção durante o cálculo;
- O método OnSuccess executará o método de callback em caso de operação bem sucedida;
- O método Command retorna a instância de IOperationExecutor; e
- O método Calculate finalmente efetuará o cálculo.
Para o compilador, isto representa apenas uma linha de codificação, com cada linha dos métodos de callback sendo contabilizadas separadamente.
Em contrapartida, caso fizésemos uso do modelo “tradicional” de desenvolvimento, nosso código teria esta aparência:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | procedure TCalculator.GetOperationAndDoMath; var Result: Double; begin try TOperationFactory.Instance .Operation[OperationComboBox.Text] .SetA(StrToFloat(AEdit.Text)) .SetB(StrToFloat(BEdit.Text)) .Calculate(Result); ResultLabel.Caption := FloatToStr(Result); except on E: Exception do begin Application.MessageBox(PWideChar(Format(DefaultExceptionMessage, [E.ClassName, E.Message])), PWideChar(Application.Title), MB_OK or MB_ICONWARNING); end; end; end; |
Não é necessariamente feio, mas muito menos elegante que a versão anterior.
Expandindo o projeto com novas operações
Assim como já vimos, qualquer implementação nova que será adicionada à aplicação deve seguir as regras do princípio L do conceito SOLID – o princípio da substituição de Liskov. As classes a serem implementadas devem seguir à risca a implementação definida pela interface. Ou seja, para criarmos novas operações, elas devem implementar IOperation, sendo implementadas segundo suas diretrizes.
Aqui está um exemplo de como adicionaríamos as operações de potência e radiciação à calculadora.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | unit Devspace.Classes.Operation.Power; interface uses Devspace.Classes.Operation; type TPower = class(TOperation) strict protected procedure DoCalculate(out AResult: Double); override; end; TRoot = class(TOperation) strict protected procedure DoCalculate(out AResult: Double); override; end; implementation uses System.Math, System.SysUtils, Devspace.Classes.Operation.Factory, Devspace.Exceptions.Operation; { TPower } procedure TPower.DoCalculate(out AResult: Double); begin AResult := Power(FA, FB); end; { TRoot } procedure TRoot.DoCalculate(out AResult: Double); begin if FB = 0 then begin EDivByZero.RaiseCannotBeZeroException('B'); end; AResult := Power(FA, 1/FB); end; initialization TOperationFactory.Instance .RegisterOperation('Power', TPower) .RegisterOperation('Root', TRoot); end. |
Tudo isso é lindo, mas posso usar nos meus projetos legados?
Eu sempre digo que o uso de recursos avançados de desenvolvimento depende sempre da maturidade da equipe em assimilar e saber como utilizar estes recursos. Eles não são recursos novos, nem complexos demais para não serem usados e acredito que, com um bom planejamento, aplicações legadas podem ser refatoradas sem muito esforço para modelos mais avançados, como este. Cabe a cada um decidir como e quando. Mas, uma coisa é certa: depois que você começa, não para mais.
E pra fechar…
Espero que todos tenham aproveitado todo o caminho até aqui. Para chegar a este artigo, um longo caminho, tratando de orientação a objetos, padrões de projeto, interfaces e Fluent Interface foi trilhado, mostrando que programar bem requer estudo e construção gradual do conhecimento.
E, como já é tradição por aqui, o código fonte deste artigo pode ser encontrado aqui.
Até a próxima!

Faça um comentário