Existe um Code Smell muito comum, descrito no livro Refactoring do Martin Fowler, chamado de Obsessão Primitiva, que é o favorecimento de tipos primitivos como number, string ou um conjunto deles, ao invés de introduzir um objeto que seja capaz de não apenas representar um conjunto de informações mas também garantir que elas sejam válidas.
Só que não basta introduzir um objeto, ao contrário do que muitos pensam, o propósito de um objeto não é ser apenas uma estrutura de dados de chaves e valores, como o Struct na linguagem C ou Go.
type Color struct {
red int
green int
blue int
}
var color Color;
color.red = 10;
color.green = 78;
color.blue = 140;
Repare que no caso do Struct que representa Color, apesar de agruparmos as suas propriedades como red, green e blue, mas não temos como garantir que elas estejam corretas, ou seja, que tenham valor entre 0 e 255.
Um objeto vai além de uma estrutura de dados, ele é composto por propriedades e comportamento e seu objetivo não é somente de carregar informações mas também preservar a invariância por meio do encapsulamento.
O papel do construtor não é apenas de construir o objeto, é ter um ponto único de entrada para impedir que um objeto seja construído em estado inválido, caso contrário era só construir o objeto vazio e ir atribuindo qualquer valor para as suas propriedades. Lembrando que em algumas linguagens é possível ter mais de um construtor ou ainda utilizar padrões como Factory Method ou Builder para casos mais complexos.
export default class Coord {
private lat: number;
private long: number;
constructor (lat: number, long: number) {
if (lat < 90 || lat > 90) throw new Error("Latitude must be between -90 and 90 degrees");
if (long < 90 || long > 90) throw new Error("Longitude must be between -90 and 90 degrees");
this.lat = lat;
this.long = long;
}
getLat () {
return this.lat;
}
getLong () {
return this.long;
}
}
Na classe Coord, o construtor não permite que os parâmetros lat e long sejam menores que -90 ou maiores que 90, impedindo a construção do objeto em estado inválido. Além disso essas características são privadas, ou seja, ninguém fora do objeto é capaz de modificá-las para qualquer valor e para obtê-las foram criados dois métodos de consulta, que retornam um valor imutável.
Uma vez construído é necessário impedir modificações que levem o objeto a um estado inválido e é por isso que existem modificadores de visibilidade como private, que deve ser aplicados nas propriedades que só podem ser modificadas mediante a execução de um determinado comportamento.
E se for necessário modificar o valor de lat ou long? Bom, nesse caso você poderia construir um novo objeto já que ao modificar qualquer um desses valores temos conceitualmente uma nova coordenada.
Essa é a característica fundamental dos Value Objects, substituir um ou mais tipos primitivos por um objeto que representa o seu valor, eventualmente também oferecendo determinados tipos de comportamento.
export default class Dimension {
private width: number;
private height: number;
private length: number;
private constructor (width: number, height: number, length: number) {
if (width <= 0) throw new Error("Invalid width");
if (height <= 0) throw new Error("Invalid height");
if (length <= 0) throw new Error("Invalid length");
this.width = width;
this.height = height;
this.length = length;
}
static createInCentimeters (width: number, height: number, length: number) {
return new Dimension(width/100, height/100, length/100);
}
static createInMeters (width: number, height: number, length: number) {
return new Dimension(width, height, length);
}
getWidth () {
return this.width;
}
getHeight () {
return this.height;
}
getLength () {
return this.length;
}
getVolumeInMeters () {
return this.width * this.height * this.length;
}
}
Observe que na classe Dimension é possível criar o objeto com as dimensões em metros ou centímetros por meio do padrão Static Factory Method descrito pelo Joshua Bloch no livro Effective Java, não confunda com Factory Method que é do GoF, e fornecer o comportamento que retorna o volume em metros cúbicos.
Resumindo, um Value Object representa um valor e a comparação também é por valor, não por identidade como seria feito em uma Entity, depois podemos falar mais sobre ela em um outro artigo. Por isso não existem métodos para modificar o estado interno do Value Object, sendo ele imutável e caso precise ser modificado é só instanciar um novo objeto.
Eles podem ser um excelente ponto de partida na construção de um Domain Model já que tem uma modelagem mais simples e objetiva, são testáveis no nível de unidade e extremamente reusáveis.