C# -Você sabe quando usar Value Objects ?


afinal quando usar Value Objects ?

Ao definir um modelo de domínio, você cria as entidades definindo as propriedades e métodos. As propriedades representam o estado interno da entidade e os métodos são as ações que podem ser executadas. As propriedades geralmente são definidas usando tipos primitivos, como strings, números, datas e assim por diante.

Mas o que são Value Objects e de onde eles surgiram ?

Os Value Objects ou Objetos de valor representam valores tipados, que não têm identidade conceitual, em seu domínio. Eles podem ajudá-lo a escrever um código melhor, menos sujeito a erros, mais eficiente e mais expressivo.

Os Value Objects  são um dos blocos de construção introduzidos no livro Domain-Driven Design escrito por Eric Evans.

O Domain Driven Design - DDD é uma filosofia de desenvolvimento de softwares complexos usando boas práticas na qual a estrutura e a linguagem do seu código (nomes de classe, métodos, variáveis, etc.) devem estar focados no modelo de domínio ou negócio.

No DDD, os Value Objects diferem das entidades por não terem o conceito de identidade. Não nos importamos com quem eles são, mas sim o que são. Eles são definidos por seus atributos e devem ser imutáveis.

Principais características dos Value Objects

Podemos definir 3 principais características dos Value Objects:

1. Igualdade de Valor (Value Equality)

Os Value Objects são definidos por seus atributos. Eles são iguais se seus atributos forem iguais. Um objeto de valor difere de uma entidade por não ter um conceito de identidade.

Por exemplo, se considerarmos uma propriedade TempoGasto como um objeto de valor, um tempo gasto de 60 segundos seria igual à duração de um minuto, pois o valor subjacente é o mesmo.

Essa característica pode ser mais fácil de implementar em alguns idiomas do que em outros. No C#, precisamos redefinir os métodos equals e hashCode.

2. Imutabilidade

A imutabilidade é um requisito importante. Os valores de um objeto de valor devem ser imutáveis depois que o objeto for criado. Portanto, quando o objeto é construído, você deve fornecer os valores necessários, mas não deve permitir que eles sejam alterados durante o tempo de vida do objeto.

Uma vez criado, um objeto de valor deve ser sempre igual. A única maneira de alterar seu valor é por substituição completa. O que isso significa, no código, é criar uma nova instância com o novo valor.

Ao implementar um Value Object, precisamos nos certificar de que removemos todos os setters e que os getters retornam objetos ou cópias imutáveis para garantir que ninguém possa alterar esses valores externamente.

3. Auto Validação

Um Value Object deve verificar a validade de seus atributos ao ser criado. Se algum de seus atributos for inválido, o objeto não deve ser criado e um erro ou exceção deve ser gerado. Por exemplo, se tivermos um conceito de Idade, não faria sentido criar uma instância de idade com um valor negativo.

Implementando um Value Object

Podemos usar uma abordagem definindo uma classe base contendo as definições comuns conforme o código abaixo:

 public abstract class ValueObject<T> where T : ValueObject<T>
 {
        public override bool Equals(object obj)
        {
            var valueObject = obj as T;
            if (ReferenceEquals(valueObject, null))
                return false;
            if (GetType() != obj.GetType())
                return false;
            return EqualsCore(valueObject);
        }
        protected abstract bool EqualsCore(T other);
        public override int GetHashCode()
        {
            return GetHashCodeCore();
        }
        protected abstract int GetHashCodeCore();
        public static bool operator ==(ValueObject<T> a, ValueObject<T> b)
        {
            if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
                return true;
            if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
                return false;
            return a.Equals(b);
        }
        public static bool operator !=(ValueObject<T> a, ValueObject<T> b)
        {
            return !(a == b);
        }
    }

Neste código temos o tratamento das partes repetidas dos membros de igualdade e o fornecimento de pontos de extensão para classes derivadas para fazer a comparação real e o cálculo do código hash.

Assim uma propriedade Endereco que é definida como Value Object pode derivar desta classe base:

 public class Endereco : ValueObject<Endereco>
 {
        public string Rua { get; }
        public string Cidade { get; }
        public string Cep { get; }

        public Endereco(string rua, string cidade, string cep)
        {
            Rua = rua;
            Cidade = cidade;
            Cep = cep;
        }

        protected override bool EqualsCore(Endereco other)
        {
            return Rua == other.Rua
                && Cidade == other.Cidade
                && Cep == other.Cep;
        }

        protected override int GetHashCodeCore()
        {
            unchecked
            {
                int hashCode = Rua.GetHashCode();
                hashCode = (hashCode * 397) ^ Cidade.GetHashCode();
                hashCode = (hashCode * 397) ^ Cep.GetHashCode();
                return hashCode;
            }
        }
    }

Note que a classe base oculta a maior parte do código comum relacionado às operações de comparação.

Nesta abordagem basta declarar dois métodos:

  1. EqualsCore()
  2. GetHashCodeCore()

É claro que você pode alterar o código implementando mais responsabilidades.

Assim podemos usar Value Objects nos seguintes cenários:

  1. Evitar a Obsessão por primitivos 
  2. Segurança de Tipo
  3. Ter mais flexibilidade
  4. Ter uma validação mais consistente
  5. Reduzir a duplicação de código
  6. Facilitar a leitura do código
  7. Melhorar o desempenho
  8. Usar o Hashing

Agora você tem que considerar que como são objetos imutáveis os Value Objects podem não interagir muito bem com banco de dados, e assim talvez você tenha que relaxar as restrições de imutabilidade ou usar um objeto de transferência de dados(DTO) separado e fazer a conversão entre os objetos o que não é uma solução  ideal, pois adiciona ainda mais classes ao seu projeto.

"Porque, se alguém cuida ser alguma coisa, não sendo nada, engana-se a si mesmo. Mas prove cada um a sua própria obra, e terá glória só em si mesmo, e não noutro."
Gálatas 6:3,4

Referências:


José Carlos Macoratti