O uso de Value Objects (VO) na modelagem ajuda a tornar o domínio muito mais robusto por meio do encapsulamento de regras de negócios. Além disso o uso dos VO torna o código muito mais legível, porém seu uso pode reservar algumas surpresas quando for utilizado em conjunto com o Entity Framework, por exemplo.

Value Objects por definição não possuem identidades próprias e a única coisa que importa é sua combinação de atributos. Telefone, CEP, Placa de Carro e moeda são exemplos de possíveis VO.

Nesse post pretendo mostrar um exemplo de utilização da classe Telefone como VO. O objetivo é elaborar sua evolução aos poucos de modo a demonstrar o problema de conflito de configurações e como solucioná-lo.

// classe cliente sem Value Objects
class Cliente
{
 public int IdCliente { get; private set; }
 public string Nome { get; private set; }
 public string TelefoneFixo { get; private set; }
 public string TelefoneCelular { get; private set; }
}

A classe Cliente não possui nada de especial. Suas propriedades são um Id, um nome e dois telefones (fixo e celular), mas a definição desta classe não deixa claro se os telefones devem ser preenchidos com DDD ou se devem estar formatados com traço e parênteses, ou seja, o código não está expressivo.

A mesma classe foi reescrita com a utilização de um Value Object para armazenar o telefone e veja como o código fica mais claro. Além disso a responsabilidade de validar o telefone quanto seu correto preenchimento foi delegada para a classe Telefone:

// classe cliente com o VO de Telefone
class Cliente
{
    public int IdCliente { get; private set; }
    public string Nome { get; private set; }
    public Telefone TelefoneFixo { get; private set; }
    public Telefone TelefoneCelular { get; private set; }
}

// Value Object Telefone
class Telefone
{
    public Telefone(string ddd, string numero)
    {
        // constrói apenas instâncias de telefones válidos
        if (!ValidarDDD(ddd) && !ValidarNumero(numero))
            throw new InvalidOperationException("Telefone inválido.");

        DDD = ddd;
        Numero = numero;
    }

    private bool ValidarDDD(string ddd)
    {
        var regex = new Regex("^[0]{0,1}[0-9]{2}$");
        return regex.IsMatch(ddd);
    }

    private bool ValidarNumero(string numero)
    {
        var regex = new Regex("^[0-9]{4,5}-[0-9]{4}$");
        return regex.IsMatch(numero);
    }

    public string DDD { get; set; }
    public string Numero { get; private set; }
}

Por enquanto tudo parece OK, portanto vamos dar uma olhada em como nossa entidade Cliente é mapeada no Entity Framework.

class AppDbContext : DbContext
{
    public virtual DbSet<Cliente> Clientes { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

        // Chave Primária - Cliente
        modelBuilder.Entity<Cliente>().HasKey(x => x.IdCliente);

        // Propriedades - Cliente
        modelBuilder.Entity<Cliente>()
            .Property(x => x.Nome).HasMaxLength(50).IsRequired();
            
        modelBuilder.Entity<Cliente>()
            .Property(x => x.TelefoneFixo.DDD).HasMaxLength(3).IsRequired();
        modelBuilder.Entity<Cliente>()
            .Property(x => x.TelefoneFixo.Numero).HasMaxLength(10).IsRequired();

        modelBuilder.Entity<Cliente>()
            .Property(x => x.TelefoneCelular.DDD).HasMaxLength(3).IsRequired();
        modelBuilder.Entity<Cliente>()
            .Property(x => x.TelefoneCelular.Numero).HasMaxLength(10).IsRequired();
    }
}

Com o mapeamento anterior todas as propriedades de cliente e de seus agregados (telefone) serão persistidos em uma mesma entidade.

Agora vamos imaginar que o número do telefone fixo seja opcional e para isso teoricamente bastaria uma pequena alteração na configuração.

// TelefoneFixo com IsOptional (o resto permanece igual)
modelBuilder.Entity<Cliente>()
     .Property(x => x.TelefoneFixo.DDD).HasMaxLength(3).IsOptional();
modelBuilder.Entity<Cliente>()
     .Property(x => x.TelefoneFixo.Numero).HasMaxLength(10).IsOptional();

É nesse ponto que encontramos a armadilha do conflito de configurações, pois o Entity Framework possui uma limitação na hora de mapear tipos complexos e não distingue que a propriedade TelefoneFixo está sendo mapeada de forma independente de TelefoneCelular. Ele mapeia uma única vez o tipo complexo telefone e a mensagem de erro deixa isso claro:

Conflicting configuration settings were specified for property ‘DDD’ on type ‘ExemploValueObjects.ValueObjects.Telefone’: Conflicting configuration settings were specified for property ‘DDD’ on type ‘ExemploValueObjects.ValueObjects.Telefone’:  IsNullable = True conflicts with IsNullable = False“.

Para contornar a situação basta criar duas classes que herdem de telefone e configurá-las de maneira independente conforme exemplo:

// classe cliente com VO independentes para TelefoneFixo e TelefoneCelular
class Cliente
{
    public int IdCliente { get; set; }
    public string Nome { get; set; }
    public TelefoneFixo TelefoneFixo { get; set; }
    public TelefoneCelular TelefoneCelular { get; set; }
}

class TelefoneFixo : Telefone
{
    public TelefoneFixo(string ddd, string numero) : base(ddd, numero) { }
}

class TelefoneCelular : Telefone
{
    public TelefoneCelular(string ddd, string numero) : base(ddd, numero) { }
}

Pronto! A alteração anterior já é o suficiente para que ambos os telefones possam ter configurações totalmente independentes.

Não gosto de alterar o modelo de domínio por conta de uma necessidade da infra-estrutura, mas é um pouco irreal a ideia de que nosso domínio será imutável e blindado quanto às questões de persistência de dados . Quem já usou DDD com Entity Framework sabe que temos que “contaminar” nossas entidades de domínio com coisas como o modificador virtual para possibilitar o Lazy Loading ou com um construtor protegido (protected) mesmo quando nosso domínio não precisa dele.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *