Trabalhando com Value Objects

Em design de software, é comum termos objetos que simplesmente fazem alguma coisa ou caracterizam alguma outra coisa. Eles mais se assemelham a um valor (como DateTime ou string) do que a um objeto propriamente dito, com estado e um ciclo de vida.

Identificar esse tipo de objeto, conhecido por Value Object, em nosso design, pode ajudar a simplifica-lo.

Este artigo faz uma introdução ao assunto.

O QUE SÃO

De forma sucinta, podemos dizer que Value Objects são objetos sem identidade conceitual e que dão característica a algum outro objeto. Em geral, estamos interessados no que eles fazem e não em quem eles são.

Um exemplo clássico seria criarmos um objeto para representar dinheiro em nossa aplicação. Em geral, um objeto Dinheiro de valor R$ 10,00 é igual a outro objeto Dinheiro de mesmo valor, ou seja, não importa de qual instância estamos falando: R$ 10,00 são R$ 10,00. Por isso, eles não possuem identidade conceitual. O que importa são seus atributos.

Outros exemplos de conceitos que poderiam ser modelados como Value Objects seriam: CPF, telefone e endereço.

CARACTERÍSTICAS

Vamos ver uma forma de implementar o Value Object Dinheiro acima mencionado e com isso apresentar mais alguma características importantes desse tipo de objeto.

public class Dinheiro
{
    public string Moeda { get; private set; }
    public decimal Valor { get; private set; }

    public Dinheiro(string moeda, decimal valor)
    {
        // validar parametros...
        Moeda = moeda;
        Valor = valor;
    }

    public Dinheiro SomarCom(Dinheiro dinheiro)
    {
        return new Dinheiro(dinheiro.Moeda, Valor + dinheiro.Valor);
    }
}

Com o pequeno código acima, já podemos visualizar outras características dos VOs.

Um delas é que um VO pode ser usado para agrupar informações relacionadas em um único conceito (Conceptual Whole). Notem que Dinheiro agrupa dois atributos fortemente relacionados: Moeda e Valor.

Outra característica é a imutabilidade: os atributos do objeto nunca são alterados (percebam os setters privados e ausência de métodos que alterem o estado do objeto). “Em português”, R$ 10,00 é e sempre será R$ 10,00. Não faz sentido alterar a moeda ou o valor. R$ 10,00 não “vira” R$ 15,00 ou US$ 10,00, caso contrário, seria outro dinheiro.

Observem que a imutabilidade também é garantida por meio de funções sem efeitos colaterais. O método SomarCom não altera o estado do objeto. Ao invés disso, retorna um novo objeto com o valor da soma.

IGUALDADE

Agora, outro detalhe importantíssimo. Já que um VO não possui identidade conceitual, como garantir que um objeto Dinheiro contendo R$ 10,00 seja considerado IGUAL a outro objeto de mesmo valor?

Em outras palavras, como garantir que o teste abaixo passe?

[Test]
public void Deve_comparar_dois_valores()
{
    var dinheiro = new Dinheiro("R$", 10.00);
    var outroDinheiro = new Dinheiro("R$", 10.00);
    Assert.IsTrue(dinheiro.Equals(outroDinheiro));
}

Apesar de terem os mesmos valores, “dinheiro” e “outroDinheiro” ainda são dois objetos distintos em memória. Sendo assim, o teste inicialmente falha. Para fazermos o teste passar, expressando realmente o conceito de VO, devemos sobrescrever Equals:

public class Dinheiro
{
    // (restante do código)
    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj)) return true;
        var dinheiro = obj as Dinheiro;
        if (ReferenceEquals(null, dinheiro)) return false;
        return (Moeda == dinheiro.Moeda && Valor == dinheiro.Valor);
    }

    public override int GetHashCode()
    {
        return Moeda.GetHashCode() ^ Valor.GetHashCode();
    }
}

Basicamente, Equals checa três coisas: se são o mesmo objeto, se são objetos do mesmo tipo e se todos os atributos são iguais. Ao rodar o teste anterior, ele deverá passar (experimente testar com moeda e valores diferentes). Vejam também o uso de todos os atributos para obtenção do hashcode do objeto.

É uma boa ideia, porém não obrigatória, sobrescrever os operadores == e != (fica como exercício para vocês!).

VALUE OBJECTS E DOMAIN-DRIVEN DESIGN

O padrão Value Object é parte fundamental de Domain-Driven Design, como um dos seus “building-blocks”. Isso significa que não temos que limitar o uso de VOs para conceitos mais genéricos como CPF e Dinheiro. Podemos modelar nosso domínio utilizando VOs para descrever conceitos importantes referentes ao problema de negócio em questão.

Supondo que nosso domínio seja financiamento de imóveis, poderíamos modelar o conceito de “Prestação” como um Value Object, composto pelos atributos “Amortização” e “Juros”, para tornar explícito o conceito de prestação e facilitar a manipulação dos atributos “Amortização” e “Juros”. Imaginem ficar passando o par “Amortização”/”Juros” para todo lugar da aplicação?

VANTAGENS E DESVANTAGENS

O uso de VOs tem uma série de vantagens:

