Inactive records: the value objects your app deserves

By Tim Riley19 Apr 2016

Hop into a typical Rails app’s source code or git history and you’ll see that its ActiveRecord::Base subclasses are a real nexus of behaviour and logic. You’ll likely find yourself reaching over to these classes and making changes as you work on any part of the app. They have a lot of responsibility, and in many ways, they feel like the backbone of a Rails app.

These classes also have another characteristic that can spread design difficulties throughout your app: they have mutable state. Your ActiveRecord subclasses can:

  • Be created brand new with empty attributes,
  • Come freshly loaded from the database,
  • Have their attributes modified in memory but not yet persisted to the database,
  • Hold invalid attributes,
  • Have their associations preloaded from the database…
  • Or not!
  • Or even have new associations built in memory but not yet persisted to the database.

This is a scary list. If you’re working with an ActiveRecord object, you never really know what you’re working with.

These issues don’t originate in the ActiveRecord library alone. They come part and parcel along with the “active record” pattern on which the library is modelled. As Bob Martin puts it:

It creates confusion about these two very different styles of programming. A database table is a data structure. It has exposed data and no behavior… [But] many people put business rule methods in their Active Record classes; which makes them appear to be objects. This leads to a dilemma. On which side of the line does the Active Record really fall? Is it an object? Or is it a data structure?

This conflation of roles leads to incredibly complex behaviour and an enormous amount of responsibility situated in a very small number of classes at the core of your app. This is not what we want.

As I mentioned in my last post, we’ve switched to ROM for database persistence in our apps. But this doesn’t mean we’ve stopped modelling our entities entirely. Instead, we’ve taken these previously active records and made them inactive. We’ve turned them into value objects.

What’s a value object? A value object is one whose notion of equality is based on the values of the attributes it contains, not its object identity. Most importantly, a good value object is immutable: you cannot change any attribute within an existing value object.

Many of the definitions for value objects use examples like monetary values or date ranges. However, this is not the extent of their usefulness for modelling data. In our apps, some of the things we have as value objects are Article, ArticleWithContributors, and Category. These are the app’s main domain entities, and exactly the kind of things you might otherwise expect to find as ActiveRecord subclasses.

Building value objects in Ruby is straightforward. We need to satisfy these two requirements:

  1. Make the objects immutable. Don’t provide attr writers or any other methods to mutate the object’s internal state.
  2. Provide equality methods that use the object’s attributes.

Here’s how a simple one looks:

require "dry-equalizer"

class Article
  include Dry::Equalizer(:id, :title, :body)

  attr_reader :id, :title, :body

  def initialize(attrs = {})
    @id = attrs.fetch(:id)
    @title = attrs.fetch(:title)
    @body = attrs.fetch(:body)

We build a lot of these, so we use dry-types to reduce boilerplate and give us stricter type expectations (more on that in a future article):

require "types"

class Article < Dry::Types::Struct
  attribute :id, Types::Strict::Int
  attribute :title, Types::Strict::String
  attribute :body, Types::Strict::String

An added bonus of a value object made using a Dry::Types::Struct is that it cannot be initialized with invalid data. In this case, an Article is an Article is an Article. It’ll work as advertised, every single instance, every single time.

(Technically, these examples are entities rather than strict value types, because we include the record IDs in their attributes, which you’d expect to be the key indicator of equality. However, in practice, Dry::Types::Struct instances still check for equality across all attributes, and in our apps we only ever instantiate these objects from known-good database data, which means they still behave effectively as value objects. So let’s continue!)

Value objects like this allow you to work much more confidently in building your app. You know exactly what information they will contain and the behaviour they offer will be as narrow and explicit as you make it. Working with complex app entities now becomes as easy as working with any other simple data type in Ruby.

Value objects bring safety and simplicity anywhere you use them. In your views, for example, once you know what shape these values will take, you can work with any of their attributes without hesitation. Multiple nil checks become a thing of the past (and when you do have a potentially nullable attribute, you can make it explicitly so). Wrapping these value objects with view-specific presenters becomes more straightforward too, since there’s no need to hide any API that’s not appropriate for consumption within views.

Once you’ve built an expressive collection of domain entities as value objects, the benefits can spread throughout your app at large. You can then use them to pass around your app’s entities to any sort of functional or “service” objects that express your app’s high-level behaviour. Because the entities are now inert, you can be confident in passing them around from place to place without worrying about any unexpected side effects being triggered along the way.

Embracing this change will require other parts of your app to come along for the ride. For example, you’ll most likely want repositories or data mappers to handle your database querying and persistence. You’ll also want to provide specific objects to handle the work of validating form posts and then creating or updating records, before returning them back to your app as valid entity value objects.

This is a change from how you see most Ruby web apps today, but it’s a positive change. Keeping your app’s entities modelled as inactive value objects encourages you to spread your app’s “active” behaviour appropriately around a greater number of other single-responsibility objects. This will only make your app easier to understand and easier to change over time.

In next week’s instalment, I’ll look at why you should use functional objects to model your app’s high-level logic (and return a few value objects in the process).

Read part 1 of this series: My past and future Ruby

Work with us, we’re good peopleGet in touch