Desenvolvimento

28 dez, 2018

Arquitetura e desenvolvimento de software: Parte 8 – Composite

Publicidade

E aí, pessoal! Tudo bem?

Nesta nova parte da série, vamos aprender um pouco sobre o pattern Composite. Vamos lá!

Composite

A ideia desse padrão é montar uma árvore onde tanto as folhas (objetos individuais) quanto os compostos (grupos de objetos) sejam tratados de maneira igual. Em termos de orientação a objetos, isso significa aplicarmos polimorfismo para chamar métodos de um objeto na árvore sem nos preocuparmos se ele é uma folha ou um composto.

Um exemplo pode ser uma página de internet. A mesma pode conter ícones, caixas de texto, botões, entre outros tipos de elementos. Todos os elementos podem ser considerados como elementos gráficos, inclusive a própria página. Considerando que ela possui uma hierarquia entre os componentes, indicando Elemento Gráfico como uma super-classe, comum a todas as que representam elementos gráficos, temos o diagrama a seguir:

Diagrama UML de exemplo do padrão Composite

No diagrama acima, temos a definição de um componente base, o elemento gráfico, que define um método desenha, que serve para renderizar o elemento na tela. Temos como extensão deste componente base, componentes mais específicos, como uma janela, ou página, uma caixa de texto, um ícone… Mas podemos notar que o componente Janela tem uma indicação que mostra que abaixo dele podem existir outros componentes que tenham como super-classe o Elemento Gráfico. Se tivéssemos um componente DIV no diagrama acima, com certeza ele teria comportamento semelhante ao de Janela, que estende o Elemento Gráfico, mas possui outros abaixo de si. Este seria um exemplo onde temos nas folhas, componentes finais e componentes compostos.

Um outro cenário de exemplo, que podemos utilizar o padrão Composite, seria a construção de um questionário. Dado o diagrama abaixo, temos os elementos a seguir.

  • A Super-Classe (ElementoDoQuestionario): é a interface que define métodos comuns às classes dentro da composição e, opcionalmente, define uma interface para acessar o objeto “pai” de um componente;
  • A Folha (Questao) é um componente que, como o nome indica, não possui filhos (está nas extremidades da árvore);
  • O Composto (Bloco) é um componente que, como o nome indica, é composto de outros componentes, que podem ser folhas ou outros compostos.

Com isso, temos uma forma comum de tratar os componentes do questionário, mas com uma distinção entre perguntas diretas ou blocos de perguntas, segmentando o questionários em assuntos, por exemplo.

Relação com o padrão Iterator

O padrão Composite é bastante utilizado com o Iterator para percorrer a estrutura em forma de árvore oferecida pelo primeiro. Imaginemos que há uma classe abstrata Component com vários métodos e um deles é utilizado para imprimir toda estrutura. Esse método printar() é herdado tanto pela folha, quanto pelo Composite; portanto, fazendo essa iteração é possível percorrer e imprimir todos os componentes de Component, pois tanto a folha, quanto o Composite, são do tipo Component e implementam suas própria versões do método printar().

Iremos abordar o padrão Iterator futuramente, então, voltaremos a mencionar o padrão Composite junto aos exemplos, deixando essa relação mais clara.

Ficha Resumo

  • Nome: Composite;
  • Objetivo / intenção: Utilizado para representar um objeto formado pela composição de objetos similares. Este conjunto de objetos pressupõe uma mesma hierarquia de classes a que ele pertence. Tal padrão é, normalmente, utilizado para representar listas recorrentes, ou recursivas, de elementos;
  • Motivação: A intenção do padrão Composite é compor objetos em estruturas de árvore para representar hierarquia partes-todo;
  • Aplicabilidade: O padrão Composite pode ser utilizado quando queremos trabalhar uma hierarquia de objetos que representem uma mesma super-classe, de modo a tornar sua operação padronizada, sendo, na maioria das vezes, em combinação com o padrão Iterator. Imagine que você está fazendo um sistema de gerenciamento de arquivos. Como você já sabe, é possível criar arquivos concretos (vídeos, textos, imagens, etc.) e arquivos pastas, que armazenam outros arquivos. O problema é o mesmo, como fazer um design que atenda estes requerimentos? Através do padrão Composite, todos terão uma forma comum de serem reconhecidos e trabalhados, através de uma super-classe;
  • Estrutura: Abaixo, temos a estrutura UML do pattern, descrita no livro do GoF. Component declara a interface para objetos nessa composição, implementa o comportamento padrão para a interface comum à todas as classes e declara uma interface para acessar os componentes-filho. Leaf representa o objeto folha na composição. A folha não tem nenhum componente-filho, define o comportamento para objetos primitivos na composição e herda todos os métodos de Component, porém só implementa de fato os que lhe interessam – neste caso, o método Operation, nos outros são inseridos exceções que serão geradas em tempo de execução. Composite define o comportamento para componentes que possuam componentes-filho, armazena componentes-filho e implementa funções relacionadas aos componentes-filho na interface do Componente. E finalmente, Client opera os objetos da composição através da interface do Componente;
  • Consequências: Objetos complexos podem ser compostos de objetos mais simples recursivamente. O cliente pode tratar objetos simples ou compostos de maneira uniforme uma vez que é definida uma interface comum (Component) à ambos. Também facilita a adição de novos componentes: o cliente não tem que mudar com a adição de novos objetos (simples ou compostos). Mas em contrapartida, seu projeto de aplicação acaba ficando “geral” demais, sendo mais difícil restringir os componentes de um objeto composto e também o fato que o sistema de tipagem da linguagem não ajuda a detectar composições erradas;
  • Implementações: Abaixo temos o padrão Composite implementado em C#, utilizando o exemplo do Questionário;
public abstract class ElementoDoQuestionario
{
    protected string Descricao;
 
    protected ElementoDoQuestionario(string descricao)
    {
        Descricao = descricao;
    }
    public abstract void Exibir();
}
 
public class Bloco : ElementoDoQuestionario
{
    private IEnumerable<ElementoDoQuestionario> _elementos = new List<ElementoDoQuestionario>();
    public IEnumerable<ElementoDoQuestionario> Elementos { get { return _elementos; } }
 
    public Bloco(string descricao) : base(descricao) { }
 
    public override void Exibir()
    {
        Console.WriteLine("Bloco: {0}", Descricao);
        foreach (var elemento in _elementos)
            elemento.Exibir();
    }
 
    public void Adicionar(ElementoDoQuestionario elemento)
    {
        _elementos.Add(elemento);
    }
 
    public void Remover(ElementoDoQuestionario elemento)
    {
        _elementos.Remove(elemento);
    }
}
 
public class Questao : ElementoDoQuestionario
{
    public Questao(string descricao) : base(descricao) { }
 
    public override void Exibir()
    {
        Console.WriteLine("Questão: {0}", Descricao);
    }
}
  • Usos conhecidos: Muito utilizado na definição de componentes de tela ou demais estruturas hierárquicas que precisem de um comportamento padrão aos elementos da árvore, sejam simples ou compostos. Também é muito utilizado com o padrão Iterator, para percorrer os elementos de uma hierarquia;
  • Padrões relacionados: Iterator.

Concluindo

Este foi o padrão Composite, geralmente utilizado para estruturas de árvore onde componentes precisam de um comportamento padrão definido e também serem componentes compostos pela combinação de outros componentes ou apenas componentes de implementação simples.

Na próxima parte desta série, vamos abordar o padrão Bridge.

Aguardo feedbacks e dúvidas de vocês.

Um abraço!