DEV Community

Cover image for Using Value Objects in Ruby on Rails
Vinicius Porto
Vinicius Porto

Posted on

Using Value Objects in Ruby on Rails

In this article, we explore the concept of Value Objects in Ruby on Rails and how they can be used to encapsulate business logic and improve the maintainability of your code. We start by explaining what a Value Object is and how it differs from a Rails Model, before diving into how to create custom Value Objects in Ruby. We also provide an example of how to use a custom Value Object as an attribute in a Rails Model, along with the necessary configuration and implementation details. Finally, we discuss how to organize Value Objects within a Rails application for maximum clarity and ease of use.

Can a Rails Model be Considered a Value Object?

No, a Rails Model is not the same thing as a Value Object. A Value Object is a small, immutable object that represents a simple value or concept, such as a name, address, or monetary amount. In contrast, a Rails Model is a more complex object that represents a database table and includes functionality for querying and updating the associated records.

Can We Use Value Object Concepts in a Rails Model?

Yes, you can use Value Object concepts in a Rails Model by creating custom Value Objects and using them as attributes in your Model. This allows you to encapsulate business logic in a separate object and keep your Model lean and focused on database-related concerns.

Should We Create Separate Models or Use Value Objects?

It depends on the complexity of the business logic you need to represent. If the business logic involves complex relationships or requires extensive querying and updating of associated records, it may be better to create separate Models. If the business logic is relatively simple and can be represented as a standalone value or concept, it may be better to use a Value Object.

Creating a Value Object

Here is an example of how you might create a Value Object for representing monetary amounts:

class Money
  attr_reader :amount, :currency

  def initialize(amount, currency = 'USD')
    @amount = amount
    @currency = currency
  end

  def add(other)
    raise "Mismatched currency" unless currency == other.currency

    Money.new(amount + other.amount, currency)
  end

  # Other methods for performing arithmetic operations, formatting output, etc.
end
Enter fullscreen mode Exit fullscreen mode

In this example, we define a Money class that represents a monetary amount with an associated currency. The class includes methods for performing arithmetic operations and other functionality related to monetary amounts.

Another example is a Value Object to represents an address:

class Address
    attr_reader :street, :number, :complement, :city, :state, :zip_code

    def initialize(street:, number:, complement:, city:, state:, zip_code:)
      @street = street
      @number = number
      @complement = complement
      @city = city
      @state = state
      @zip_code = zip_code
    end

    def eql?(other)
      return false unless other.is_a?(Address)
      street == other.street &&
        city == other.city &&
        state == other.state &&
        zip_code == other.zip_code
    end
    alias_method :==, :eql?

    def hash
      [street, number, complement, city, state, zip_code].hash
    end

    def to_s
      "#{street}, #{number}, #{complement} - #{city}/#{state}, #{zip_code}"
    end
end

Enter fullscreen mode Exit fullscreen mode

Using a Value Object in a Rails Model

Here is an example of how you might use the Address Value Object as an attribute in a Rails Model:

class Order < ApplicationRecord
    attribute :shipping_address, AddressType.new  
end
Enter fullscreen mode Exit fullscreen mode

using this method you need to make some aditional configurations on config/initializers/types.rb to register the new custom type like:

ActiveSupport.on_load(:active_record) do
    ActiveRecord::Type.register :address_type, AddressType
end
Enter fullscreen mode Exit fullscreen mode

you still have to create a class, that inherits from ActiveRecord::Type::Value , that describes the convertion of the Address value object into a really usable type.

class AddressType < ActiveRecord::Type::Value
  def type
    :jsonb
  end

  def cast(value)
    return if value.blank?
    if value.is_a?(Address)
      value
    elsif value.is_a?(Hash)
      Address.new(value.symbolize_keys)
    else
      raise ArgumentError, "Invalid address value: #{value.inspect}"
    end
  end

  def serialize(value)
    return nil if value.blank?

    value.to_s
  end

  def deserialize(value)
    value
  end

  def ==(other)
    other.is_a?(AddressType)
  end
