This post is cross-posted at
Ruby on Rails is a great web framework, and
we use it extensively at Braintree
to build web apps. One criticism of Rails, however, is that it
encourages tight coupling of domain and persistence layers. The
convention in Ruby on Rails is for domain objects to extend
ActiveRecord::Base and directly gain persistence. This makes building
small apps easy, but as applications grow, the tight coupling starts to
make things more difficult.
Domain objects in more complicated systems no longer have a simple
mapping into a database row. For example, money might be represented as
cents and currency in the database, but as a Money object in the domain
layer. Now, the domain objects have to handle both the money logic and
the money persistence. Even if the code is abstracted away by a gem, it
still lives in the domain object. Take a look at the methods on a brand
new model in Rails:
>> class Bar < ActiveRecord::Base >> end >> (Bar.methods.sort - Object.methods).count => 391
These methods are in addition to anything you add to the model.
There’s nothing in Rails that says you have to tie your domain objects
to persistence. It’s merely a convention. At
Braintree, we’re using a new
convention for new projects. Our domain objects do not contain any
persistence logic. Instead, we’ve introduced a
repository layer which is in
charge of the persistence of these domain objects. This pattern is
well-documented in Domain Driven
Introducing a Repository layer into our applications has a number of
benefits. The main benefit is that it separates out different kinds of
logic. Domain logic goes into the domain objects, and can be tested in
isolation without any persistence. Persistence logic goes into the
repository objects, which only deal with saving and retrieving objects.
The second big benefit is that abstracting persistence logic into a
repository allows us to swap persistence back-ends without much trouble.
The most notable case is that we can swap in an in-memory data store for
testing (or for most tests), but use Riak for the real application.
Since the repository interacts with the back-end data store, the
application code is the same regardless of back-end. It also allows us
to use different back-ends for different kinds of data but still have a
consistent pattern around persistence.
We’ve extracted this model and repository framework from our current
applications and released it as
curator. Curator currently
supports Riak as the persistence back-end, but more
back-ends are coming soon.
Domain objects include
Curator::Model, which just gives helper methods
like an initialize that sets instance variables. It also adds some
helper methods for Rails.
class Note include Curator::Model attr_accessor :id, :title, :description, :user_id end note = Note.new(:title => "My Note", :description => "My description") puts note.description
These model classes don’t have a lot of extra stuff:
>> class Bar >> include Curator::Model >> end >> Bar.methods.sort - Object.methods => [:_to_partial_path, :current_version, :model_name, :version]
Repositories include the
class NoteRepository include Curator::Repository indexed_fields :user_id end
find_by methods for
note = Note.new(:user_id => "my_user") NoteRepository.save(note) NoteRepository.find_by_id(note.id) NoteRepository.find_by_user_id("my_user")
As persistence gets more complicated, repositories can implement their
own serialize and deserialize methods to handle any case. For example,
suppose our note object contains a PDF:
class NoteRepository include Curator::Repository def self.serialize(note) attributes = super(note) attributes[:pdf] = Base64.encode64(note.pdf) if note.pdf attributes end def self.deserialize(attributes) note = super(attributes) note.pdf = Base64.decode64(attributes[:pdf]) if attributes[:pdf] note end end
As you can see, all persistence logic around PDFs lives in the
Repository. The Note class does not care how PDFs are stored.
Curator in action
If you want to see a simple, fully functional Rails application using
curator, check out
I will detail the relevant bits below.
Thanks to Rails 3.x, you can include
ActiveModel::Validations in any
class to get validations. So if you want your note class to validate, it
would look like:
class Note include Curator::Model include ActiveModel::Validations attr_accessor :id, :title, :description validates :title, :presence => true end
You can also build forms from these objects. new.html.erb would look
<%= form_for @note do |f| %> <%= f.error_messages %> <%= f.label :title %> <%= f.text_field :title %> <%= f.label :description %> <%= f.text_area :description, :size => "60x12" %> <%= f.submit "Create" %> <% end %>
And the controller looks like:
class NotesController < ActionController::Base def new @note = Note.new end def create @note = Note.new(request.POST[:note]) if @note.valid? NoteRepository.save(@note) redirect_to notes_path else render :new end end end
That's it. Rails makes it really easy to build forms and validate
against our models.
Please check out the curator
code and let us know what you think. Like all software,
curator is a work in progress,
so feel free to open issues on Github, submit pull requests, and help us
make it better.