
Última atualização em 20 de maio 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 e performance 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 via ARC (Contagem de Referências).
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 métodos assíncronos e callbacks para transformar seu código legado em uma obra de arte da engenharia de software.
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 uma única linha.
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 infinitos 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 – ou fluent interfaces. Isso sempre esteve ali, com o Delphi, desde o início, 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 já tínhamos também, desde o começo, eram os callbacks. Mesmo que de uma forma diferente, event notifiers declarados como procedure of object funcionavam como callbacks. O modelo de callback como conhecemos, onde uma parcela de código procedural pode ser passada como parâmetro em um método, 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: o ARC. Ele 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 ARC no Delphi (do inglês, Automatic Reference Counting) 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 Delphi injeta secretamente uma chamada ao método _AddRef, incrementando o contador interno; assim que essa referência sai de escopo – o que acontece imediatamente ao término da execução de uma linha com objetos temporários e anônimos -, 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 instantaneamente, eliminando de vez a necessidade do clássico bloco try…finally e blindando sua aplicação contra os temidos memory leaks.
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 da instância pelo ARC. 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 interfaces 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 FOperacoes: TObjectOrderedDictionary<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 FOperacoes := TObjectOrderedDictionary<string, TOperationClass>.Create; end; destructor TOperationFactory.Destroy; begin FOperacoes.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 FOperacoes.ContainsKey(AName) then begin Exit; end; Result := FOperacoes[AName].Create; end; function TOperationFactory.GetOperations: TOperations; begin Result := FOperacoes.Keys.ToArray; end; function TOperationFactory.RegisterOperation(const AName: string; const AOperation: TOperationClass): IOperationFactory; begin Result := Self; FOperacoes.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. |
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 | 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; 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; 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 | 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.'; 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 IOnSuccess, a qual é responsável por prover a chamada de callback de execução bem sucedida.
1 2 3 4 | IOnSuccess = interface ['{CA28A782-FD8C-4D34-9E85-11F7AE0F17B1}'] procedure OnSuccess(const ACallback: TSuccessCallback); 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 | IOperationFacade = interface ['{D135285E-3755-4094-B416-7BCD228E06D0}'] function Calculate: IOperationFacade; function OnError(const ACallback: TErrorCallback): IOnSuccess; 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 tratar o erro na operação antes do sucesso. Isso garante, até certo ponto logicamente, que o erro não seja suprimido quando ele ocorrer.
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 131 132 133 134 135 | unit Devspace.Classes.Operation.Facade; interface type TErrorCallback = reference to procedure(const AError: string); TSuccessCallback = reference to procedure(const AResult: Double); IOnSuccess = interface ['{CA28A782-FD8C-4D34-9E85-11F7AE0F17B1}'] procedure OnSuccess(const ACallback: TSuccessCallback); end; TOnSuccess = class(TInterfacedObject, IOnSuccess) strict private FResult: Double; FSuccess: Boolean; private constructor Create(const ASuccess: Boolean; const AResult: Double); public procedure OnSuccess(const ACallback: TSuccessCallback); end; IOperationFacade = interface ['{D135285E-3755-4094-B416-7BCD228E06D0}'] function Calculate: IOperationFacade; function OnError(const ACallback: TErrorCallback): IOnSuccess; function SetA(const AValue: Double): IOperationFacade; function SetB(const AValue: Double): IOperationFacade; end; TOperationFacade = class(TInterfacedObject, IOperationFacade) strict private FA: Double; FB: Double; FLastError: string; FOperation: string; FResult: Double; FSuccess: Boolean; private constructor Create(const AOperation: string); public class function New(const AOperation: string): IOperationFacade; function Calculate: IOperationFacade; function OnError(const ACallback: TErrorCallback): IOnSuccess; 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; { TOnSuccess } constructor TOnSuccess.Create(const ASuccess: Boolean; const AResult: Double); begin FResult := AResult; FSuccess := ASuccess; end; procedure TOnSuccess.OnSuccess(const ACallback: TSuccessCallback); begin if not (Assigned(ACallback) and FSuccess) then begin Exit; end; ACallback(FResult); end; { TOperationFacade } constructor TOperationFacade.Create(const AOperation: string); begin FOperation := AOperation; end; class function TOperationFacade.New(const AOperation: string): IOperationFacade; begin Result := TOperationFacade.Create(AOperation); end; function TOperationFacade.Calculate: IOperationFacade; begin Result := Self; FSuccess := False; try TOperationFactory.Instance .Operation[FOperation] .SetA(FA) .SetB(FB) .Calculate(FResult); FSuccess := True; except on E: Exception do begin FLastError := Format(DefaultExceptionMessage, [E.ClassName, E.Message]); end; end; end; function TOperationFacade.OnError(const ACallback: TErrorCallback): IOnSuccess; begin if not Assigned(ACallback) then begin EOnErrorException.RaiseOnErrorCallbackNotAssignedException; end; Result := TOnSuccess.Create(FSuccess, FResult); if not FSuccess then begin ACallback(FLastError); end; 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.OnError não executará suas operações se seu callback não estiver definido apropriadamente.
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.
Atenção: como a facade encapsula e silencia a exceção original para tratá-la via callback, certifique-se de sempre mapear o método OnError no seu código de consumo, evitando que erros críticos passem despercebidos.
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 | 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; end; var Calculator: TCalculator; 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)) .Calculate .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); end; procedure TCalculator.FillEditsIfEmpty; begin if AEdit.Text = EmptyStr then begin AEdit.Text := '0'; end; if BEdit.Text = EmptyStr then begin BEdit.Text := '1'; 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 | procedure TCalculator.GetOperationAndDoMath; begin TOperationFacade.New(OperationComboBox.Text) .SetA(StrToFloat(AEdit.Text)) .SetB(StrToFloat(BEdit.Text)) .Calculate .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); 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 Calculate efetuará o cálculo;
- O método OnError executará o método de callback em caso de exceção durante o cálculo; e
- O método OnSuccess executará o método de callback em caso de operação bem sucedida.
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 derivar de 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 interfaces 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