– Proporcionam o uso de orientação a objetos de verdade, encapsulando comportamento em um objeto ao invés de trabalharmos com tipos primitivos e espalharmos regras em diversos pontos da aplicação (como em métodos de extensão, por ex.);
– Como são imutáveis, dois objetos podem compartilhar o mesmo VO sem perigo de uma alteração no VO se propagar para todos eles;
– Também graças à imutabilidade, podemos passá-los como parâmetros de métodos sem o risco de uma alteração indevida pelo método-cliente;
– Tornam explícitos conceitos do domínio antes escondidos em atributos relacionados;
– São simples de implementar e testar.

Como fazer as coisas bem feitas dá trabalho, podemos dizer que a “desvantagem” do padrão não está propriamente em usá-lo e sim em identificar objetos na aplicação que possam ser implementados como um Value Object.

É preciso entender bem o problema, discutir e sempre questionar se o objeto é algo que define algum outro objeto e não possui identidade conceitual ou se realmente estamos interessados em diferenciar cada instância e acompanhar o ciclo de vida do objeto (o objeto é mutável ao longo do tempo).

CONCLUSÃO

Neste artigo, vimos um pouco sobre o padrão Value Object, um importante padrão a ser considerado no design do software, fácil de ser implementado e que pode evitar dores de cabeça no design. Conhecemos suas principais características com um exemplo simples de código e vimos suas principais vantagens.

Para aqueles que quiserem aprofundar mais no assunto recomendo a leitura do mesmo nos livros: Patterns of Enterprise Application Architecture (Fowler), Domain-Driven Design (Evans) e Implementing Domain-Driven Design (Vernon).

17 comentários em “Trabalhando com Value Objects

Adicione o seu

  1. Excelente artigo, Robson. Usarei como referência para apresentar o conceito quando precisar.
    Uma provocação que gostaria de lhe fazer é: E quanto ao uso de structs ao invés de classes para representar Value Objects em C#?
    Abraços

    Curtir

    1. Obrigado, Rafael.
      Confesso que, por falta de costume, as structs ficaram esquecidas na minha vida 🙂 .. Me parece que o uso delas é tão especifico que nunca vi necessidade (e nem vi ninguém usando). Nunca li nada sobre elas que me fizesse pensar nelas com mais carinho!
      []s

      Curtir

  2. São uma boa opção para implementação de Value Objects, mas é necessário ter muito cuidado ao usá-las. Eu ainda não tenho opinião formada quanto a usá-las ou não como opção padrão. A impossibilidade de usar herança é o que mais me incomoda.

    Curtir

  3. Excelente artigo Robson! Estava com dúvidas nos conceitos de Value Objects e agora tenho uma visão muito melhor. Obrigado!

    Curtir

  4. Estou lendo o livro do Vaughn Vernon e ele explica exatamente como você, ou vice-versa, parabéns pelo post, muito bem explicado.

    Curtir

  5. Robson gostaria de tirar uma dúvida, eu entendi o conceito de value Object, minha pergunta é a seguinte :
    Suponha que eu tenha um ORM como o entity framework, uma classe cliente onde eu tenha um VO endereço, como vc faz o mapeamento disso para salvar no banco ?

    Curtir

    1. Olá, Roger

      Não saberei te responder assim de “bate-pronto”.
      Só dei algumas brincadas com o EF e faz muito, mas muito tempo que não pego na prática. Não sei como ele está hoje em maturidade para lidar como um modelo rico (não leio muitas coisas boas a esse respeito).

      Em se tratando de ORM, sempre trabalhei com o NHibernate em código de produção e com ele é bem simples fazer esse tipo de mapeamento. Na sua classe de mapeamento do Cliente, teria algo como: Component(c => c.Endereco), e todos os atributos do endereço seria mapeados em colunas na própria tabela do Cliente (você pode, inclusive, configurar no mapeamento qual será o prefixo das colunas do endereço, por exemplo, Endereco_Logradouro, Endereco_CEP,…).

      Sorry!
      []s

      Curtir

      1. Obrigado pela resposta Robson,

        Estava um pouco difícil de encontrar a forma de fazer, mas consegui encontrar, testei, e deu certo.
        Para ficar registrado aqui caso alguém tenha essa mesma dúvida, basta utilizar a tag [Complextype] na classe que é o value object.
        []s

        Curtir

      2. Legal, Roger.

        Obrigado por compartilhar a solução.

        Esse é um dos motivos de eu não gostar do EF. O domínio acaba sujo por anotações por causa da persistência. Se houver outra forma de fazer, por meio de alguma classe de mapeamento, seria o ideal, senão, paciência. 🙂
        []s

        Curtir

      3. Opa,

        Consegui descobrir como fazer via mapeamento, abaixo segue o exemplo :

        public class Customer
        {
        public Customer()
        {
        this.ShippingAddress = new Adress();
        }

        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Adress ShippingAddress { get; set; }
        }

        public class Adress
        {
        public string Street { get; set; }
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
        //Aqui adiciono o meu tipo complexo (VO)
        modelBuilder.ComplexType();

        //Aqui faço o mapeamento da propriedade da VO para a tabela
        modelBuilder.Types().Configure(ctc => ctc.Property(cust => cust.ShippingAddress.Street).HasColumnName(“ShipAddress”));

        base.OnModelCreating(modelBuilder);
        }

        Fica de exemplo as duas formas agora, abraços.

        Curtir

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Blog no WordPress.com.

Acima ↑