[Delphi] Design Patterns GRASP – Creator

[Delphi] Design Patterns GRASP - Creator

Olá, pessoal, como vão?
Sabemos que, em uma arquitetura orientada a objetos, a criação (ou instanciação) de objetos é uma das atividades mais comuns, além de ser bastante frequente. Porém, embora seja tão trivial, muitas vezes criamos estes objetos em classes erradas e não sabemos!
O propósito do Design Pattern Creator é nos ajudar a identificar as classes devidamente responsáveis pela criação de cada objeto. Acompanhe!

 

Não há segredo em criar objetos. Basta declarar, instanciar e usá-lo, não é? Oras, pensando assim, por que existiria um padrão de projeto dedicado exclusivamente à este conceito?
Para responder à essa pergunta, considere o pequeno código a seguir:

uses
  Classe;
 
var
  Objeto: TClasse;
begin
  Objeto := TClasse.Create;
  try
    Objeto.Executar;
  finally
    Objeto.Free;
  end;
end;

 

Não há nada incorreto no código acima. No entanto, o meu objetivo é destacar uma linha do código que provavelmente passou despercebida: a seção uses.
Para que você possa criar um objeto de um determinado tipo, é necessário incluir a referência nessa seção, ou seja, a unit na qual a classe está declarada. A simples inclusão de uma referência pode parecer “inofensiva”, mas, caso essa referência seja utilizada em abundância ou em diferentes módulos (pacotes, projetos, bibliotecas, etc.), a arquitetura pode ficar comprometida em função do alto acoplamento.
Há três motivos que atestam essa afirmação:

  • Quando uma unit que é utilizada em vários locais do projeto sofre uma alteração, ocorre um efeito conhecido como Dependência Magnética (que já comentei no artigo sobre DRY). Em suma, esse efeito consiste no impacto que será causado em todas as classes que utilizam essa unit e que, por consequência, terão que ser recompiladas;
  • Quando se tem uma única unit que é referenciada em diversos pontos do código, provavelmente há um erro de abstração;
  • A inclusão demasiada de units na seção uses prejudica a performance do projeto, já que produz problemas de referência circular (já tive experiência com isso!).

Creator

Para evitar todos estes problemas, portanto, podemos empregar as diretrizes recomendadas pelo Design Pattern Creator para identificar os responsáveis ideais pela criação de objetos. Desse modo, o acoplamento da arquitetura é reduzido e haverá menos instanciações arbitrárias, além de uma arquitetura mais previsível. A intenção é deixar claro que a criação de objetos não deve ocorrer em qualquer classe. Deve existir um motivo.
Observe que, diferente dos outros padrões de projeto, o Creator traz orientações, e não uma solução de arquitetura.
Para o Creator, uma classe X só pode criar objetos da classe Y caso um (ou mais) dos requisitos abaixo seja atendido.

Show me the Code!

1) Classe X compõe ou agrega instâncias da classe Y.
Por exemplo, uma classe de pedidos (X) que agrega instâncias dos itens do pedido (Y):

type
  TItemPedido = class
  private
    FCodProduto: integer;
    FQuantidade: integer;
  public
    property CodProduto: integer read FCodProduto write FCodProduto;
    property Quantidade: integer read FQuantidade write FQuantidade;
  end;
 
  TPedido = class
  private
    FItensPedido: TObjectList<TItemPedido>;
  public
    procedure AdicionarItem(const ACodProduto, AQuantidade: integer);
  end;
 
{ TPedido }
 
procedure TPedido.AdicionarItem(const ACodProduto, AQuantidade: integer);
var
  LItemPedido: TItemPedido;
begin
  LItemPedido := TItemPedido.Create;
  LItemPedido.CodProduto := ACodProduto;
  LItemPedido.Quantidade := AQuantidade;
 
  FitensPedido.Add(LItemPedido);
end;

 

2) Classe X registra instâncias da classe Y.
“Registrar”, neste contexto, se refere à persistência da instância (em um banco de dados, por exemplo), ou equivale simplesmente a “manter” uma instância em memória. No exemplo abaixo, usando o mesmo cenário do requisito anterior, a classe de pedidos (X) registra (mantém) uma instância da classe de clientes (Y).

