Cookies
Diese Website verwendet Cookies und ähnliche Technologien für Analyse- und Marketingzwecke. Durch Auswahl von Akzeptieren stimmen Sie der Nutzung zu, alternativ können Sie die Nutzung auch ablehnen. Details zur Verwendung Ihrer Daten finden Sie in unseren Datenschutz­hinweisen, dort können Sie Ihre Einstellungen auch jederzeit anpassen.

Engineering

ValueObjects in Rails ActiveRecord
Minuten Lesezeit
Blog Post - ValueObjects in Rails ActiveRecord
Stephan Epping

Value Objects provide a lot of benefits. For example, encapsulating domain knowledge in such objects may enrich and simplify your domain model at the same time. Their inherent immutability and comparability can make it much easier to reason about your code. Furthermore, they provide a decent home for additional getter methods that are purely based on its own attributes. If you want to learn more about Value Objects and their applications, I recommend reading about Domain-Driven Design (DDD), for example in Eric Evans influential Book.

Along this post I’ll describe three simple techniques to integrate Value Objects with ActiveRecord models:

  1. Using plain old Ruby accessors
  2. Using ActiveRecord’s composed_of
  3. Using a JSON column to back the Value Object’s data

We will apply these techniques to an ActiveRecord model called Invoice, which has a shipping_address as well as a billing_address. We have not yet decided how we will store the addresses in our database, but we want to represent them using a Value Object called Address:

class Address
  attr_reader :street, :postalcode, :city
  def initialize(street, postalcode, city)
    @street, @postalcode, @city = street, postalcode, city
  end

  def to_s
    "#{street}, #{postalcode} #{city}"
  end
end

If the Value Objects need to be comparable, some more boilerplate code is required, but this would exceed the scope of this article. For more information, see this great StackOverflow Answer.

1. Using Plain Old Ruby accessors

We’ll start with our Invoice ActiveRecord model using multiple database fields to store the shipping and billing address:

create_table :invoices do |t|
  t.string :title
  t.numeric :amount
  t.string :billing_street
  t.string :billing_postalcode
  t.string :billing_city
  t.string :shipping_street
  t.string :shipping_postalcode
  t.string :shipping_city

  t.timestamps
end

Using plain old ruby methods (getter and setter) we can easily introduce Value Objects to the Invoice model:

class Invoice < ApplicationRecord
  def billing_address
    Address.new(billing_street, billing_postalcode, billing_city)
  end

  def billing_address=(address)
    self.billing_street = address.street
    self.billing_postalcode = address.postalcode
    self.billing_city = address.city
  end

  def shipping_address
    Address.new(shipping_street, shipping_postalcode, shipping_city)
  end

  def shipping_address=(address)
    self.shipping_street = address.street
    self.shipping_postalcode = address.postalcode
    self.shipping_city = address.city
  end
end

Now we can easily instantiate a new invoice using existing addresses, for example

# setup sample addresses
billing_address = Address.new('Alter Fischmarkt 12', '48143', 'Münster')
shipping_address = Address.new('Pariser Platz', '10117', 'Berlin')

invoice = Invoice.new(
  title: 'Dustbuster 3000',
  amount: 1250.0,
  billing_address: billing_address,
  shipping_address: shipping_address,
)

Pretty nice, huh? Furthermore, we can send intention revealing messages to the invoice, like invoice.billing_address= that scream CHANGE BILLING ADDRESS.

Here are some more uses:

# show address as string
puts invoice.billing_address
# => "Alter Fischmarkt 12, 48143 Münster"
puts invoice.shipping_address
# => "Pariser Platz, 10117 Berlin"

# change shipping_address to billing_address
invoice.shipping_address = invoice.billing_address
puts invoice.shipping_address
# => "Alter Fischmarkt 12, 48143 Münster"

Instead of setting all attributes by hand or implementing a custom method changebillingaddress, we have a real object that we can pass around and contains it’s own behaviour/getters.

2. Using ActiveRecord composed_of

In the previous section you saw how to implement Value Objects in ActiveRecord by hand. But ActiveRecord already provides a neat helper composed_of. Here is the same implementation using composed_of.

# show address as string
puts invoice.billing_address
# => "Alter Fischmarkt 12, 48143 Münster"
puts invoice.shipping_address
# => "Pariser Platz, 10117 Berlin"

# change shipping_address to billing_address
invoice.shipping_address = invoice.billing_address
puts invoice.shipping_address
# => "Alter Fischmarkt 12, 48143 Münster"

While this saves us some boilerplate code and provides some more useful configuration options (see composed_of), the first approach provides us more overall flexibility.

3. Using a JSON column

In some cases a model contains a list of Value Objects (like a list of addresses) or contains many different Value Objects (maybe depending on the type/state of the object). In such a case, we could store the data of all Value Objects in a single database json column instead of creating a lot of bloating columns for each single Value Object. I do not want to discuss database layout/normalisation here, but just want to demonstrate useful (maybe very pragmatic) techniques.

class Invoice < ApplicationRecord
  composed_of :billing_address, class_name: 'Address',
                                mapping: [
                                  %w(billing_street street),
                                  %w(billing_postalcode postalcode),
                                  %w(billing_city city)
                                ]

  composed_of :shipping_address, class_name: 'Address',
                                mapping: [
                                  %w(shipping_street street),
                                  %w(shipping_postalcode postalcode),
                                  %w(shipping_city city)
                                ]
end

While this is pretty similar to the first approach, it gives us much more flexibility in storing the addresses. Instead of having a lot of flat attributes, we could store them in a nested way, e.g. {shippingaddress: {street:, postalcode:, ...}, billingaddress: ...}. Then, we could use something like address.to_h and Address.from_h to serialize/deserialize addresses into/from our data json blob. Furthermore, we can now (dynamically) add new Value Objects to an invoice without adding new database columns.

Summary

Use the Value Object pattern more often, it is a really handy one. It is intention revealing, compounds attributes and due to its immutability, it also guards against some nasty bugs. Some more examples of Value Objects are Money, Temperature or GPS, but it’s really up to you to identify them in your domain. I hope you can apply this pattern in your daily work and find it as helpful as I do.

Du suchst den richtigen Partner für erfolgreiche Projekte?

Lass uns ins Gespräch kommen.