Using Value Objects in Ruby on Rails
Tags: designpatterns, rails, programming
Published: 2023-04-01 04:10:00


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:

```ruby
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
```

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:

```ruby
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

```

### 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:

```ruby
class Order < ApplicationRecord
	attribute :shipping_address, AddressType.new  
end
```

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

```ruby
ActiveSupport.on_load(:active_record) do
    ActiveRecord::Type.register :address_type, AddressType
end
```

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.

```ruby
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
```

- 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:

```flow
app/
  ├── controllers/
  ├── models/
  │   ├── order.rb
  ├── value_objects/
  │   └── money.rb
      └── address.rb
```

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:

```ruby
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
```

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:

```ruby
order = Order.new(total_value: Money.new(100, 'usd'))
order.total_value # returns a Money object with usd100
```

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:

- `ActiveRecord::Aggregations::ClassMethods`. Ruby on Rails API Documentation. Retrieved March 31, 2023, from [https://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html](https://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html)
- `ActiveModel::Type::Value`. Ruby on Rails API Documentation. Retrieved March 31, 2023, from [https://api.rubyonrails.org/classes/ActiveModel/Type/Value.html#method-i-serialize](https://api.rubyonrails.org/classes/ActiveModel/Type/Value.html#method-i-serialize)
- Fowler, M. (2003, August 20). ValueObject. Martin Fowler. Retrieved March 31, 2023, from [https://martinfowler.com/bliki/ValueObject.html](https://martinfowler.com/bliki/ValueObject.html)