type
  TCliente = class
  public
    constructor Create(const ACodigo: integer);
    function GetEnderecoEntrega: string;
  end;
 
  TPedido = class
  private
    FCliente: TCliente;
  public
    procedure ConsultarCliente(const ACodCliente: integer);
    function GetEnderecoEntrega: string;
  end;
 
implementation  
 
{ TPedido }
 
procedure TPedido.ConsultarCliente(const ACodCliente: integer);
begin
  // Cria uma instância da classe TCliente correspondente ao código do parâmetro
  FCliente := TCliente.Create(ACodCliente);
end;  
 
procedure TPedido.GetEnderecoEntrega(const ACodCliente: integer);
begin
  // Delega a chamada para a instância da classe TCliente
  result := FCliente.GetEnderecoEntrega;
end;

Dessa forma, é possível obter o endereço de entrega do cliente com essa instrução:

LEnderecoEntrega := LPedido.GetEnderecoEntrega;

Observe que também poderíamos obter o endereço de entrega como LPedido.Cliente.GetEnderecoEntrega, mas, para isso, teríamos que expor uma propriedade para acessar o objeto FCliente, prejudicando o encapsulamento.

 

3) Classe X é “íntima” das instâncias da classe Y.
Em outras palavras, a classe X deve ser “próxima” das instâncias da classe Y, ou seja, fazer parte do mesmo contexto. Na minha opinião, este é o requisito mais relevante, mas, ao mesmo tempo, é também o mais negligenciado.
No exemplo a seguir, a classe de transferência de arquivos (X) cria instâncias da classe TIdFTP (Y), já que estão intimamente vinculadas na perspectiva de contexto.

type
  TTransferenciaArquivos = class
  public
    procedure TransferirArquivo(const AArquivo: string);
  end;
 
implementation
 
{ TTransferenciaArquivos }
 
procedure TTransferenciaArquivos.TransferirArquivo(const AArquivo: string);
var
  IdFTP: TIdFTP;
begin
  IdFTP := TIdFTP.Create(nil);
  try
    { ... }
 
    IdFTP.Put(AArquivo);
  finally
    IdFTP.Free;
  end;
end;

 

4) Classe X possui dados de inicialização da instância da classe Y.
Se a classe X possui a maior parte ou todos os dados necessários para instanciar um objeto da classe Y, então a responsabilidade está correta.
No código abaixo, a classe de contas a receber (X) preenche os atributos da classe de correção monetária (Y) pelo construtor antes de executar o método de cálculo:

procedure TContasReceber.CalcularCorrecaoMonetaria;
var
  CorrecaoMonetaria: TCorrecaoMonetaria;
begin
  try
    CorrecaoMonetaria := TCorrecaoMonetaria.Create
      (FDataVencimento, Date, FValor, (FPercentualJuros / 100));
 
    CorrecaoMonetaria.ExecutarCalculo;
  finally
    CorrecaoMonetaria.Free;
  end;
end;

Outra forma de preencher os atributos é através de propriedades:

procedure TContasReceber.CalcularCorrecaoMonetaria;
var
  CorrecaoMonetaria: TCorrecaoMonetaria;
begin
  try
    CorrecaoMonetaria := TCorrecaoMonetaria.Create;
 
    CorrecaoMonetaria.DataVencimento := FDataVencimento;
    CorrecaoMonetaria.DataCalculo := FDataCalculo;
    CorrecaoMonetaria.Valor := FValor;
    CorrecaoMonetaria.PercentualJuros := FPercentualJuros;
 
    CorrecaoMonetaria.ExecutarCalculo;
  finally
    CorrecaoMonetaria.Free;
  end;
end;

 

That’s It!

Leitores, caso nenhum dos requisitos acima seja atendido, recomendo a revisão da arquitetura. Existe uma grande possibilidade de uma classe estar criando objetos indevidamente. Neste caso, novas classes intermediárias devem ser criadas; algumas responsabilidades devem ser extraídas para classes específicas; ou a criação de objetos deve ser movida para métodos compartilhados.
Uma pequena exceção destes requisitos é o Design Pattern Factory, do GOF, uma vez que a sua responsabilidade é justamente instanciar objetos.
Na verdade, eu gostaria de detalhar um pouco mais sobre cada um destes requisitos, mas o artigo ficaria muito extenso. 🙁

Espero que tenham gostado, pessoal.
Até breve!


 

Deixe uma resposta

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