ActiveRecord - Value Objects with serialize
Tags: ruby, rails, oop, ddd
Published: 2021-04-13 06:41:46

I love Value Objects. They can improve and clean up your project by insane amount, yet they are simple and easy to understand for almost everyone. Not only easy to understand but actually easy to write as well!

But wait, most of the Value Object implementations are just Plain Ruby Objects right? What if I use ActiveRecord? 

Doesn't matter if you like it, or just have to live with the legacy written by hardcore Rails developer - ActiveRecord has a way to inject Value Objects. In both cases they will improve your life and domain model of your code. Lets take a look at one of the simplest possibilities.

### The underestimated serialize

One of the simplest method to use is the [serialize](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html) method. All it does it is loading data into object using `load` method and dumps the data into db using `dump`. Its simple yet powerful mechanism. Lets get some example (implemented as internal class for simplicity):

```ruby
class CreateMeetings < ActiveRecord::Migration[6.0]
  def change
    create_table :meetings do |t|
      t.integer :time_limit, default: 1

      t.timestamps
    end
  end
end

class Meeting < ApplicationRecord
  class TimeLimit
    include Comparable

    def self.dump(time_limit)
      time_limit.to_i
    end

    def self.load(time_limit)
      new(time_limit)
    end

    def initialize(time_limit)
      @time_limit = position.to_i
    end

    def to_i
      time_limit
    end

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

    def minimum?
      time_limit == 1
    end

    def maximum?
      time_limit == 60
    end

    def seconds
      time_limit.minutes.seconds
    end

    private

    attr_reader :time_limit
  end

  serialize :time_limit, TimeLimit
end
```

As you can see there is implementation of `TimeLimit` value object, which allows us to have domain related behaviors (`minimum?`, `maximum?`, `seconds` methods). Its then simply registered into ActiveRecord by using `serialize` macro. 

```ruby
serialize :time_limit, TimeLimit
```

Lets see it in action:
```
 :004 > meeting = Meeting.new
 :005 > meeting
 => #<Meeting id: nil, time_limit: #<Meeting::TimeLimit:0x00007f9744443120 @time_limit=1>, created_at: nil, updated_at: nil>
 :006 > meeting.time_limit.minimum?
 => true 
```

As you can see its clearly visible that there is separate object used. We can call all the methods on it directly, without any Rails magic involved. Lets try to change the value using setter:
```
 :014 > meeting.time_limit = 5
 :015 > meeting
 => #<Meeting id: nil, time_limit: #<Meeting::TimeLimit:0x000056020ecfec50 @time_limit=5>, created_at: nil, updated_at: nil> 
```

Interesting right? A new object with our value! ActiveRecord is using the `load` method to create it for us. Lets do a save then!

```
 :016 > meeting.save
   (0.1ms)  begin transaction
  Meeting Create (0.5ms)  INSERT INTO "meetings" ("time_limit", "created_at", "updated_at") VALUES (?, ?, ?)  [["time_limit", 5], ["created_at", "2021-04-12 20:32:06.867606"], ["updated_at", "2021-04-12 20:32:06.867606"]]
   (17.9ms)  commit transaction
 => true
 :017 > meeting
 => #<Meeting id: 2, time_limit: #<Meeting::TimeLimit:0x000056020ebf89c8 @time_limit=5>, created_at: "2021-04-12 20:32:06", updated_at: "2021-04-12 20:32:06"> 
```

Great so it saves a correct value! Turns out it used our `dump` method correctly. But this is the same record in memory, lets fetch our newly created meeting from db instead:

```
 :024 > meeting = Meeting.last
  Meeting Load (0.2ms)  SELECT "meetings".* FROM "meetings" ORDER BY "meetings"."id" DESC LIMIT ?  [["LIMIT", 1]]
 :025 > meeting
 => #<Meeting id: 2, time_limit: #<Meeting::TimeLimit:0x000056020b8a3618 @time_limit=5>, created_at: "2021-04-12 20:32:06", updated_at: "2021-04-12 20:32:06"> 
 :026 > meeting.time_limit.seconds
 => 300 seconds 
```

This is so cool right? Simple yet powerful interface, almost no magic involved. This allows us to create Plain Old Ruby Objects and inject them into our ActiveRecord based entities without problems. No need to spread your `maximum` logic all over the place (Service Objects I'm looking at you...), we can encapsulate them and still use our beloved(hated?) models!

---
For "get rid of AR" developers: you can use this mechanism to inject a domain modeling into legacy code without problems. Ruby duck typing is your friend:

```ruby
# using ActiveRecord:
meeting.time_limit.seconds

# using some PORO or w/e:
meeting.time_limit.seconds
```

Yes, there is no difference, you can refactor at any point of time, and your code will remain untouched. Isn't that beautiful?

---
There is one thing to mention that might catch you off guard: object creation validation. Lets modify our example a bit:

```ruby
  class TimeLimit
    # ...

    InvalidLimitError = Class.new(StandardError)

    def initialize(time_limit)
      raise InvalidLimitError unless (1..60).include?(time_limit.to_i)

      @time_limit = time_limit.to_i
    end

   # ...
```
And try to create invalid limit:
```
 :034 > meeting.time_limit = 100
 :035 > meeting.id
 :036 > meeting.time_limit
Traceback (most recent call last):
        3: from app/models/meeting.rb:10:in `load'
        2: from app/models/meeting.rb:10:in `new'
        1: from app/models/meeting.rb:16:in `initialize'
Meeting::TimeLimit::InvalidLimitError (Meeting::TimeLimit::InvalidLimitError)
```

As you can see, ActiveRecord does a lazy loading on serialized attributes, so the `load` method is called after first call - **not on the assignment**. Keep that in mind when writing this kind of code.

---
Useful links:
- [serialize documentation](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html)
- [Value Object by Martin Fowler](https://martinfowler.com/bliki/ValueObject.html)
- [Good Value Object Conventions for Ruby](https://github.com/zverok/good-value-object)