[Delphi] Design Patterns – Mediator

[Delphi] Design Patterns - Mediator

Boa noite, pessoal!
Em algumas ocasiões, um intermediário para coordenar as mensagens e interações entre objetos pode parecer uma solução adequada para evitar a forte dependência entre eles. Com o Mediator, essa solução é factível. Veremos, neste artigo, o conceito, propósito e uma aplicação prática deste padrão de projeto, mas, de antemão, já esclareço: o Mediator é bem fácil de compreender. 😉

 

Antes de iniciar o artigo, gostaria de fazer um singelo agradecimento ao Maykon Capellari, um amigo de trabalho com quem discuto e compartilho bastante conhecimento sobre Engenharia de Software. Obrigado, “MK”!

Durante o desenvolvimento de um software, é comum ocorrer situações em que precisamos referenciar algumas classes para consumir seus atributos ou métodos. Porém, à medida que a complexidade da funcionalidade expande, novas classes são referenciadas, criando uma dependência cada vez maior entre elas.
O Mediator consiste em um Design Pattern que provê um mecanismo de encapsulamento das interações que ocorrem entre diferentes objetos. Dessa forma, quando dois objetos precisam interagir, a comunicação é realizada exclusivamente por meio do Mediator. O objetivo dessa abordagem é alcançar um baixo nível de acoplamento na arquitetura do software, já que os objetos passam a não referenciar diretamente outros objetos. Podemos afirmar, portanto, que o Mediator é adequado para cenários em que muitas classes referenciam muitas classes. Quando isso ocorre, uma simples alteração pode gerar um impacto significativo no software, uma vez que o acoplamento é elevado.

Considere, como analogia, o funcionamento do aplicativo WhatsApp. Quando enviamos uma mensagem, o aplicativo age como intercessor para entregá-la ao destinatário desejado. Não precisamos estar próximos do destinatário e nem sequer tê-lo adicionado como contato.
Outra analogia bastante interessante, que encontrei no blog do Luís Gustavo Fabbro, é um controle de tráfego aéreo. As aeronaves que circulam o aeroporto não “se conhecem”, portanto, não conseguem comunicar suas intenções de decolagem e aterrissagem para evitar acidentes. Essa administração é realizada pela torre de controle que, como possui comunicação com todas as aeronaves, atua como Mediator.
Um exemplo clássico e mais técnico seria o formulário de uma aplicação Delphi. Observe que podemos adicionar diferentes componentes no formulário, mas cada um deles não tem conhecimento da existência do outro. Mesmo assim, podemos solicitar que eles se comuniquem, por exemplo, atribuindo o Text de um TEdit para o Caption de um TLabel em um evento do formulário que, neste caso, recebe o papel de Mediator.

 

A estrutura deste padrão de projeto é de fácil compreensão, composta por apenas quatro elementos:

  • Mediator: Interface que define os métodos de comunicação entre os objetos;
  • Concrete Mediator: Classe concreta que implementa a Interface Mediator;
  • Colleague: Interface que define os métodos referente às “intenções” dos objetos;
  • Concrete Colleague: Classe concreta que implementa a Interface Colleage.

Em uma tradução livre, podemos dizer que temos “mediadores” e “colegas”. A propósito, nunca comentei no blog, mas os elementos de cada Design Pattern geralmente representam essa combinação: Interface + Classe Concreta. Lembre-se que essa estrutura está presente na maioria dos artigos dessa série. 🙂

 

Pois bem, já que mencionamos o WhatsApp como um “provedor de serviços de mensagens”, o nosso exemplo prático seguirá a mesma ideia. Codificaremos uma aplicação de compra e venda, em que membros podem se registrar e enviar propostas para outros membros sem que seja necessário conhecê-los.

Iniciaremos pela Interface Colleague. São cinco métodos em comum para qualquer membro que for adicionado à aplicação:

type
  { Colleague }
  IColleague = interface
    function EnviarProposta(const Destinatario, Proposta: string): string;
    function ReceberProposta(const Remetente, Proposta: string): string;
    function ObterNome: string;
    function Entrar: string;
    function Sair: string;
  end;

A próxima etapa é a definição da Interface Mediator, que faz referência à Interface Colleague em dois de seus métodos:

type
  { Mediator }
  IMediator = interface
    function AdicionarMembro(Membro: IColleague): string;
    function RemoverMembro(const Nome: string): string;
    function EnviarProposta(const Remetente, Destinatario, Proposta: string): string;
    function ObterMembro(const Nome: string): IColleague;
  end;

 

Antes de continuar, vale justificar que, como as ações executadas pelos métodos serão exibidas em um controle visual (como um TMemo), decidi assiná-los como functions para retornarem uma string.
A partir de agora, codificaremos a implementação concreta das Interfaces. Iniciaremos pelo Concrete Mediator, que será responsável por toda a coordenação das interações. Para isso, faremos uso do recurso da classe TDictionary do Delphi, que representa uma coleção de pares na estrutura de chave-valor. Salvo engano, essa classe está disponível desde a versão XE4 do produto.
Para auxiliar na compreensão, procurei adicionar um comentário nos principais métodos:

type
  { Concrete Mediator }
  TConcreteMediator = class(TInterfacedObject, IMediator)
  private
    // Variável para armazenar a lista de membros
    FListaMembros: TDictionary<string, IColleague>;
  public
    constructor Create;
 
    // Adiciona o objeto no dicionário
    function AdicionarMembro(Membro: IColleague): string;
 
    // Remove o objeto do dicionário
    function RemoverMembro(const Nome: string): string;
 
    // Envia a proposta do remetente para o destino
    function EnviarProposta(const Remetente, Destinatario, Proposta: string): string;
 
    // Busca a referência do membro através do nome, que é chave do par no dicionário
    function ObterMembro(const Nome: string): IColleague;
  end;
 
implementation
 
uses
  SysUtils;
 
{ TConcreteMediator }
 
constructor TConcreteMediator.Create;
begin
  // Cria o dicionário
  FListaMembros := TDictionary<string, IColleague>.Create;
end;
 
function TConcreteMediator.EnviarProposta(const Remetente, Destinatario, Proposta: string): string;
var
  MembroRemetente: IColleague;
  MembroDestinatario: IColleague;
begin
  // Encontra o remetente no dicionário
  MembroRemetente := FListaMembros.Items[Remetente];
 
  // Encontra o destinatário no dicionário
  MembroDestinatario := FListaMembros.Items[Destinatario];
 
  // Executa o método de recebimento da proposta no destinatário
  result := MembroDestinatario.ReceberProposta(MembroRemetente.ObterNome, Proposta);
end;
 
function TConcreteMediator.AdicionarMembro(Membro: IColleague): string;
begin
  // Adiciona o membro no dicionário
  FListaMembros.Add(Membro.ObterNome, Membro);
 
  result := Format('Usuário "%s" entrou.', [Membro.ObterNome]);
end;
 
function TConcreteMediator.RemoverMembro(const Nome: string): string;
begin
  // Remove o membro do dicionário
  FListaMembros.Remove(Nome);
 
  result := Format('Usuário "%s" saiu.', [Nome]);
end;
 
function TConcreteMediator.ObterMembro(const Nome: string): IColleague;
begin
  // Obtém uma referência ao objeto pelo nome (utilizado posteriormente pelo Client)
  result := FListaMembros.Items[Nome];
end;

Os métodos são bem simples e demonstram claramente a manipulação do dicionário. Mesmo assim, gostaria de enfatizar a forma como o dicionário foi declarado:

FListaMembros: TDictionary<string, IColleague>;

Com essa declaração, é possível armazenar vários objetos que implementam a Interface Colleague (como uma lista de objetos), dado que o identificador (chave) de cada posição é uma string que será preenchida com o nome do membro. Logo, podemos encontrar o objeto pelo nome do membro acessando a propriedade Items.

 

A última etapa para finalizar a codificação do padrão de projeto é construir o Concrete Colleague. Mais uma vez, como apoio, também adicionei alguns comentários:

type
  { Concrete Colleague }
  TConcreteColleague = class(TInterfacedObject, IColleague)
  private
    FNome: string;
 
    // Variável para armazenar uma referência ao Mediator
    FMediator: IMediator;
  public
    constructor Create(const Nome: string; Mediator: IMediator);
 
    // Chama o Mediator para enviar a proposta ao destinatário
    function EnviarProposta(const Destinatario, Proposta: string): string;
 
    // Retorna uma mensagem de recebimento da proposta
    function ReceberProposta(const Remetente, Proposta: string): string;
 
    // Obtém o nome do membro
    function ObterNome: string;
 
    // Chama o Mediator para adicionar o usuário no dicionário
    function Entrar: string;
 
    // Chama o Mediator para remover o usuário do dicionário
    function Sair: string;
  end;
 
implementation
 
uses
  SysUtils;
 
{ TConcreteColleague }
 
constructor TConcreteColleague.Create(const Nome: string; Mediator: IMediator);
begin
  FNome := Nome;
  FMediator := Mediator;
end;
 
function TConcreteColleague.ObterNome: string;
begin
  result := FNome;
end;
 
function TConcreteColleague.Entrar: string;
begin
  // Adiciona o usuário no dicionário
  result := FMediator.AdicionarMembro(Self);
end;
 
function TConcreteColleague.Sair: string;
begin
  // Remove o usuário do dicionário
  result := FMediator.RemoverMembro(Self.ObterNome);
end;
 
function TConcreteColleague.EnviarProposta(const Destinatario, Proposta: string): string;
begin
  // Envia a proposta através do Mediator
  result := FMediator.EnviarProposta(Self.ObterNome, Destinatario, Proposta);
end;
 
function TConcreteColleague.ReceberProposta(const Remetente, Proposta: string): string;
begin
  // Retorna uma mensagem indicando o recebimento da proposta
  result := Format('De [%s] para [%s]: %s', [Remetente, Self.ObterNome, Proposta]);
end;

Nada de muito especial. Recebemos a instância do Mediator no construtor e o utilizamos para executar cada intenção do membro (entrar, sair e enviar propostas).

 

Para avaliar todo o funcionamento do padrão de projeto, utilizaremos um formulário como Client com um componente TMemo para exibir as ações. Em primeiro lugar, o Mediator deve ser criado na inicialização e permanecer instanciado durante a execução.

private
  FMediator: IMediator;
 
...
 
procedure TForm1.FormCreate(Sender: TObject);
begin
  // Cria a instância do Mediator
  FMediator := TConcreteMediator.Create;
end;

Em seguida, podemos adicionar membros com a código abaixo, substituindo o texto fixo por um componente de entrada de dados:

var
  Membro: IColleague;
begin
  Membro := TConcreteColleague.Create('André Celestino', FMediator);
 
  // Executa o método de registro para adicionar o membro ao dicionário
  Memo1.Lines.Add(Membro.Entrar);
end;

Para remover o membro, buscamos a sua referência no dicionário para chamar o método “Sair”:

var
  Membro: IColleague;
begin
  // Obtém a referência do objeto pelo nome no dicionário
  Membro := FMediator.ObterMembro('André Celestino');
 
  // Executa o método de remoção do membro
  Memo1.Lines.Add(Membro.Sair);
end;

Por fim, a ação principal, que é o envio de propostas, recebe uma codificação bem sucinta, na qual indicamos o destinatário e a proposta como parâmetros:

var
  Remetente: IColleague;
begin
  // Obtém a referência do remetente pelo nome no dicionário
  Remetente := FMediator.ObterMembro('André Celestino');
 
  // Executa o método de envio de proposta, que será gerenciado pelo Mediator
  Memo1.Lines.Add(Remetente.EnviarProposta('Beatriz Mayumi',
    'Gostei do seu notebook. O que acha de vendê-lo por R$ 1.900,00?'));
end;

 

André, mesmo detalhando as codificações, não consigo visualizar o funcionamento…
Sem problemas! Confira o resultado na imagem abaixo:

Exemplo de aplicação utilizando o Design Pattern Mediator

 

Neste projeto de exemplo, por ser didático, o Mediator está no próprio formulário. Em um ambiente real, essa classe pode residir em um local remoto, como um servidor de aplicação, por exemplo.
A codificação apresentada neste artigo está disponível no GitHub, com alguns aperfeiçoamentos. Além de usuários, criei também um Concrete Colleague para representar administradores e adicionei um novo método no Mediator para liberar o dicionário da memória ao fechar a aplicação.

https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/Mediator-AndreCelestino

 

Para finalizar, deixo aqui algumas boas observações sobre a aplicação do Mediator neste projeto:

  • A única dependência da classe de membros é com o Mediator;
  • Os membros (Concrete Colleagues) não se conhecem, portanto, não se referenciam diretamente;
  • A inclusão de novos Concrete Colleagues não impacta na arquitetura existente;
  • A unit do Mediator não faz referência aos Concrete Colleagues. Apenas Interfaces.

Além disso, claro, vale destacar novamente o objetivo do Mediator: investir no baixo acoplamento. Um projeto com essa característica está automaticamente submetido à fácil manutenção.

 

Fico por aqui, mas volto em breve!
Abraços!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

2 comentários

  1. Parabéns, André! Excelente artigo! Com certeza esse é um dos maiores desafios para quem desenvolve: manter o baixo acoplamento entre as classes. O uso de design patterns é sempre bem-vindo quando aplicado corretamente e a sua explicação foi muito clara e detalhada. Continue com seu ótimo trabalho, a comunidade de desenvolvedores Delphi agradece.

    1. Concordo plenamente, Cleo!
      O estudo e planejamento da arquitetura tornou-se uma atividade de grande relevância em projetos de software. Existe uma série de técnicas para alcançar o baixo acoplamento e a alta coesão, e pretendo abordá-las aos poucos aqui no blog.
      Muito obrigado pelas palavras, Cleo. São comentários como o seu que me mantém motivado para continuar este trabalho.
      Grande abraço!

Deixe uma resposta

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

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.