Back-End

1 fev, 2017

Aprendendo sobre objetos de valor

Publicidade

Quando programo, sempre acho útil representar as coisas como um composto. Uma coordenada 2D consiste de um valor X e um valor Y. Uma quantidade de dinheiro consiste de um número e uma moeda. Um intervalo de datas consiste de uma data de início e uma data de fim, que podem ser compostas de dia, mês e ano.

Conforme eu faço isso, chego à pergunta se dois objetos compostos são os mesmos. Se eu tenho dois objetos de pontos que representam as coordenadas cartesianas de (2,3), faz sentido tratá-los como iguais. Os objetos que são iguais devido aos valores de suas propriedades, nesse caso suas coordenadas x e y, são chamados objetos de valor.

Mas, a menos que eu seja cuidadoso ao programar, eu posso não alcançar esse comportamento em meus programas.

Digamos que eu queira representar um ponto em JavaScript:

const p1 = {x: 2, y: 3};
const p2 = {x: 2, y: 3};
assert(p1 !== p2);  // NOT what I want

Infelizmente, esse teste passa. Isso acontece porque o JavaScript testa a igualdade de seus objetos olhando para suas referências, ignorando os valores que eles contêm.

Em  muitas situações, utilizar referências em vez de valores faz sentido. Se estou carregando e manipulando várias ordens de compras, faz sentido carregar cada ordem em um espaço único. Se então eu precisar ver se a última ordem da Alice está na próxima entrega, posso pegar a referência de memória ou identidade da ordem da Alice e ver se essa referência está na lista de ordens de entrega. Para esse teste, não tenho que me preocupar sobre o que tem na ordem. De maneira similar, eu poderia contar com um único número de ordem, testar para ver se o número da ordem da Alice está na lista de entregas.

Embora eu acho útil pensar em duas classes de objeto: objetos de valor e objetos de referência, dependendo de como eu as distinguir [1]. Eu preciso garantir que eu saiba exatamente como eu espero que cada objeto lide com a igualdade e programá-los para se comportar exatamente como eu espero. Como eu faço isso depende da linguagem com a qual estou trabalhando.

Algumas linguagens tratam todos os dados compostos como valores. Se eu fizer uma composição simples em Clojure, vai ficar assim:

> (= {:x 2, :y 3} {:x 2, :y 3})
true

Esse é o estilo funcional – tratar tudo como valores imutáveis.

Mas se não estou utilizando uma linguagem funcional, ainda posso criar objetos de valor. Em Java, por exemplo, a classe de ponto padrão se comporta como eu gostaria.

assertEquals(new Point(2, 3), new Point(2, 3)); // Java

O jeito como isso funciona é que a classe de ponto sobrescreve o método equals padrão com testes para os valores.[2][3]

Eu posso fazer algo similar em JavaScript

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  equals (other) {
    return this.x === other.x && this.y === other.y;
  }
}

const p1 = new Point(2,3);
const p2 = new Point(2,3);
assert(p1.equals(p2));

O problema com o JavaScript aqui é que esse método equals que eu defini é um mistério para qualquer outra biblioteca JavaScript.

const somePoints = [new Point(2,3)];
const p = new Point(2,3);
assert.isFalse(somePoints.includes(p)); // not what I want

//so I have to do this
assert(somePoints.some(i => i.equals(p)));

Isso não é um problema em Java porque Object.equals é definido na biblioteca principal e todas as outras bibliotecas o utilizam para comparações (== normalmente é utilizado somente para primitivos).

Uma das boas consequências dos objetos de valor é que eu não preciso me preocupar se eu tenho uma referência para o mesmo objeto na memória ou uma referência diferente com um valor igual. No entanto, se eu não tomar cuidado, essa feliz ignorância pode levar a um problema, que eu vou ilustrar com um pouco de Java.

Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));

// this means we need a retirement party
Date partyDate = retirementDate;

// but that date is a Tuesday, let's party on the weekend
partyDate.setDate(5);

assertEquals(new Date(Date.parse("Sat 5 Nov 2016")), retirementDate);
// oops, now I have to work three more days :-(

Esse é um exemplo de um erro de alias, eu mudo a data em um lugar, e tem consequências além do que esperava[4]. Para evitar esses erros, eu sigo uma regra simples mas importante: objetos de valor devem ser imutáveis. Se eu quiser mudar minha data, eu crio um novo objeto.

Date retirementDate = new Date(Date.parse("Tue 1 Nov 2016"));
Date partyDate = retirementDate;

// treat date as immutable
partyDate = new Date(Date.parse("Sat 5 Nov 2016"));

// and I still retire on Tuesday
assertEquals(new Date(Date.parse("Tue 1 Nov 2016")), retirementDate);

É claro que fica muito mais fácil tratar os objetos como imutáveis se eles realmente são imutáveis. Com os objetos, eu posso normalmente fazer isso simplesmente não fornecendo métodos de configuração. Então, minha classe JavaScript de antes ficaria assim[5]:

class Point {
  constructor(x, y) {
    this._data = {x: x, y: y};
  }
  get x() {return this._data.x;}
  get y() {return this._data.y;}
  equals (other) {
    return this.x === other.x && this.y === other.y;
  }
}

