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. 😉
Introdução
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.
Analogia
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.
Estrutura
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.
Interface Colleague
Iniciaremos pela Interface Colleague. São cinco métodos em comum para qualquer membro que for adicionado à aplicação:
1 2 3 4 5 6 7 8 9 |
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; |
Interface Mediator
A próxima etapa é a definição da Interface Mediator, que faz referência à Interface Colleague em dois de seus métodos:
1 2 3 4 5 6 7 8 |
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.
Classe Concrete Mediator
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:
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 |
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:
1 |
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
.
Classe Concrete Colleague
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:
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 |
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).
Em ação!
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.
1 2 3 4 5 6 7 8 9 10 |
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:
1 2 3 4 5 6 7 8 |
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
:
1 2 3 4 5 6 7 8 9 |
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:
1 2 3 4 5 6 7 8 9 10 |
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; |
Confira o resultado na imagem abaixo:
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.
Conclusão
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.
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!
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.
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!