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:

Padrões de projeto em Delphi

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:

Padrões de projeto em Delphi

Padrões de projeto em Delphi

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.

Padrões de projeto em Delphi

Padrões de projeto em Delphi

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!

One Comment

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *