Olá, pessoal, tudo bem?
Bom, primeiro, preciso justificar a minha demora. Recentemente, alguns projetos paralelos tomaram um pouco do meu tempo, mas, em contrapartida, vocês ouvirão novidades em breve! 😉
Prosseguindo com a nossa série sobre Design Patterns, neste artigo discutiremos sobre o Composite, um padrão de projeto muito útil para executar operações em um conjunto de objetos de forma única.
Introdução
Na ementa da disciplina de Estrutura de Dados nos cursos superiores de TI, ouvimos falar muito em árvores. O conceito, em poucas palavras, significa que uma classe pode ser composta por uma ou várias classes da mesma famÃlia, formando uma estrutura de árvore ou uma composição. Imagine, por exemplo, uma linha de produção de móveis que é composta por várias máquinas. Cada máquina executa sua própria operação na peça do móvel (corte, lixa, pintura, acabamento, etc), como se, em programação, o seguinte método fosse chamado:
1 |
Maquina.ExecutarTrabalho; |
Porém, cada máquina individual é parte de uma única máquina principal, responsável por mover a peça na esteira. À medida que a peça se move, cada máquina faz o seu trabalho especÃfico e a peça segue adiante para o próximo procedimento. Usando o mesmo raciocÃnio do código acima, este cenário poderia ser representado dessa forma:
1 2 3 4 5 6 7 8 9 10 |
var Maquina: TMaquina; begin // faz um loop em todas as máquinas que estão em uma "lista" for Maquina in ListaMaquinas do begin // executa a operação da máquina atual Maquina.ExecutarTrabalho; end; end; |
Esse é o propósito chave do Composite. Podemos executar ações tanto no objeto “todo” (que chamará a ação de cada objeto contido na composição), quanto no objeto “parte”. O cliente que chamará essas ações não deve saber com qual está trabalhando, já que ambos devem se comportar da mesma forma.
Motivação do Composite
Na analogia acima, da indústria de móveis, considere que uma das peças teve uma falha de pintura. Neste caso, executarÃamos o trabalho apenas da máquina referente à essa etapa, e não do processo como um todo. Veja, então, que ExecutarTrabalho
pode ser uma ação de uma máquina individual, como também pode ser uma ação da máquina principal.
Podemos citar também uma analogia de ferramentas (objeto “parte”) e caixa de ferramentas (objeto “todo”). Para algumas operações, precisamos apenas de uma ferramenta especÃfica, como uma chave de fenda. Em outras ocasiões, como o conserto de um veÃculo, é necessário utilizar um conjunto, ou melhor, uma composição de ferramentas.
Pois bem, para empregarmos o padrão Composite em um projeto, é necessário quatro elementos: Component, Operation, Composite e Leaf.
O Component representa uma Interface que será implementada pelas classes relacionadas à composição, ou seja, pela classe principal e suas “ramificações”. Além disso, essa Interface também deverá conter um método que será comum entre esse objetos, chamado Operation. No exemplo acima, a Interface e o Operation poderiam ser IMaquina
e ExecutarTrabalho
, respectivamente.
O Composite é a classe concreta de composição que possui uma lista de objetos (ou filhos), comportando-se como uma árvore. O Leaf são as classes filhas, ou as “folhas” da árvore, que estarão atreladas à uma composição.
Sei que, a princÃpio, todos esses termos parecem complexos, portanto, vou tentar encaixá-los na nossa analogia da indústria de móveis:
- Component: IMaquina (representa uma abstração das máquinas);
- Operation: ExecutarTrabalho (operação que tanto a máquina principal quanto cada uma de suas “composições” pode executar);
- Composite: TMaquinaPrincipal (máquina que leva as peças em uma esteira para as outras máquinas);
- Leaf: TMaquina (máquinas que realizam trabalhos especÃficos nas peças).
Dessa forma, ao chamarmos o método ExecutarTrabalho
do Composite (máquina principal), este mesmo método será chamado para cada um dos filhos (máquina individual), resultando em um processo completo de fabricação da peça de um móvel. Interessante, não?
Exemplo de codificação do Composite
Bom, leitores, para a nossa aplicação prática, pensei em uma agência de turismo.
Pensei nesse segmento em função do contexto de viagens e pacotes de viagens. Se você já conseguiu identificar que as viagens serão as classes Leaf e o pacote de viagem será a classe Composite, parabéns! 🙂
A ideia é simples: um cliente pode realizar uma única viagem como também pode optar por montar um pacote de viagens que, obviamente, é composto por várias viagens individuais. Sendo assim, podemos considerar que, para qualquer um dos dois, haverá uma consulta de valor, certo? Opa, então esse será o nosso Operation!
Interface Component e método Operation
Para iniciar, é necessário declarar o Component (Interface) e também o Operation (método):
1 2 3 4 5 6 7 |
type { Component } IViagem = interface { Operation } function CalcularValor: double; end; |
Classe Leaf
A classe de viagem será o nosso Leaf…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type { Leaf } TViagem = class(TInterfacedObject, IViagem) private Origem: string; Destino: string; Data: TDateTime; public procedure DefinirOrigem(const Cidade: string); procedure DefinirDestino(const Cidade: string); procedure DefinirData(const Data: TDateTime); { Operation } function CalcularValor: double; end; |
… que terá as seguintes implementações:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
procedure TViagem.DefinirDestino(const Cidade: string); begin Self.Destino := Cidade; end; procedure TViagem.DefinirOrigem(const Cidade: string); begin Self.Origem := Cidade; end; procedure TViagem.DefinirData(const Data: TDateTime); begin Self.Data := Data; end; function TViagem.CalcularValor: double; begin result := ConsultarValorViagem(Origem, Destino, Data); end; |
No código acima, existe uma chamada ao método ConsultarValorViagem
que não será abordado do artigo, já que foge do nosso escopo. A responsabilidade deste método é buscar o valor da viagem em um banco de dados ou até mesmo em um WebService, com base na cidade de origem, destino e data da viagem informados como parâmetros.
Classe Composite
A próxima etapa é implementar a classe do Composite:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type { Composite } TPacoteViagem = class(TInterfacedObject, IViagem) private // lista de objetos para armazenar as viagens (Leaf) do pacote Viagens: TObjectList<TViagem>; public constructor Create; { Operation } function CalcularValor: double; procedure AdicionarViagem(Viagem: TViagem); end; |
Com exceção do construtor, observe que há um método diferente, chamado AdicionarViagem
, responsável por incluir um objeto do tipo TViagem
(Leaf) na lista de objetos Viagens
:
1 2 3 4 |
procedure TPacoteViagem.AdicionarViagem(Viagem: TViagem); begin Viagens.Add(Viagem); end; |
Confira agora, com detalhes, como é a implementação do Operation da classe de pacote de viagens:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function TPacoteViagem.CalcularValor: double; var Viagem: TViagem; begin // Este é o método principal (Operation) que dá propósito ao padrão Composite. // O método irá ler cada uma das viagens dentro do pacote, // ou seja, cada Leaf dentro do Composite, // para calcular o valor de cada viagem, e por fim, obter o valor total do pacote result := 0; for Viagem in Viagens do begin // Chama o Operation do Leaf result := result + Viagem.CalcularValor; end; end; |
Agora o conceito ficou bem mais claro, não é?
Em ação!
Para apresentar, na prática, a vantagem deste padrão de projeto, vamos considerar 2 cenários:
1) O cliente da agência de turismo decide realizar uma única viagem.
Não precisamos do Composite. Apenas a chamada do Operation do Leaf já seria o suficiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var Viagem: TViagem; begin Viagem := TViagem.Create; try Viagem.DefinirOrigem('São Paulo'); Viagem.DefinirDestino('Rio de Janeiro'); Viagem.DefinirData('07/11/2016'); // chama o Operation ShowMessage('Total: ' + FloatToStr(Viagem.CalcularValor)); finally FreeAndNil(Viagem); end; end; |
2) O cliente da agência de turismo decide comprar um pacote de viagens.
Neste caso, utilizamos o Composite para formar a “composição” de viagens:
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 |
var Pacote: TPacoteViagem; Viagem1: TViagem; Viagem2: TViagem; begin Pacote := TPacoteViagem.Create; try // primeira viagem do pacote Viagem1 := TViagem.Create; Viagem1.DefinirOrigem('São Paulo'); Viagem1.DefinirDestino('Rio de Janeiro'); Viagem1.DefinirData('07/11/2016'); // segunda viagem do pacote Viagem2 := TViagem.Create; Viagem2.DefinirOrigem('Rio de Janeiro'); Viagem2.DefinirDestino('Curitiba'); Viagem2.DefinirData('10/11/2016'); Pacote.AdicionarViagem(Viagem1); Pacote.AdicionarViagem(Viagem2); // O método irá ler cada uma das viagens dentro do pacote, // ou seja, cada Leaf dentro do Composite, // para calcular o valor de cada viagem, e por fim, obter o valor total do pacote ShowMessage('Total: ' + FloatToStr(Pacote.CalcularValor)); finally FreeAndNil(Pacote); end; end; |
Feito!
Conclusão
Este Design Pattern deve ser utilizado quando o Client (funcionalidade que consumirá o padrão Composite, como uma tela) deve trabalhar com um ou mais métodos (Operations) que sejam comuns entre classes que atuam de forma individual e classes que são compostas de outras classes. Em outras palavras, o Client não deve saber distinguir, por exemplo, se está trabalhando com um ou outro. Deve apenas chamar o Operation e aguardar o resultado. No caso do nosso exemplo prático, o Client aciona o método CalcularValor
, independente se é uma viagem ou um pacote de viagens.
Se você ainda estiver com algumas dúvidas sobre o funcionamento do Composite, baixe o projeto de exemplo no link abaixo e execute-o no seu Delphi. Neste projeto, codifiquei uma classe Singleton para armazenar e buscar os valores das viagens a partir de um arquivo XML.
Fico por aqui, meus caros!
Abraço e até o próximo artigo!
Como ficaria se o calculo fosse mais complexo… alem de somar ou subtrair…mas envolvendo multiplicação divisão ?
Boa noite, Lindemberg!
No exemplo deste artigo, todo o cálculo ocorre no método
CalcularValor
(Operation), portanto, se fossem necessárias outras operações matemáticas, elas seriam centralizadas neste método.Em casos mais complexos, pode-se também criar novos métodos para compor os cálculos, desde que sejam chamados no Operation. O importante é que a mesma operação seja realizada tanto para o Leaf quanto para o Composite!
Abraço!
Parabéns pelo excelente trabalho !!
Obrigado, Carlos! 🙂