Enquanto a imutabilidade é minha técnica favorita para evitar esses erros, também é possível evitá-los garantindo que as tarefas sempre tenham uma cópia. Algumas linguagens fornecem essa habilidade, como as structs em C.

Tanto tratar um conceito como um objeto de referencia ou objeto de valor vão depender do seu contexto. Em muitas situações, vale a pena tratar um endereço postal como uma simples estrutura de texto com igualdade de valor. Mas um sistema de mapeamento mais sofisticado pode ligar os endereços postais em um modelo hierárquico sofisticado onde as referências fazem mais sentido. Como com a maioria dos problemas de modelagem, diferentes contextos levam a diferentes soluções[6].

Normalmente, é uma boa ideia substituir os primitivos comuns, como as strings, pelos objetos de valor adequados. Enquanto eu posso representar um número de telefone como uma string, transformá-lo em objeto número de telefone torna as variáveis e parâmetros mais explícitos (com verificação de tipo, quando a linguagem suportar), um foco natural por validação, e evitando comportamentos inaplicáveis (com realizar cálculos com números de identificação inteiros).

Pequenos objetos, como pontos, moedas ou extensões, são bons exemplos de objetos de valor. Mas estruturas maiores podem frequentemente ser programadas como objetos de valor se elas não tiverem identidades conceituais ou não precisarem compartilhar referências pelo programa. Esse é o ajuste mais natural com linguagens funcionais do que o padrão de imutabilidade.

Eu acho que os objetos de valor, particularmente os pequenos, normalmente são negligenciados – vistos como triviais demais para valerem a pena serem analisados. Mas como eu já vi muitos, acho que posso criar um bom comportamento com eles. Para experimentar, tente utilizar uma Range Class e veja como ela evita todos os tipos de perda de tempo com atributos iniciais e finais duplicados. Eu geralmente busco em bases de código onde objetos de valor de domínio específicos como esse podem agir com foco na refatoração, levando a uma drástica simplificação de um sistema. Tal simplificação geralmente surpreende as pessoas, até elas a verem algumas vezes.

Reconhecimentos

James Shore, Beth Andres-Beck e Pete Hogson compartilharem suas experiências com JavaScript.

Graham Brooks, James Birnie , Jeroen Soeters, Mariano Giuffrida, Mateo Vaccari e Steven Lowe forneceram valiosos comentários em nossa lista de mailing interno.

Leituras futuras

A descrição de Vaughn Vernon é provavelmente a melhor discussão sobre os objetos de valor de uma perspectiva DDD. Ele cobre como decidir entre valores e entidades, dicas de implementação e técnicas de persistência dos objetos de valor.

O termo começou a ganhar força nos primeiros anos. Dois livros que falam sobre eles desde aquele tempo são PoEAA e DDD. Existem também algumas discussões interessantes na Ward’s Wiki.

Uma fonte de confusão terminológica é que por volta da virada do século algumas literaturas da J2EE utilizaram “objeto de valor” para Objeto de Transferência de Dados. Essa utilização quase desapareceu agora, mas você pode encontrá-la ainda.

Notas

[1] No Desenvolvimento Orientado a Domínio (DDD em inglês), a classificação de Evans contrasta objetos de valor com entidades. Eu considero as entidades como uma forma comum de objetos de referência, mas utilizo o termo “entidade” somente dentro de modelos de domínio enquanto o objeto de referência/valor é útil para todos os códigos.

[2] Estritamente isso é feito em awt.geom.Point2D, que é uma superclasse de awt.Point.

[3] A maioria das comparações em Java é feita com equals –  o que é, por si só, um pouco estranho, pois eu tenho que lembrar de utilizar de utilizá-lo em vez do operador ==. Isso incomoda, mas os programadores Java logo se acostumam a isso, pois uma string se comporta do mesmo jeito. Outras linguagens orientadas a objeto evitam isso. O Ruby utiliza o operador == , mas permite que seja sobrescrito.

[4] Existe uma competição robusta pela pior funcionalidade de data e hora do sistema do pré-Java 8 – mas meu voto seria esse. Ainda bem que agora podemos evitar a maioria disso agora com o pacote java.time.

[5] Isso não é estritamente imutável, desde que um cliente possa manipular a propriedade _data. Mas uma equipe adequadamente disciplinada pode tornar o imutável uma prática. Se eu estivesse preocupado que uma equipe não fosse disciplinada o suficiente, eu poderia utilizar o freeze. Na verdade, posso somente utilizar o freeze em um objeto JavaScript, mas prefiro uma classe com os assessores explicitamente declarados.

[6] Existe mais discussão no livro sobre DDD de Evans.

[7] Imutabilidade é valiosa para os objetos de referência também – se uma ordem de venda não muda durante a solicitação, então torná-la imutável é de grande valor; e isso tornaria seguro criar uma cópia, se for útil. Mas isso não faria a ordem de venda ser um objeto de valor se estou determinando a igualdade baseado em um único número de ordem.

***

Martin Fowler faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://martinfowler.com/bliki/ValueObject.html.