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).
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
CurtirCurtir
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
CurtirCurtir
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.
CurtirCurtir
Exato!
No fim, acho que nem compensa pensar muito nelas.
Quem sabe um dia vejo utilidade 🙂
CurtirCurtir
Excelente artigo Robson! Estava com dúvidas nos conceitos de Value Objects e agora tenho uma visão muito melhor. Obrigado!
CurtirCurtir
Obrigado, Iago!
Que bom que foi útil.
[]s
CurtirCurtir
Estou lendo o livro do Vaughn Vernon e ele explica exatamente como você, ou vice-versa, parabéns pelo post, muito bem explicado.
CurtirCurtir
Obrigado, Roger.
Este é um ótimo livro de referencia (não apenas de DDD, mas de software no geral).
[]s
CurtirCurtir
Uma dúvida, enums são VOs? EX: enum Sexo { Masculino = 1, Feminino = 2}
CurtirCurtir
Enums são….enums! 🙂
VOs são mais ricos, como no artigo, embora você possa implementa-los com valores delimitados (tipo um enum com comportamento), como aqui: https://lostechies.com/jimmybogard/2008/08/12/enumeration-classes/.
Qualquer coisa, estamos aí!
[]s!
CurtirCurtir
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 ?
CurtirCurtir
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
CurtirCurtir
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
CurtirCurtir
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
CurtirCurtir
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.
CurtirCurtir
Valeu por compartilhar, Roger
[]s
CurtirCurtir