Design de software

Padrão Middleware em Swift: combinação prática de Decorator e Chain of Responsibility

Ao criar uma camada de rede, você inevitavelmente chega a um momento assim.

4 min de leitura
Imagem de capa de Padrão Middleware em Swift: combinação prática de Decorator e Chain of Responsibility

Ao criar uma camada de rede, você inevitavelmente chega a um momento assim.

Uma única requisição precisa de token de autenticação, logs e novas tentativas em caso de falha.

Ao colocar tudo em uma função, send() logo chega a 200 linhas.

É nesse momento que entra o padrão middleware.

A conclusão: em Swift, a abordagem mais limpa é envolver cada funcionalidade com o padrão Decorator e conectar esses wrappers em ordem usando Chain of Responsibility. Eles não competem; formam uma dupla.

Hoje veremos por que usar essa combinação e como montá-la com um exemplo prático.


O que é o padrão middleware?

Middleware é uma camada intermediária de processamento entre a requisição e a resposta.

Se você já usou o RequestInterceptor do Alamofire ou o Middleware do Vapor no servidor, já conhece esse conceito.

A ideia central é simples.

Manter o núcleo que processa a requisição intacto e permitir adicionar ou remover funcionalidades ao seu redor.

Criar autenticação, logs, cache e novas tentativas como componentes independentes.

Assim, você pode combiná-los livremente: apenas autenticação em uma requisição; autenticação e cache em outra.


Qual é a diferença entre Decorator e Chain of Responsibility?

Muita gente confunde esses dois conceitos.

Resumindo, a diferença é esta.

Categoria Decorator Chain of Responsibility
Objetivo Adicionar funcionalidade a um objeto existente Colocar vários processadores em sequência
Relação Estrutura de wrappers (aninhada) Estrutura conectada (cadeia)
Continuidade Sempre encaminha para o próximo Pode parar no meio
Papel no middleware Implementar cada funcionalidade Conectar e ordenar essas funcionalidades

Depois de usar os dois, percebi que não são ideias opostas, mas conceitos de níveis diferentes.

Decorator trata de “como adicionar uma funcionalidade”; Chain of Responsibility, de “em que ordem executar o que foi adicionado”.

Juntos, eles compensam as limitações um do outro.


Combinação prática: montando com código

Primeiro, definimos a interface comum do middleware. Ela recebe uma requisição e a encaminha ao próximo middleware.

protocol Middleware {
    // requestrecebe e processa e, depois, encaminha para a próxima etapa por meio de , next
    func handle(_ request: Request,
                next: (Request) async throws -> Response)
    async throws -> Response
}

Agora criamos cada funcionalidade uma por vez, como um decorator. Este é o middleware de logs.

struct LoggingMiddleware: Middleware {
    func handle(_ request: Request,
                next: (Request) async throws -> Response)
    async throws -> Response {
        print("➡️ requisição: \(request.url)")
        let response = try await next(request)  // para a próxima etapa
        print("⬅️ resposta: \(response.status)")
        return response
    }
}

A chamada a next(request) é o elo do Chain of Responsibility.

Ele faz seu trabalho —registrar o log— e encaminha o restante ao próximo middleware.

Por fim, reunimos vários middlewares em uma única cadeia.

func buildChain(_ middlewares: [Middleware],
                final: @escaping (Request) async throws -> Response)
-> (Request) async throws -> Response {
    middlewares.reversed().reduce(final) { next, mw in
        { req in try await mw.handle(req, next: next) }
    }
}

O ponto principal é envolver tudo de trás para frente com reduce.

Se você inserir o array na ordem [인증, 로깅, 재시도], as requisições passarão nessa ordem e as respostas sairão na ordem inversa. Pense nas camadas de uma cebola.

As requisições entram; as respostas saem no sentido inverso.
As requisições entram; as respostas saem no sentido inverso.
Antes de escrever o código, desenhei a estrutura em camadas no papel
Antes de escrever o código, desenhei a estrutura em camadas no papel

Quais são as vantagens?

Veja as vantagens que percebi depois de aplicar isso em um projeto real.

  1. Adicionar uma funcionalidade vira adicionar uma linha ao array. Se precisar de cache, basta inserir CachingMiddleware() no array.

  2. É fácil mudar a ordem. Para executar a autenticação antes ou depois dos logs, basta alterar a ordem do array.

  3. Testar fica mais fácil. Como cada middleware é independente, você pode adicionar testes unitários um por um.

  4. O núcleo fica limpo. O send() de 200 linhas volta a ter menos de dez.

Por outro lado, há alguns cuidados.

Quando há middlewares demais, fica difícil rastrear por onde uma requisição passa.

Por isso, quando a cadeia passa de cinco ou seis componentes, coloco o middleware de logs no início e registro o caminho percorrido.


Resumo

Depois que você se acostuma, essa combinação —criar funcionalidades com Decorator e ordená-las com Chain of Responsibility— pode ser usada não só em camadas de rede, mas também no processamento de eventos e na validação.

Recomendo adicionar o exemplo de hoje a um pequeno projeto de teste. Ao desmontar as camadas por conta própria, o conceito fica muito mais fácil de absorver. Boa refatoração!

Leituras relacionadas