Engenharia iOS

[Arquitetura iOS #6] Fundamentos da Clean Architecture

Você vê com frequência a expressão “baseado em Clean Architecture” em vagas e blogs técnicos. Mas, quando abre o código, percebe que cada equipe implementa isso de um jeito. Alguns projetos têm UseCase, outros não, e as responsabilidades de Repository também variam.

4 min de leitura
Imagem de capa de [Arquitetura iOS #6] Fundamentos da Clean Architecture

Você vê com frequência a expressão “baseado em Clean Architecture” em vagas e blogs técnicos. Mas, quando abre o código, percebe que cada equipe implementa isso de um jeito. Alguns projetos têm UseCase, outros não, e as responsabilidades de Repository também variam.

O motivo da confusão é simples: Clean Architecture é um princípio, não uma estrutura específica de pastas nem uma lista de classes.

Hoje vamos esclarecer esse princípio e o papel de UseCase e Repository quando são implementados no iOS.


O ponto central é: “as dependências apontam apenas para dentro”

Clean Architecture é um conceito organizado por Robert Martin (Uncle Bob) que enxerga o app como camadas concêntricas.

  • Camada mais interna: domínio. Entidades e regras de negócio. “O que este app faz?”
  • Camada intermediária: UseCase. Cenários de comportamento do app
  • Camada externa: UI, bancos de dados, rede e frameworks

Existe apenas uma regra. As dependências sempre apontam de fora para dentro.

O código do domínio interno não deve conhecer UIKit, URLSession nem SwiftData, que estão nas camadas externas. Já as camadas externas podem conhecer as internas. Assim, mesmo trocando o framework de UI ou alterando a API do servidor, as regras centrais do app permanecem intactas.

Como você deve ter percebido, isso amplia o DIP (Princípio da Inversão de Dependência) para o app inteiro. É aplicar “depender de abstrações, não de implementações concretas” no nível das camadas.


Repository: a fronteira que esconde “de onde vêm os dados”

Repository é a fronteira entre o domínio e as fontes de dados. O protocolo fica no domínio, e a implementação, nas camadas externas.

// Camada de domínio — URLSessionnão SwiftDatasabe nada
protocol PostRepository {
    func fetchPosts(userID: String) async throws -> [Post]
}

// Camada de dados — a implementação fica fora
final class RemotePostRepository: PostRepository {
    func fetchPosts(userID: String) async throws -> [Post] {
        // URLSession chamada, DTO decodificação, Postconversão
    }
}

O ponto é onde o protocolo fica. Como o domínio é dono da interface, ele só sabe que “é possível buscar posts”; não sabe se eles vêm do servidor, do cache ou do banco local. Se o formato da resposta do servidor mudar, basta alterar o DTO (Data Transfer Object, objeto de transferência de dados que transporta a resposta do servidor) e a implementação. Nos testes, você pode conectar um Repository falso.

O domínio não precisa saber de onde vêm os dados
O domínio não precisa saber de onde vêm os dados

UseCase: o contêiner de uma coisa que “este app faz”

Um UseCase transforma um cenário de comportamento do app em um objeto, como “carregar o feed” ou “publicar um post”.

final class LoadFeedUseCase {
    private let postRepository: PostRepository
    private let blockRepository: BlockRepository

    func execute(userID: String) async throws -> [Post] {
        let posts = try await postRepository.fetchPosts(userID: userID)
        let blocked = try await blockRepository.blockedUserIDs()
        return posts
            .filter { !blocked.contains($0.authorID) }
            .sorted { $0.createdAt > $1.createdAt }
    }
}

A regra de negócio “excluir do feed os posts de usuários bloqueados” pertence ao UseCase. O que acontece se ela ficar no ViewModel? As telas de feed, perfil e busca implementariam seus próprios filtros de bloqueio, e em algum momento uma delas ficaria diferente. Ao extraí-la para o UseCase, a regra fica em um só lugar e pode ser testada sem uma tela.

Por isso, MVVM (Model-View-ViewModel) e Clean Architecture não competem entre si: são abordagens ortogonais. MVVM organiza o lado da View, enquanto Clean Architecture organiza as camadas por trás dela. No artigo anterior, dissemos que, para evitar um Massive ViewModel, é preciso “descer a lógica”; UseCase e Repository são o lugar para onde ela deve ir.


Até onde devemos adotar isso?

A armadilha da Clean Architecture é adotá-la em excesso. Se você colocar UseCase, Repository, DTO e Mapper em um app de duas telas, passar um valor exigirá atravessar cinco arquivos. O problema de boilerplate do VIPER (View·Interactor·Presenter·Entity·Router) será reproduzido.

Um critério prático é este.

  • **Repository quase sempre vale a pena.**Só separar o código de rede e DB das telas já facilita testes e mudanças.
  • **Adicione UseCase quando surgirem regras de negócio compartilhadas por várias telas.**Se a maioria dos UseCase apenas repassa chamadas ao Repository, ainda é cedo.
  • **Separe os modelos por camada (DTO/domínio/modelo de tela) depois que o projeto crescer.**Separar tudo desde o início só acumula código de Mapper.
Não é preciso ter tudo: crie uma camada por vez, quando necessário
Não é preciso ter tudo: crie uma camada por vez, quando necessário

Resumo

  • Clean Architecture não é uma estrutura de pastas, mas o princípio de que “as dependências apontam apenas para dentro (o domínio)”. É o DIP ampliado para a escala do app.
  • Repository é a fronteira que esconde a origem dos dados, e o ponto central é que o domínio é dono do protocolo.
  • UseCase é o lugar das regras de negócio compartilhadas por várias telas. Ele é ortogonal ao MVVM, portanto os dois podem ser usados juntos.
  • O objetivo não é ter tudo. Comece pelo Repository e adicione UseCase quando as regras se acumularem.

No próximo artigo, vamos mudar de direção e abordar TCA (The Composable Architecture), que transforma o gerenciamento de estado em uma arquitetura.