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.
- Defina primeiro o comportamento comum em um protocolo
- O decorator segue esse protocolo e mantém internamente outro valor do mesmo tipo de protocolo
- Ele faz seu próprio trabalho, como logging ou cache, e delegará o restante ao objeto envolvido
- 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.
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.
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 🙂

