How I Design Rails Applications Part 1: Value Objects

I have been thinking and experimenting with different techniques to make Rails applications manageable as they grow for about 2 years now. This exercise is not because I desire to add layers/complexity to my Rails applications, but because I have experienced the pain that a Rails application can provide as it matures. In this multi-part series I will break down some of the successful techniques I am using to achieve my goals.

One of my starting places was this article by Bryan Helmkamp. I will often reference this article in this series. Thanks Bryan for sharing your techniques.

I agree with Bryan the suggestion to make “fat models” is not a best practice. The model should not contain logic that does not directly correspond to reading/writing to the database.

Part 1

The first technique Bryan discusses in his article is to “Extract Value Objects.” According to Bryan, value objects are, “simple objects whose equality is dependent on their value rather than an identity.” This is an important conecpt taken from domain driven design. In his book on DDD, Eric Evans states: “Many objects have no conceptual identity. These objects describe characteristics of a thing.”

A great example of a value object is color:

class Color
  include Comparable

  def initialize(key)
    @key = key
  end

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

  def eql?(other)
    to_s == other.to_s
  end

  def to_s
    @key.to_s
  end
end

Value Objects in the Model

It is common practice for developers to define value objects in a model, usually as a constant in the model.

class Car < ActiveRecord::Base
  COLOR = %w(
    black
    blue
    green
  )
end

This practice is not good because the value object may be used in attribute(s) of a model, but has nothing directly to do with the domain the model is representing. Defining value objects inline in the model class carries the at least following disadvantages:

  • Impossible to add functionality to value object without further polluting the model
  • Limits reuse of the value object
  • Harder to test

Value Objects in the Database

Quite often, developers will include value objects in the database as a model. This is also bad form. The value object is not dynamic so it does not necessitate use of the database. In addition, the value object has no identity, so we do not need a keying system. The only value the database really provides in this case is the limiting of the valid values for a value object to some predefined set that can be enforced.

Enumerative Gem

Enter the enumerative gem. This gem was authored by Nils Jonsson and myself. The enumerative gem provides the tools necessary to create value objects that are limited to a finite set of valid values.

An enumeration for color might be implemented like:

class Color

  def self.valid_keys
    %w(
      black
      blue
      green
    )
  end

  include Enumerative::Enumeration

end

In addition, you must add the translations for the enumeration’s values to the config/en.yml file:

en:
  enumerations:
    color:
      black: Black
      blue: Blue
      green: Green

Now you can use the enumeration.

Color::BLACK # #<Color:0x000001015e6aa8 @key="black">
Color::BLACK.key # "black"
Color::BLACK.value # "Black"
Color.to_select # [["Black", "black"], ["Blue", "blue"], ["Green", "green"]]
Color.new('black').valid? # true
Color.new('some invalid value').valid? # false

The initiatlizer is very forgiving of the values it will accept so that it can be used to easily standardize input, etc.

Color.new(Color::BLACK) # #<Color:0x000001015e7aa0 @key="black">
Color.new('black') # #<Color:0x000001015e7aa0 @key="black">
Color.new(key: 'black') # #<Color:0x000001015e7aa0 @key="black">
Color.new(key: :black) # #<Color:0x000001015e7aa0 @key="black">

HasEnumeration Module

It gets even more exciting when you want to use it in a model. If you included the Enumerative::HasEnumeration module you get automatic casting from the key that is stored in the database to the type of the enumeration your attribute is defined as on a read and back to the key for storage on a write.

class Car < ActiveRecord::Base
  include Enumerative::HasEnumeration

  has_enumeration color, from: Color
end

Convenience when you read from the database:

car = Car.first
car.color_before_type_cast # "black"
car.color # #<Color:0x000001015e7aa0 @key="black">

Convenience when you write to the database

car = Car.new
car.color = Color::BLACK
color.save!
car.reload
car.color_before_type_cast # "black"
car.color # #<Color:0x000001015e7aa0 @key="black">

Testing Enumerations

Enumerative provides some a shared spec for easier specing of enumerations. Following is an example spec for the color enumeration.

require 'spec_helper'
require 'enumerative/enumeration_sharedspec'

describe Color do

  it_should_behave_like 'an Enumeration'

  def self.keys
    %w(
      black
      blue
      green
    )
  end

  self.keys.each do |key|
    const = key.upcase

    it "should have #{const}" do
      described_class.const_get( const ).should be_valid
    end
  end

  it "should have the correct select-box values" do
    described_class.to_select.should == [
      ["Black", "black"],
      ["Blue", "blue"],
      ["Green", "green"]
    ]
  end
end

Project Organization

I recommend placing enumerations in the app/enumerations directory.

Conclusion

With very little effort we have extracted our value object out of the database and retained the ability to limit the valid values to a finite set. We can also use our value object in a model and persist the model’s value for the enumeration in the database. Most importantly, we have successfully removed a common contributer to making Rails models bloated and unmanageable.

Stay tuned for the next installment in the series.

Comments