end
Enter fullscreen mode Exit fullscreen mode
  • In the type method we are defining the real type that ‘ll be saved on the database
  • In the cast method has the responsability to casts a value from user input (e.g. from a setter). This value may be a string from the form builder, or a ruby object passed to a setter. There is currently no way to differentiate between which source it came from. The return value of this method will be returned from [ActiveRecord::AttributeMethods::Read#read_attribute](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Read.html#method-i-read_attribute).
  • The serialize method casts a value from the ruby type to a type that the database knows how to understand. The returned value from this method should be a String, Numeric, Date, Time, Symbol, true, false, or nil .
  • and the deserialize method converts a value from database input to the appropriate ruby type. The return value of this method will be returned from ActiveRecord::AttributeMethods::Read#read_attribute. The default implementation just calls [Value#cast](https://api.rubyonrails.org/classes/ActiveModel/Type/Value.html#method-i-cast) .

Organizing Value Objects in a Rails Application

To organize your Value Objects in a Rails application, you can create a separate directory for them within the app directory. Here is an example directory structure:

app/
  ├── controllers/
  ├── models/
  │   ├── order.rb
  ├── value_objects/
  │   └── money.rb
      └── address.rb
Enter fullscreen mode Exit fullscreen mode

In this example, we create a value_objects directory within the app directory to store our custom Value Objects. We then create a money.rb file within the value_objects directory to define our Money Value Object.

We can also use composed_of to use the value object

You can use the composed_of method in Rails to simplify the use of the Money value object in the Order model.

Here’s an example of how you could use composed_of:

class Order < ApplicationRecord
  composed_of :total_value,
              class_name: 'Money',
              mapping: %w[total_value amount currency],
              converter: ->(value) { Money.new(value.amount, value.currency) },
              allow_nil: true
end
Enter fullscreen mode Exit fullscreen mode

In this example, the composed_of method is used to define a total_value attribute in the Order model. The :class_name option specifies the name of the value object class (in this case, Money). The :mapping option maps the total_value attribute to the cents attribute of the Money object. The :converter option specifies a lambda that converts the total_value attribute to a Money object. The :allow_nil option specifies that the total_value attribute can be set to nil.

With this setup, you can treat the total_value attribute as a Money object in your code, like this:

order = Order.new(total_value: Money.new(100, 'usd'))
order.total_value # returns a Money object with usd100
Enter fullscreen mode Exit fullscreen mode

This approach allows you to abstract away the details of how the Money object is stored in the database and provides a simpler interface for working with the total_value attribute.

Conclusion

In conclusion, using value objects in a Ruby on Rails application can help you encapsulate business logic in a separate object and keep your models focused on database-related concerns. By using value objects to represent simple values or concepts, such as monetary amounts or addresses, you can make your code more readable, testable, and maintainable.

Creating a value object is relatively easy in Ruby, and you can use it as an attribute in your Rails models. You can also create custom types to handle the conversion of your value object to a database column. By organizing your value objects in a separate directory, you can make it easier to maintain and reuse them across your application.

references:

Top comments (2)

Collapse
 
andreimaxim profile image
Andrei Maxim

In contrast, a Rails Model is a more complex object that represents a database table and includes functionality for querying and updating the associated records.

There really isn't anything about the app/models folder that forces you to store there only objects that inherit from ActiveRecord. That folder is actualy meant as a home for all types of models, regardless if they are backed up by a database table or not.

Obviously, if you feel like it makes more sense for you or your team to put value objects in a separate folder, like you did in that example.

Collapse
 
feliperaul profile image
flprben

Really great article.

It would be nice if you could add a section on serialize with a custom coder, which seems to be a valid approach as well (see gorails.com/episodes/custom-active...).

It looks to me that serialize with a custom coder can achieve pretty much the same as the attribute with a custom ActiveRecord::Type.

The composed_of, however, has the added benefit of being able to create a value object that (as the name implies) is composed of multiple columns of the model, instead of a single one, so it's more versatile.