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:
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.
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.
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.
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.
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.
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.
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.
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:
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.
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 Rio.
Ah, o código fonte deste artigo pode ser baixado aqui, na íntegra.
Até mais!
Engenheiro de software especialista em desenvolvimento de aplicações distribuídas, desenvolvimento de APIs dinâmicas e verticalização de dados. Atuante nas comunidades de software livre e apaixonado por tecnologia. Sócio fundador da LT Digital Labs e defensor do uso da tecnologia como meio de distribuição de conhecimento e melhoria da qualidade de vida.
[…] podemos ver alguns exemplos de orientação a objetos, alguns que já discutimos antes, como Singleton, por exemplo. Mas, mais importante que isso, podemos perceber que, ao unir uma classe a um Class […]