Value Objects no Ruby on Rails

Value Object é um padrão que visa transformar valores primitivos em objetos ricos de domínio. Ele é uma forma de distribuir a complexidade de classes com muitas responsabilidades de forma organizada e encapsulada. Vamos ver uma das formas de implementar Value Objects fornecidas pelo Active Record do Rails com o…

Value Object é um padrão que visa transformar valores primitivos em objetos ricos de domínio. Ele é uma forma de distribuir a complexidade de classes com muitas responsabilidades de forma organizada e encapsulada. Vamos ver uma das formas de implementar Value Objects fornecidas pelo Active Record do Rails com o método composed_of.

Exemplo: Trabalhando com Preço

Value Objects são objetos que se identificam pelo seu conteúdo, e não por um identificador específico.

Vamos supor que estejamos no contexto onde um item de um pedido tenha um valor unitário e pode ser aplicado o desconto, ao invés de aplicar toda a lógica do desconto, validações e cambio de moeda na classe de Pedido, podemos encapsular esse comportamento numa classe específica para o valor unitário.

class ValorUnitario
  attr_reader :valor_unitario, :desconto_unitario, :moeda

  def initialize(valor_unitario, desconto_unitario, moeda)
    @valor_unitario = valor_unitario
    @desconto_unitario = desconto_unitario
    @moeda = moeda
  end

  def aplicar_desconto(desconto_unitario)
    valor_com_desconto = valor_unitario - desconto_unitario
    self.class.new(valor_com_desconto, desconto_unitario, moeda)
  end
end

No código acima a classe que representa o valor unitário só tem seus valores definidos via construtor, caso seja necessária uma alteração nos valores, uma nova classe deve ser inicializada conforme ocorre no método `aplicar_desconto`.

Para usar o value object em models do Rails, podemos usar o método composed_of, onde passamos o nome da classe como symbol, e o framework irá procurar automaticamente a classe ValorUnitario, caso queira definir um nome diferente do da classe, é possível definir explicitamente, como composed_of :preco_unitario, class_name: "ValorUnitario"

composed_of :valor_unitario, mapping: { valor_unitario: :valor_unitario,
                                        desconto_unitario: desconto_unitario,
                                        moeda: :moeda }

Também é possível usar as validações do Rails nos objetos de valor e validá-los nos models, para isso será necessário incluir o ActiveModel::Validations.

class ValorUnitario
  include ActiveModel::Validations

  attr_reader :valor_unitario, :desconto_unitario, :moeda

  validates :valor_unitario, :desconto_unitario, numericality: { greater_than_or_equal_to: 0 }

E no modelo de Pedido utilizar o `validates_associated`:

class Pedido < ApplicationRecord
  composed_of :valor_unitario, mapping: { preco_unitario: :valor_unitario,
                                          desconto_unitario: :desconto_unitario,
                                          moeda: :moeda }

  validates_associated :valor_unitario
end

Com isso, ao tentar salvar um pedido sem valor unitário preenchido vamos receber as mensagens de erro:

pedido = Pedido.new
=> #<Pedido:0x00007065afb691c8 id: nil, valor_unitario: nil, qtde: nil, desconto_unitario: nil, moeda: nil, created_at: nil, updated_at: nil>
(dev)> pedido.save
=> false
(dev)> pedido.errors.messages
=> {:valor_unitario=>["is invalid"]}
(dev)> pedido.valor_unitario.errors.messages
=> {:valor_unitario=>["is not a number"], :desconto_unitario=>["is not a number"]}

Imutabilidade

Idealmente, um Value Object deve ser imutável, ou seja, seus valores não podem ser alterados após serem instanciados. Caso seja feita uma alteração em um dos seus valores, deverá ser criado um novo objeto. Com isso é importante não criar atributos com writer públicos, somente attr_readers. Ao utilizar o composed_of o Active Record congela automaticamente o objeto, então caso um writer seja exposto, ao fazer uma alteração ocorrerá um RunTimeError.

Comparações

Para comparações entre objetos de valor em Ruby é recomendado fazer o include e a sobrescrita dos métodos do mixin Comparable como os métodos == e <=> .

class ValorUnitario
  include Comparable
  include ActiveModel::Validations

  attr_reader :valor_unitario, :desconto_unitario, :moeda

  validates :valor_unitario, :desconto_unitario, numericality: { greater_than_or_equal_to: 0 }

  def initialize(valor_unitario, desconto_unitario, moeda)
    @valor_unitario = valor_unitario
    @desconto_unitario = desconto_unitario
    @moeda = moeda
  end

  def aplicar_desconto(desconto_unitario)
    valor_com_desconto = valor_unitario - desconto_unitario
    self.class.new(valor_com_desconto, desconto_unitario, moeda)
  end

  def ==(other)
    valor_unitario == other.valor_unitario
  end

  def <=>(other)
    valor_unitario <=> other.valor_unitario
  end
end

Buscas em Queries

As queries do Active Record também funcionam com os Value Objects, um exemplo de busca possível é:

teste-rails8(dev)> Pedido.where(valor_unitario: ValorUnitario.new(10.0, 0, "BRL"))
  Pedido Load (0.2ms)  SELECT "pedidos".* FROM "pedidos" WHERE "pedidos"."preco_unitario" = 10.0 AND "pedidos"."desconto_unitario" = 0.0 AND "pedidos"."moeda" = 'BRL' /* loading for pp */ LIMIT 11 /*application='TesteRails8'*/
=> 
[#<Pedido:0x00007f510a14f660
  id: 1,
  preco_unitario: 0.1e2,
  qtde: nil,
  desconto_unitario: 0.0,
  moeda: "BRL",
  created_at: "2024-10-14 04:26:25.606715000 +0000",
  updated_at: "2024-10-14 04:26:25.606715000 +0000">]

Quando utilizar Value Objects?

É possível usar o padrão com atributos comuns, como email e nome. Uma entidade / modelo pode ter vários Value Objects, é recomendado para distribuir a complexidade diminuindo os “fat models” comuns em aplicação Rails com certa complexidade de regra de negócio. Alguns exemplos interessantes são:

  • Um objeto de Valor para Money, recebendo o valor e taxa de câmbio e cuidando da cotação;
  • Temperatura que compara e converte as unidades de medidas como Celsius e Fahrenheit;
  • Endereço: quando temos uma associação 1:1 de usuário com endereço, podemos deixar as colunas na tabela de usuarios, mas criar um Value Object específico para o Endereço.

Conclusão

Value Objects ajudam a encapsular e isolar a lógica relacionada a valores dentro de classes pequenas e reutilizáveis. É um padrão amplamente recomendado em literaturas como Domain Driven Design. No Rails, o uso de composed_of facilita a integração desses objetos nos models, e as validações nativas do Rails podem ser usadas para garantir a consistência desses valores. Essa prática melhora a legibilidade do código, torna-o mais modular e facilita a manutenção, especialmente em domínios ricos em regras de negócio.

Referências

0 Comentário