Design de software

Padrão Composite em Swift: tratando estruturas de árvore como um único objeto

Uma pasta dentro de outra, com mais arquivos dentro. Se você já lidou com esse tipo de estrutura em árvore no código, provavelmente se sentiu perdido pelo menos uma vez.

4 min de leitura
Imagem de capa de Padrão Composite em Swift: tratando estruturas de árvore como um único objeto

Uma pasta dentro de outra, com mais arquivos dentro. Se você já lidou com esse tipo de estrutura em árvore no código, provavelmente se sentiu perdido pelo menos uma vez.

Se você usa if toda vez para separar «isto é uma pasta ou um arquivo?», o código fica confuso rapidamente.

É um problema comum ao criar algo parecido com um explorador de arquivos. Nessa situação, a ferramenta certa é o padrão Composite.

O padrão Composite trata objetos individuais (arquivos) e seus agrupamentos (pastas) como o mesmo tipo, permitindo lidar com toda a árvore como um único objeto.

Hoje vou mostrar como implementar esse padrão em Swift, usando o código que escrevi na prática.

Resumo dos pontos principais

  1. Padrão Composite = agrupar folhas e composites no mesmo protocolo
  2. O cliente faz a mesma chamada sem distinguir arquivos de pastas
  3. O cálculo da pasta pergunta novamente aos filhos, usando uma estrutura recursiva
  4. No Swift, é possível implementar isso de forma simples com apenas protocol

O que é o padrão Composite?

Literalmente, é «composição»: reunir coisas pequenas para formar uma unidade maior.

A analogia mais comum é um sistema de arquivos.

Um arquivo é uma folha que termina em si mesma. Já uma pasta é um ramo que pode conter arquivos ou outras pastas.

Uma pasta dentro de outra: essa estrutura é exatamente um Composite
Uma pasta dentro de outra: essa estrutura é exatamente um Composite

Mas a perspectiva do usuário é diferente. Ao abrir uma pasta ou um arquivo, simplesmente damos o comando «Abrir».

O objetivo do padrão Composite é tratar elementos individuais e agrupamentos da mesma maneira.

Os componentes se dividem em três tipos.

Visto em um diagrama, é simples assim
Visto em um diagrama, é simples assim
  • Component: interface comum (protocolo)
  • Leaf: objeto terminal sem filhos (arquivo)
  • Composite: objeto que contém filhos (pasta)

Como implementar em Swift?

Primeiro, defina o protocolo comum para consultar o nome e o tamanho.

A seguir estão as regras (Component) que arquivos e pastas seguem em conjunto.

protocol FileComponent {
    var name: String { get }
    func size() -> Int   // retorna o tamanho em bytes
}

Agora temos o arquivo, que corresponde à folha. Basta retornar seu próprio tamanho, então é simples.

struct File: FileComponent {
    let name: String
    let bytes: Int
    func size() -> Int { bytes }  // retorna o próprio tamanho sem alterações
}

O ponto principal é a pasta (Composite). Ela armazena os filhos em um array e, quando consultada sobre o tamanho, pergunta novamente aos filhos.

struct Folder: FileComponent {
    let name: String
    var children: [FileComponent] = []
    func size() -> Int {
        children.reduce(0) { $0 + $1.size() }  // soma dos filhos (recursiva)
    }
}

O ponto importante é que size() chama a si mesma novamente, formando uma estrutura recursiva.

Não importa quantos níveis de pastas existam: essa linha percorre tudo automaticamente até o fim.


Na prática, fica assim

Agora vamos criar uma árvore misturando arquivos e pastas.

let root = Folder(name: "Documentos", children: [
    File(name: "Notas.txt", bytes: 100),
    Folder(name: "Fotos", children: [
        File(name: "Viagem.jpg", bytes: 2000)
    ])
])
print(root.size())  // 2100 imprimir

Viu? Uma única chamada a root.size() soma o tamanho de todos os arquivos da pasta.

Essa foi a parte que mais me impressionou quando executei o código.

No código do cliente, nunca perguntamos «isto é uma pasta ou um arquivo?».

Basta chamar size(). Cada objeto cuida do restante.

Colei o exemplo no Playground exatamente como estava e executei
Colei o exemplo no Playground exatamente como estava e executei

Qual é a diferença em relação a usar condicionais?

Você deve estar pensando: «Não posso simplesmente separar os tipos com uma instrução if e processá-los?»

Pode. Mas, quanto mais profunda a árvore, maior fica a diferença.

Categoria Abordagem com condicionais (if/switch) Padrão Composite
Identificação do tipo Verificar manualmente todas as vezes Não é necessário
Adicionar um novo tipo Modificar condicionais em vários lugares Adotar o protocolo
Processamento recursivo Escrever o código de percurso manualmente O objeto cuida disso
Legibilidade do código Fica mais complexo com a profundidade Mantém-se consistente

Na abordagem condicional, ao surgir um novo tipo (como um link de atalho), é preciso adicionar if novamente em vários lugares.

Com Composite, basta criar um novo tipo que adote FileComponent. Não é necessário alterar o código existente.


Perguntas frequentes

P. As interfaces de Leaf e Composite precisam ser exatamente iguais?

Idealmente, sim. Mas uma função como «adicionar filho» não faz sentido para um arquivo. Nesse caso, coloque no protocolo apenas os comportamentos comuns (como size) e deixe o gerenciamento dos filhos exclusivamente para as pastas.

P. Posso usar class em vez de struct?

Sim, como no exemplo acima, um tipo por valor também funciona bem. Porém, se você altera a árvore com frequência e precisa compartilhar referências, class pode ser mais conveniente.


Se estruturas em árvore colocaram você no inferno de if, experimente usar o padrão Composite.

Ele funciona bem não apenas em sistemas de arquivos, mas também em hierarquias de views, menus e organogramas — qualquer estrutura com «mais coisas dentro».

Cole o exemplo de hoje no Playground e execute-o; a ideia ficará clara rapidamente.

Leituras recomendadas