Design de software

Padrão Decorator no Swift: adicionando recursos sem herança

Ao desenvolver para iOS, às vezes você quer manter a funcionalidade básica e adicionar apenas logging ou cache. Se começar a herdar classes para isso, a árvore de herança logo vira uma bagunça.

4 min de leitura
Imagem de capa de Padrão Decorator no Swift: adicionando recursos sem herança

Ao desenvolver para iOS, às vezes você quer manter a funcionalidade básica e adicionar apenas logging ou cache. Se começar a herdar classes para isso, a árvore de herança logo vira uma bagunça.

Ao adicionar recursos um a um a um cliente de rede, é fácil acabar criando uma classe com um nome como CachingLoggingRetryingClient.

Hoje vamos organizar o padrão Decorator do Swift, que resolve esse problema: adicionar recursos em camadas, como uma cebola, sem herança.

O padrão Decorator envolve um objeto com outro que segue o mesmo protocolo, adicionando comportamento em camadas sem alterar o código original.

A conclusão é esta.

  1. Defina primeiro o comportamento comum em um protocolo
  2. O decorator segue esse protocolo e mantém internamente outro valor do mesmo tipo de protocolo
  3. Ele faz seu próprio trabalho, como logging ou cache, e delegará o restante ao objeto envolvido
  4. Continue envolvendo conforme necessário para acumular recursos

O que é o padrão Decorator? Qual é a diferença em relação à herança?

Herança expressa uma relação “é um tipo de”. A classe filha herda tudo da classe pai.

O problema são as combinações. Se você criar clientes com logging, cache ou ambos usando herança, cada combinação vira uma classe.

Um decorator expressa uma relação de “algo envolvido”.

Como o objeto que envolve e o objeto envolvido compartilham a mesma interface, eles parecem iguais externamente. O código consumidor permanece igual, independentemente do número de camadas.

Em resumo, a herança fixa relações em tempo de compilação, enquanto decorators podem ser compostos livremente em tempo de execução.


Implementando o padrão Decorator do Swift com código

Vamos criar um DataLoader que carregue dados. Primeiro, este é o protocolo que todos seguirão.

protocol DataLoader {
    func load(id: String) -> Data?
}

// Implementação básica que faz o trabalho real
struct NetworkLoader: DataLoader {
    func load(id: String) -> Data? {
        print("Requisição \(id)  à rede")
        return Data("payload-\(id)".utf8)
    }
}

Até aqui, temos apenas um protocolo e uma implementação comuns.

Agora vem a parte central. O decorator segue DataLoader e contém outro DataLoader internamente.

// Decorator de logging
struct LoggingLoader: DataLoader {
    let wrapped: DataLoader   // Alvo envolvido

    func load(id: String) -> Data? {
        print("[Início] load  do log: \(id)")
        let result = wrapped.load(id: id)  // Delegar o trabalho real
        print("[Conclusão] load  do log: \(id)")
        return result
    }
}

LoggingLoader faz apenas seu trabalho, registrar o log, e passa o carregamento real para wrapped.

Graças a essa estrutura, você não precisa se preocupar com o que foi envolvido. Basta ser DataLoader.

Estrutura que faz seu trabalho e delega o restante ao objeto interno
Estrutura que faz seu trabalho e delega o restante ao objeto interno

Acumulando recursos em camadas

Vamos criar mais um decorator de cache para realmente empilhar as camadas.

final class CachingLoader: DataLoader {
    let wrapped: DataLoader
    private var cache: [String: Data] = [:]

    init(wrapping loader: DataLoader) { self.wrapped = loader }

    func load(id: String) -> Data? {
        if let hit = cache[id] { return hit }        // Retornar imediatamente se estiver no cache
        let data = wrapped.load(id: id)              // Delegar se não estiver
        cache[id] = data
        return data
    }
}

A parte de montagem é o ponto alto do padrão Decorator.

let loader = LoggingLoader(
    wrapped: CachingLoader(wrapping: NetworkLoader())
)

Leia de dentro para fora. Envolvemos o carregador de rede com cache e depois envolvemos esse resultado com logging.

Quando chega uma chamada, o fluxo é logging → verificar o cache → rede se não estiver disponível.

As chamadas descem uma camada por vez pelos wrappers
As chamadas descem uma camada por vez pelos wrappers
Camadas aninhadas, de dentro para fora: rede, cache e logging
Camadas aninhadas, de dentro para fora: rede, cache e logging

Para mudar a ordem, basta mudar a ordem dos wrappers. Nem NetworkLoader nem CachingLoader exigem alterar uma linha de código.

Se um recurso não for necessário, basta remover essa camada.


Herança ou decorators: quando usar cada um?

Nenhuma opção é sempre a resposta certa. Vamos separar por situação.

Situação Recomendação
Relação clara de “é um tipo de” Herança
Combinar e remover recursos livremente Decorator
Ativar e desativar recursos em tempo de execução Decorator
Quando o código original não pode ser alterado Decorator
Muitas combinações possíveis Decorator

Decorators funcionam especialmente bem para recursos transversais, não para o comportamento essencial, como logging, cache, tentativas novamente e adição de cabeçalhos de autenticação.

Forçar decorators pode criar muitas camadas e aprofundar a call stack durante a depuração. É importante considerar essa desvantagem.


Duas perguntas frequentes

P. Devo usar struct ou class?

Se o decorator precisar manter estado, como um cache, uma class é conveniente. Para delegação simples, um struct basta. Por isso, no exemplo acima, apenas o cache usa class.

P. Muitas camadas de wrappers causam problemas de desempenho?

Como as chamadas passam por cada camada, existe uma sobrecarga mínima. Comparada à E/S de rede ou disco, porém, ela é insignificante na maioria dos apps.


No fim, o padrão Decorator consiste em definir um protocolo, criar um objeto que contenha o mesmo protocolo e delegar por meio dele.

Na próxima vez que você se pegar vasculhando a árvore de herança para adicionar um recurso, pense em envolvê-lo com mais uma camada. O código ficará muito mais leve. Recomendo levar o exemplo de hoje diretamente para um playground e testá-lo 🙂

Leituras recomendadas