Modelando o Domínio com DDD: Entidades, Value Objects e Agregados em Apps Android
Modelando o Domínio com DDD: Entidades, Value Objects e Agregados em Apps Android
A diferença entre um código que precisa ser reescrito em 6 meses e um que dura anos está na forma como você modela seu domínio. Neste artigo, você aprenderá como implementar corretamente os blocos fundamentais do Domain-Driven Design em aplicativos Android.
▶️ Assista ao vídeo completo sobre Entidades, Value Objects e Agregados em DDD para Android
Por que a modelagem de domínio é crucial para apps Android de qualidade
Se você já desenvolveu um aplicativo Android de médio ou grande porte, provavelmente já enfrentou um ou mais destes problemas:
- Código que quebra facilmente quando requisitos mudam
- Regras de negócio espalhadas por Activities, Fragments e ViewModels
- Dificuldade em manter a consistência dos dados, especialmente em operações offline
- Testes que se tornam pesadelos devido ao acoplamento entre componentes
A modelagem de domínio usando conceitos do Domain-Driven Design (DDD) oferece soluções comprovadas para estes problemas. Neste artigo, vamos focar nos blocos de construção táticos do DDD: Entidades, Value Objects e Agregados, e como implementá-los corretamente em aplicativos Android.
Entidades vs. Value Objects: Entendendo as diferenças
Entidades
Definição: Objetos que têm uma identidade única que persiste ao longo do tempo.
Características:
- Possuem identificador único
- Podem ser mutáveis
- Têm ciclo de vida
- Igualdade baseada em identidade
Exemplo:
class Documento( val id: UUID, private var numero: String, private var tipo: TipoDocumento ) { fun alterarNumero(novoNumero: String) { validarNumero(novoNumero) this.numero = novoNumero } fun getNumero(): String = numero // Lógica de validação e regras de negócio }
Value Objects
Definição: Objetos que não têm identidade própria e representam valores conceituais.
Características:
- Imutáveis
- Sem identificador
- Igualdade baseada em atributos
- Descartáveis/substituíveis
Exemplo:
data class CPF(val valor: String) { init { require(validarFormato(valor)) { "CPF com formato inválido" } require(validarDigitos(valor)) { "CPF com dígitos verificadores inválidos" } } fun formatado(): String { // Retorna o CPF formatado (xxx.xxx.xxx-xx) } // Funções de validação }
A distinção entre Entidades e Value Objects não é apenas conceitual, mas tem implicações práticas profundas em como seu código se comporta e evolui. Entidades representam objetos que mantêm sua identidade mesmo quando seus atributos mudam. Por exemplo, um Usuario
em seu sistema continua sendo o mesmo usuário mesmo se ele mudar seu nome ou email.
Já Value Objects são definidos por seus atributos. Um CPF "123.456.789-00" é sempre igual a outro CPF com o mesmo valor, não importa onde ou como seja usado. Value Objects são imutáveis, o que os torna seguros para uso concorrente e facilita o raciocínio sobre o código.
Implementando Value Objects em Kotlin para Android
Kotlin é particularmente adequado para implementação de Value Objects graças às data classes
, que já fornecem implementações de equals()
, hashCode()
e toString()
baseadas em propriedades:
data class Email(val endereco: String) { init { require(endereco.matches(Regex("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"))) { "Email inválido: $endereco" } } fun dominio(): String = endereco.substringAfterLast("@") // Métodos utilitários que não alteram o estado } // Uso: val emailUsuario = Email("usuario@exemplo.com") val dominio = emailUsuario.dominio() // "exemplo.com"
💡 Dica de Implementação
Value Objects são excelentes candidatos para validação no construtor, já que são imutáveis. Isso garante que todos os Value Objects em seu sistema estejam sempre em um estado válido, simplificando enormemente o código que os utiliza.
Agregados: Mantendo a Consistência em Seu Domínio
Enquanto Entidades e Value Objects são os blocos básicos de construção, Agregados são como os encapsulamos para garantir a consistência do domínio. Um Agregado é um cluster de objetos tratados como uma unidade para mudanças de dados.
Figura 1: Estrutura de um Agregado com sua Raiz e componentes internos
Cada Agregado tem uma Raiz de Agregado (Aggregate Root), que:
- É a única entidade visível fora do Agregado
- Controla todo o acesso aos objetos internos do Agregado
- Garante que todas as invariantes do Agregado sejam mantidas
Exemplo de Agregado em nosso aplicativo:
// Raiz do Agregado class ValidacaoEmLote( val id: UUID, private val descricao: String ) { private val documentos = mutableListOf() private var status = StatusValidacao.PENDENTE // Controla o acesso e modificações da coleção interna fun adicionarDocumento(documento: Documento) { require(status == StatusValidacao.PENDENTE) { "Não é possível adicionar documentos a uma validação já processada" } documentos.add(documento) } fun iniciarValidacao() { require(documentos.isNotEmpty()) { "Não há documentos para validar" } status = StatusValidacao.EM_PROCESSAMENTO // Lógica de validação } // Outros métodos que manipulam o estado interno // Métodos de acesso controlado fun getDocumentos(): List = documentos.toList() // Cópia defensiva fun getStatus(): StatusValidacao = status } enum class StatusValidacao { PENDENTE, EM_PROCESSAMENTO, CONCLUIDA, FALHA }
⚠️ Cuidado!
Um erro comum ao implementar Agregados em Android é permitir o acesso direto a coleções mutáveis internas. Sempre retorne cópias defensivas (toList()
, copy()
) para evitar que o estado interno seja modificado incorretamente.
Protegendo Invariantes de Domínio em Aplicativos Android
Invariantes são condições que devem sempre ser verdadeiras para que seu sistema esteja em um estado consistente. Em ambientes mobile, proteger essas invariantes é ainda mais desafiador devido a:
- Processos que podem ser encerrados a qualquer momento
- Ciclo de vida complexo das Activities e Fragments
- Operações offline e sincronização posterior
- Limitações de recursos de hardware
Estratégias para proteger invariantes:
1. Encapsulamento rigoroso
Torne campos privados e forneça métodos que garantam que as alterações mantenham as invariantes. Use interfaces para expor apenas o necessário.
private var saldo: BigDecimal // Em vez de setters públicos: fun debitar(valor: BigDecimal) { require(valor > BigDecimal.ZERO) require(temSaldoSuficiente(valor)) this.saldo = this.saldo.subtract(valor) }
2. Construtores e Factory Methods
Garanta que objetos sejam criados já em estado válido, rejeitando dados inválidos imediatamente.
companion object { fun criarDocumento( numero: String, tipo: TipoDocumento ): Documento { // Validações return Documento(UUID.randomUUID(), numero, tipo) } }
3. Objeto Imutáveis
Use objetos imutáveis sempre que possível, especialmente para Value Objects, eliminando preocupações com estados inconsistentes.
// Modificação retorna nova instância fun comNovoEndereco(endereco: Endereco): Cliente { return copy(endereco = endereco) }
Persistência e Estado em Aplicativos DDD para Android
Um desafio único em aplicativos Android é gerenciar a persistência de entidades e value objects, especialmente considerando as restrições do ambiente móvel.
O padrão Repository
O padrão Repository fornece uma abstração sobre sua camada de persistência, permitindo que seu código de domínio trabalhe com objetos sem se preocupar com os detalhes de como eles são armazenados ou recuperados.
interface DocumentoRepository { suspend fun salvar(documento: Documento) suspend fun buscarPorId(id: UUID): Documento? suspend fun buscarPorNumero(numero: String): Listsuspend fun remover(documento: Documento) } // Implementação usando Room class RoomDocumentoRepository( private val documentoDao: DocumentoDao, private val mapper: DocumentoMapper ) : DocumentoRepository { override suspend fun salvar(documento: Documento) { val documentoEntity = mapper.toEntity(documento) documentoDao.insert(documentoEntity) } override suspend fun buscarPorId(id: UUID): Documento? { val entity = documentoDao.getById(id.toString()) ?: return null return mapper.toDomain(entity) } // Outras implementações }
💡 Dica para Persistência
Mantenha seus objetos de domínio separados das entidades de persistência (classes Room). Use mappers para conversão entre eles. Isso permite que seu modelo de domínio evolua independentemente do esquema de banco de dados.
Lidando com Value Objects em bancos de dados:
Value objects geralmente não se encaixam perfeitamente em modelos relacionais. Algumas estratégias:
- Serialização: Converter para JSON ou outra representação de string
- Atributos separados: Mapear componentes individuais para colunas
- Type Converters: Usar conversores personalizados do Room
// Exemplo de Type Converter para Room class CPFConverter { @TypeConverter fun fromCPF(cpf: CPF): String = cpf.valor @TypeConverter fun toCPF(valor: String): CPF = CPF(valor) }
Implementação na Prática: Considerações para Android
Vamos ver como estes conceitos se traduzem em uma implementação real de Android:
Integrando com Android Architecture Components
- Use ViewModels como fachada para casos de uso do domínio
- Mantenha a lógica do domínio independente do Android Framework
- Use coroutines para operações assíncronas
- Considere Flow para observar mudanças em repositórios
Salvando estado durante mudanças de configuração
- Mantenha estado em ViewModels, não em Activities/Fragments
- Use SavedStateHandle para persistência durante o ciclo de vida
- Considere o uso de Process Death Test para garantir recuperação adequada
Testabilidade
- Modelos de domínio bem projetados são facilmente testáveis
- Escreva testes unitários para entities e value objects
- Use testes de integração para repositories
- Teste invariantes explicitamente
Conclusão: O Poder do Modelo de Domínio Bem Projetado
Um modelo de domínio bem projetado, utilizando conceitos de DDD como Entidades, Value Objects e Agregados, oferece benefícios substanciais para aplicativos Android:
- Código mais expressivo que reflete o domínio do problema
- Regras de negócio centralizadas e protegidas
- Maior testabilidade com lógica de domínio isolada
- Evolução mais fácil conforme os requisitos mudam
- Base sólida para recursos avançados como sincronização offline
No próximo artigo da série, exploraremos como integrar estes conceitos de domínio com uma arquitetura Clean no Android, criando uma separação clara entre as camadas e responsabilidades do aplicativo.
Quer aprofundar seus conhecimentos em DDD para Android?
Acompanhe nossa série completa no canal Dialogando TI e baixe os exemplos de código no GitHub.
INSCREVER-SE NO CANAL CÓDIGO NO GITHUBPerguntas Frequentes
P: Entidades e Value Objects precisam ser classes separadas? Não posso usar apenas data classes para tudo?
R: Você pode usar data classes para Value Objects, mas não é recomendado usá-las para Entidades. Entidades têm identidade e comportamento que vai além do que data classes oferecem. Além disso, Entidades geralmente são mutáveis, enquanto Value Objects devem ser imutáveis.
P: Como lidar com Agregados em um ambiente offline-first como Android?
R: Em ambientes offline, é crucial definir limites claros de consistência. Use eventos de domínio para comunicação entre agregados e considere a consistência eventual para sincronização. Mantenha agregados pequenos para minimizar conflitos.
P: Vale a pena implementar DDD em apps Android pequenos?
R: Mesmo em apps pequenos, conceitos como Value Objects e encapsulamento de regras de negócio trazem benefícios. Para projetos muito simples, você pode aplicar partes do DDD sem implementar todos os padrões. Comece com os conceitos que resolvem seus problemas mais imediatos.
Deixe um comentário: Qual aspecto da modelagem de domínio você achou mais útil para seus projetos Android?
Nenhum comentário
Postar um comentário