
Última atualização em 9 de agosto de 2022 por Willian Tuttoilmondo
Como implementar padrões de projeto em Delphi de maneira simples e prática
Uma das maiores discussões sobre o uso do Delphi em novos projetos é a capacidade (ou não) da ferramenta implementar novas tecnologias ou padrões vistos em outras linguagens que implementam a orientação a objetos de maneira mais rígida, como Java ou C#. Sim, é possível aplicar padrões de projeto em Delphi de uma maneira bem simples e rápida, diminuindo o custo de manutenção e, principalmente, o tempo de resposta de novas implementações.
Uma fórmula básica – e que funciona muito bem – é a implementação de 3 padrões em conjunto: Facade, Singleton e Abstract Factory. Com este conjunto, é possível resolver 95% dos problemas relacionados ao desenvolvimento de um novo projeto, ampliando as chances de sucesso e longevidade de uma aplicação ou sistema. Não adianta querer implementar padrões de projeto em Delphi para sistemas legados e construídos de maneira errada, pois seria como “colocar remendo de tecido novo em roupa velha”.
Mas, para que possamos explorar o máximo dessa característica do Delphi, é preciso entender: você precisa atualizar seu IDE! Desenvolvedores que ainda estejam presos ao Delphi 7 terão muitas dificuldades para alcançar esse objetivo. Alguns recursos importantíssimos, como Generics, por exemplo, surgiram apenas com o Delphi 2009. Isso sem mencionar que, para o complemento deste artigo, o qual será publicado em outra oportunidade, será necessário utilizar o novo Datasnap, que apareceu no Delphi 2010.
Mas, antes disso, comecemos “pelo começo”.
Entendendo o padrão Facade
Facade nada mais é do que a implementação de uma classe que encapsula outras classes em si para a resolução de um problema. Quando um desenvolvedor Delphi cria um DataModule ou um formulário, ele está, mesmo que não saiba disso, implementando Facade. Dentre os padrões de projeto em Delphi, este é o mais comum de todos.
Para implementarmos este padrão em uma aplicação Delphi, precisamos nos ater ao fato de que, invariavelmente, outros objetos estarão aninhados em um objeto que fará a interface entre o usuário e o algoritmo que resolverá o problema. Esses objetos aninhados nunca poderão ser acessados diretamente, mas isso não significa que não poderemos acessar sua instância através do objeto de Facade. Apesar disso, é fortemente recomendado que apenas o objeto Facade tenha acesso aos objetos aninhados. Um exemplo muito simples pode ser visto aqui:
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 | unit Class.Calculadora; interface type TSoma = class public function Soma(const A, B: Integer): Integer; end; TSubtracao = class public function Subtrai(const A, B: Integer): Integer; end; TMultiplicacao = class public function Multiplica(const A, B: Integer): Integer; end; TDivisao = class public function Divide(const A, B: Integer): Extended; end; TCalculadora = class strict private FSoma: TSoma; FSubtracao: TSubtracao; FMultiplicacao: TMultiplicacao; FDivisao: TDivisao; public constructor Create; destructor Destroy; override; function Soma(const A, B: Integer): Integer; function Subtrai(const A, B: Integer): Integer; function Multiplica(const A, B: Integer): Integer; function Divide(const A, B: Integer): Extended; end; implementation uses System.SysUtils; { TCalculadora } constructor TCalculadora.Create; begin FSoma := TSoma.Create; FSubtracao := TSubtracao.Create; FMultiplicacao := TMultiplicacao.Create; FDivisao := TDivisao.Create; end; destructor TCalculadora.Destroy; begin FreeAndNil(FSoma); FreeAndNil(FSubtracao); FreeAndNil(FMultiplicacao); FreeAndNil(FDivisao); inherited; end; function TCalculadora.Divide(const A, B: Integer): Extended; begin Result := FDivisao.Divide(A, B); end; function TCalculadora.Multiplica(const A, B: Integer): Integer; begin Result := FMultiplicacao.Multiplica(A, B); end; function TCalculadora.Soma(const A, B: Integer): Integer; begin Result := FSoma.Soma(A, B); end; function TCalculadora.Subtrai(const A, B: Integer): Integer; begin Result := FSubtracao.Subtrai(A, B); end; { TSoma } function TSoma.Soma(const A, B: Integer): Integer; begin Result := A + B; end; { TSubtracao } function TSubtracao.Subtrai(const A, B: Integer): Integer; begin Result := A - B; end; { TMultiplicacao } function TMultiplicacao.Multiplica(const A, B: Integer): Integer; begin Result := A * B; end; { TDivisao } function TDivisao.Divide(const A, B: Integer): Extended; begin Result := A / B; end; end. |
Como pode ser visto neste exemplo, da coleção de padrões de projeto em Delphi, o Facade é o mais simples de se implementar. A classe TCalculadora encapsula instâncias das classes TSoma, TSubtracao, TMultiplicacao e TDivisao. Como resultado disso, os métodos Soma, Subtrai, Multiplica e Divide podem ser acessados apenas através de uma instância de TCalculadora. Assim, a interface entre o usuário e os objetos que implementam os cálculos é feita através dessa única instância “visível” a ele, permitindo que o usuário faça uso de um único objeto para resolver algum problema.
Como funciona uma Abstract Factory
Como foi citado lá no início do artigo, para implementar padrões de projeto em Delphi, principalmente uma Abstract Factory, é necessário utilizar uma versão mais atualizada do IDE. Isso se deve ao fato de que é necessário utilizar uma implementação de Generics presente no Delphi à partir da versão 2009. Para isso, utilizaremos uma instância de um TObjectDictionary<TKey,TValue>.
Para facilitar as coisas, as classes TSoma, TSubtracao, TMultiplicacao e TDivisao terão um ancestral comum: a classe TOperacao. Uma das determinações da implementação de uma Abstract Factory em Delphi implica em fazer com que todos os construtores e destrutores sejam os mesmos em todas as classes controladas por ela.
Como queremos facilitar futuras implementações evolutivas de nosso sistema, devemos dividir nossas classes em diversas units. Assim, cada classe estendida de TOperacao poderá efetuar seu registo na Abstract Factory.
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 | unit Classes.Operacao; interface type TOperacao = class abstract strict private FB: Integer; FA: Integer; procedure SetA(const Value: Integer); procedure SetB(const Value: Integer); public constructor Create; virtual; function Efetuar: Integer; virtual; abstract; function EfetuarExt: Extended; virtual; abstract; property A: Integer read FA write SetA; property B: Integer read FB write SetB; end; TOperacaoClass = class of TOperacao; implementation { TOperacao } constructor TOperacao.Create; begin FA := 0; FB := 0; end; procedure TOperacao.SetA(const Value: Integer); begin if FA = Value then begin Exit; end; FA := Value; end; procedure TOperacao.SetB(const Value: Integer); begin if FB = Value then begin Exit; end; FB := Value; end; end. |
Com a classe TOperacao implementada, podemos implementar nossa Abstract Factory.
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 | unit Classes.Factory; interface uses System.Generics.Collections, Classes.Operacao; type TFactory = class strict private FDictionary: TObjectDictionary<string, TOperacaoClass>; public constructor Create; destructor Destroy; procedure RegisterClass(const AKey: string; const AClass: TOperacaoClass); function GetOperacao(const AKey: string): TOperacao; end; implementation uses System.SysUtils; { TFactory } constructor TFactory.Create; begin FDictionary := TObjectDictionary<string, TOperacaoClass>.Create; end; destructor TFactory.Destroy; begin FreeAndNil(FDictionary); end; function TFactory.GetOperacao(const AKey: string): TOperacao; var classOperacao: TOperacaoClass; begin Result := nil; if not (FDictionary.TryGetValue(AKey, classOperacao) and Assigned(classOperacao)) then begin Exit; end; Result := classOperacao.Create; end; procedure TFactory.RegisterClass(const AKey: string; const AClass: TOperacaoClass); begin if FDictionary.ContainsKey(AKey) then begin Exit; end; FDictionary.Add(AKey, AClass); end; end. |
Antes de continuarmos, vamos a alguns esclarecimentos:
- Para facilitar a indexação do nosso dicionário, a chave foi estabelecida como string;
- Como queremos que a factory retorne um objeto concreto, o tipo armazenado no dicionário é um TOperacaoClass;
- Implementamos um método para registro de classes na factory, o que facilitará novas implementações;
- E implementamos um método para obtermos um objeto concreto através de sua chave.
Apesar do conceito parecer complexo, vimos que sua implementação é extremamente simples. Dentre as implementações de padrões de projeto em Delphi, o padrão Abstract Factory é, com certeza, um dos mais úteis em termos de arquitetura.
Eis que surge o Singleton
Outro dos padrões de projeto em Delphi que implementamos sem perceber é o Singleton. Toda vez que implementamos uma instância única de um objeto, como a instância do formulário principal de uma aplicação ou a conexão com o banco de dados, estamos implementando Singleton. Mas, para que nossa aplicação seja realmente eficiente, precisamos fazer com que a classe que abrigará o Singleton gerencie sua própria instância.
No nosso caso, queremos que nossa factory se comporte como Singleton. Para tanto, vamos fazer uma pequena implementação na nossa classe TFactory.
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 | unit Classes.Factory; interface uses System.Generics.Collections, Classes.Operacao; type TFactory = class strict private FDictionary: TObjectDictionary<string, TOperacaoClass>; public constructor Create; destructor Destroy; class function Instance: TFactory; procedure RegisterClass(const AKey: string; const AClass: TOperacaoClass); function GetOperacao(const AKey: string): TOperacao; property Operacoes: TObjectDictionary<string, TOperacaoClass> read FDictionary; end; implementation uses System.SysUtils; var FFactory: TFactory; { TFactory } constructor TFactory.Create; begin FDictionary := TObjectDictionary<string, TOperacaoClass>.Create; end; destructor TFactory.Destroy; begin FreeAndNil(FDictionary); end; function TFactory.GetOperacao(const AKey: string): TOperacao; var classOperacao: TOperacaoClass; begin Result := nil; if not (FDictionary.TryGetValue(AKey, classOperacao) and Assigned(classOperacao)) then begin Exit; end; Result := classOperacao.Create; end; class function TFactory.Instance: TFactory; begin if not Assigned(FFactory) then begin FFactory := TFactory.Create; end; Result := FFactory; end; procedure TFactory.RegisterClass(const AKey: string; const AClass: TOperacaoClass); begin if FDictionary.ContainsKey(AKey) then begin Exit; end; FDictionary.Add(AKey, AClass); end; initialization FFactory := nil; finalization FreeAndNil(FFactory); end |
Para que nossa factory possa controlar sua própria instância, criamos uma variável para abrigá-la e a retornamos através do método estático Instance. Sendo assim, ao invocarmos TFactory.Instance, o método verificará se já há uma instância dela nesta variável. Se não houver, ela será criada pelo próprio método. E, como resultado, este entregará a instância que consta na variável FFactory.
E lá vamos nós…
Como já implementamos nossa factory, podemos implementar nossas classes para as quatro operações básicas.
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 | unit Classes.Soma; interface uses Classes.Operacao; type TSoma = class(TOperacao) public function Efetuar: Integer; override; function EfetuarExt: Extended; override; end; implementation uses Classes.Factory; { TSoma } function TSoma.EfetuarExt: Extended; begin Result := (A + B) * 1.0; end; function TSoma.Efetuar: Integer; begin Result := A + B; end; initialization TFactory.Instance.RegisterClass('soma', TSoma); end. |
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 | unit Classes.Subtracao; interface uses Classes.Operacao; type TSubtracao = class(TOperacao) public function Efetuar: Integer; override; function EfetuarExt: Extended; override; end; implementation uses Classes.Factory; { TSubtracao } function TSubtracao.Efetuar: Integer; begin Result := A - B; end; function TSubtracao.EfetuarExt: Extended; begin Result := (A - B) * 1.0; end; initialization TFactory.Instance.RegisterClass('subtracao', TSubtracao); end. |
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 | unit Classes.Multiplicacao; interface uses Classes.Operacao; type TMultiplicacao = class(TOperacao) public function Efetuar: Integer; override; function EfetuarExt: Extended; override; end; implementation uses Classes.Factory; { TMultiplicacao } function TMultiplicacao.Efetuar: Integer; begin Result := A * B; end; function TMultiplicacao.EfetuarExt: Extended; begin Result := A * B * 1.0; end; initialization TFactory.Instance.RegisterClass('multiplicacao', TMultiplicacao); end. |
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 | unit Classes.Divisao; interface uses Classes.Operacao; type TDivisao = class(TOperacao) public function Efetuar: Integer; override; function EfetuarExt: Extended; override; end; implementation uses Classes.Factory; { TDivisao } function TDivisao.Efetuar: Integer; begin Result := Trunc(A / B); end; function TDivisao.EfetuarExt: Extended; begin Result := A / B; end; initialization TFactory.Instance.RegisterClass('divisao', TDivisao); end. |
Até aqui, nada demais. Mas, claro, para compreender completamente o que está sendo abordado neste artigo, você precisa ter um bom conhecimento sobre orientação a objetos com Delphi. Mas, mesmo assim, vamos aos esclarecimentos:
- A seção initialization de cada unit garante que a própria classe fará seu registro na factory assim que for linkada em tempo de compilação;
- Ao efetuarmos o registro de cada classe na factory, garantiremos que uma instância dela estará disponível a cada unit que invocá-la em sua cláusula uses e;
- Essa mágica toda só é possível se adicionarmos todas as units, tanto da factory quanto das classes, ao projeto.
E, com a implementação destes 3 padrões de projetos em Delphi, seremos capazes de dar uma aplicação prática a eles.
Ah, meu primeiro programa usando padrões de projeto em Delphi
Como todo bom artigo que se preze, aqui também teremos um exemplo prático da utilização dos fontes que já vimos. E, como não poderia deixar de ser, nosso “projeto” será uma “calculadora”! Não vou entrar em detalhes sobre como criar um projeto, formulários, componentes, etc., pois acredito que todos saibam o que estão fazendo a esse respeito. Se você ainda não está familiarizado com isso, sem problemas. Mas recomendo outras leituras antes de você continuar.
Bem, vamos direto ao assunto. Veja abaixo como ficará o formulário principal da nossa aplicação:
Para demonstrar a flexibilidade da implementação destes 3 padrões de projeto em Delphi, o ComboBox de operações será preenchido dinamicamente durante a criação do formulário. Então, essencialmente, nossa aplicação terá apenas 2 métodos no formulário principal: um para preencher o ComboBox e limpar os Edits e outro no botão Efetuar que, como é de se esperar, fará a operação.
E, abaixo, está o código fonte do formulário principal:
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 | unit Form.Calculadora; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TfrmCalculadora = class(TForm) edtA: TEdit; edtB: TEdit; cbxOperacao: TComboBox; lblA: TLabel; lblB: TLabel; lblOperacao: TLabel; lblResultadoIntDesc: TLabel; lblResultadoFlutuanteDesc: TLabel; lblResultadoInteiro: TLabel; lblResultadoFlutuante: TLabel; btnEfetuar: TButton; procedure FormCreate(Sender: TObject); procedure btnEfetuarClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var frmCalculadora: TfrmCalculadora; implementation uses Classes.Factory, Classes.Operacao; {$R *.dfm} procedure TfrmCalculadora.btnEfetuarClick(Sender: TObject); var strOperacao: string; objOperacao: TOperacao; begin strOperacao := cbxOperacao.Text; if strOperacao = EmptyStr then begin ShowMessage('Selecione uma operação!'); Exit; end; objOperacao := TFactory.Instance.GetOperacao(strOperacao); objOperacao.A := StrToIntDef(edtA.Text, 0); objOperacao.B := StrToIntDef(edtB.Text, 0); lblResultadoInteiro.Caption := objOperacao.Efetuar.ToString; lblResultadoFlutuante.Caption := objOperacao.EfetuarExt.ToString; FreeAndNil(objOperacao); end; procedure TfrmCalculadora.FormCreate(Sender: TObject); var bufOperacoes: TArray<string>; i: Integer; begin edtA.Clear; edtB.Clear; bufOperacoes := TFactory.Instance.Operacoes.Keys.ToArray; cbxOperacao.Items.Clear; for i := 0 to Pred(Length(bufOperacoes)) do begin cbxOperacao.Items.Add(bufOperacoes[i]); end; end; end. |
Como se trata de um artigo mais didático, o código fonte do formulário está bem detalhado – incluindo a variável bufOperacoes, desnecessária, pois poderíamos acessar o array de chaves registradas na factory diretamente, mas achei mais elegante demonstrar essa funcionalidade no código.
Abaixo, seguem imagens do nosso programa em operação:
E assim, construímos nosso primeiro programa utilizando padrões de projeto em Delphi.
E como ficam as implementações?
Como gosto de frisar, este modelo de implementação é o ideal para sistemas que trabalham de maneira evolutiva, onde os desenvolvedores podem evoluir a aplicação sem muita intervenção na interface com o usuário. Aplicando estes padrões de projeto em Delphi, você terá uma aplicação incrivelmente simples e com alta capacidade de manutenção, totalmente desacoplada e, claro, que pode se manter por muito mais tempo do que os modelos “tradicionais”.
E, para demonstrar essa maravilha, vamos desenvolver apenas mais uma classe e vamos adicioná-la ao projeto, sem escrever mais uma linha de código sequer no formulário principal.
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 | unit Classes.Potencia; interface uses Classes.Operacao; type TPotencia = class(TOperacao) public function Efetuar: Integer; override; function EfetuarExt: Extended; override; end; implementation uses Classes.Factory, System.Math; { TPotencia } function TPotencia.EfetuarExt: Extended; begin Result := Power(A, B); end; function TPotencia.Efetuar: Integer; begin Result := Trunc(Power(A, B)); end; initialization TFactory.Instance.RegisterClass('potencia', TPotencia); end. |
Ao adicionarmos esta classe ao nosso projeto e recompilarmos a aplicação, ela já poderá ser utilizada, sem qualquer outra implementação.
Finalizando…
Com todas as evoluções ocorridas na linguagem e o no IDE, é impossível pensar em desenvolvimento de aplicações sem pensar na implementação de padrões de projeto em Delphi. Uma das aplicações mais viáveis e, claro, mais interessantes de se fazer é utilizando o Datasnap em APIs restful, onde posso ter apenas um único endpoint para receber minhas solicitações e efetuar minhas operações, facilitando a vida de quem vai desenvolver a aplicação cliente (que pode ser você mesmo).
E, para uma próxima oportunidade, exploraremos esse modelo de desenvolvimento em uma API RESTful utilizando DataSnap no Delphi Sydney.
Ah, o código fonte deste artigo pode ser baixado aqui, na íntegra.
Até mais!
Faça um comentário