Pular para o conteúdo

Boas práticas e padrões · Arquitetura do código · 7 min de leitura

Clean Architecture e hexagonal (portas e adaptadores)

Regras de negócio no centro, frameworks na borda, dependências apontando sempre para dentro.

Domínio (sem framework)UI → ViewModelViewModel → Casos de usoCasos de uso → PortasBanco → Portas: implementaimplementaAPI → Portas: implementaimplementaUI — Compose / SwiftUIUICompose / SwiftUIViewModel — apresentaçãoViewModelapresentaçãoCasos de uso — regrasCasos de usoregrasPortas — interfacesPortasinterfacesBanco — adaptadorBancoadaptadorAPI — adaptadorAPIadaptador

Arraste para o lado para ver o diagrama inteiro.

O domínio (casos de uso + portas) não importa nenhum framework. Banco e API implementam as portas — a seta de dependência aponta para dentro.

A regra da dependência

Martin (2012) unificou ideias da arquitetura hexagonal de Alistair Cockburn (2005) e da arquitetura cebola: o código só pode depender de camadas mais internas. Entidades e casos de uso não conhecem banco, HTTP nem UI. Por isso dá para testar o negócio sem nada disso.

Portas e adaptadores

Na hexagonal, o domínio define portas (interfaces) de entrada (o que o app oferece) e de saída (o que ele precisa: salvar, enviar). Adaptadores conectam o mundo real: controller HTTP, tela, Room, Retrofit.

Sem cerimônia

Para um app pequeno, três pacotes (ui, domain, data) bastam; casos de uso só valem quando há regra para colocar neles. O guia do Android marca a camada de domínio como opcional. A arquitetura deve acompanhar a complexidade do projeto.

No código

✓ ExemploKotlin
// domain (módulo puro Kotlin, sem Android)
data class Meta(val id: String, val valorAlvo: Long, val guardado: Long) { val progresso get() = guardado * 100 / valorAlvo }
interface MetasPort { suspend fun buscar(id: String): Meta?; suspend fun salvar(m: Meta) }
class Depositar(private val metas: MetasPort) {
  suspend operator fun invoke(id: String, centavos: Long): Meta {
    require(centavos > 0)
    val m = metas.buscar(id) ?: error("meta não encontrada")
    return m.copy(guardado = m.guardado + centavos).also { metas.salvar(it) }
  }
}
// data (adaptador)
class RoomMetas(private val dao: MetaDao) : MetasPort { /* mapeia entity <-> domínio */ }
// ui
class MetaViewModel(private val depositar: Depositar) : ViewModel()

O módulo domain compila sem Android e pode ir para o código compartilhado do KMP; Room é só um adaptador.

Em resumo

Vantagens e custos

Vantagens
  • Regras de negócio testáveis e independentes de framework.
  • Trocar banco, API ou UI afeta só a borda.
  • Compartilhar o domínio em Kotlin Multiplatform.
Custos e riscos
  • Mais interfaces, mapeamentos e arquivos.
  • Cerimônia desnecessária em CRUD simples.

Referências

Continue aprendendo

Todos os temas de boas práticas e padrões →