Últimas

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.

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): List
    suspend 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:

  1. Serialização: Converter para JSON ou outra representação de string
  2. Atributos separados: Mapear componentes individuais para colunas
  3. 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 GITHUB

Perguntas 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.

Autor: Equipe Dialogando TI

Contato: dialogandoti@gmail.com

Site: www.dialogandoti.com.br

Deixe um comentário: Qual aspecto da modelagem de domínio você achou mais útil para seus projetos Android?

#DDD #Android #ModelagemDeDominio #Entidades #ValueObjects #Agregados #KotlinDev

Nenhum comentário