Ricardo Tealdi blog

Yet another tech blog

Repository pattern in Ruby

| Comments

Ruby’s ActiveRecord is a great ORM framework for Ruby and is very easy and simple to use. It is normally used as the Model of the MVC pattern in most of the Rails applications.

The convention over configuration approach is incredibly well implemented on ActiveRecord and it fits very well in cases you want to query the database and show its results on a web page, without having to write dozens of lines to configure it. It’s very good for testing a hypothesis really fast without having to code too much.

The problem of using the Ruby’s ActiveRecord on that way is when you have to deal with big applications that needs to have some layers and responsibilities well defined and not coupled, in this case, we’ll need to have a well distinguished layer between business objects and persistence rules (or even the serialization rules – e.g.: versioning of RESTful resources).

In those scenarios, if we use only one class (aka model), that represents your business object, data mapping and serialization (and view models, services, async process, and so on), we’ll end up with a big and fat model. Sometimes, even the instantiation of that class becomes too difficult.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class User < ActiveRecord::Base
  ACTIVE = 'active'

  validates :name, presence: true
  scope :active, Proc.new { where(status: ACTIVE) }

  before_save :encrypt_pass

  def real_name?
    ...
  end

  def enqueue
    ...
  end

  def encrypt_pass
    ...
  end

  def upload_picture(picture)
    ...
  end

  def serialize
    ...
  end
end

You can try to separate those responsibilities into modules, although you’ll still have a single class that will include all of them, in other words, you’ll still have a class with too many responsibilities (and maybe it becomes harder to test and to predict all its behaviors).

1
2
3
4
5
6
7
class User < ActiveRecord::Base
  include Scopes
  include Validations
  include Callbacks
  include Async
  include Upload
end

So, to avoid it, you can start the refactoring by separating the data persistence from the business object. A good pattern for that is the Repository Pattern.

A Repository mediates between the domain and data mapping layers, acting like
an in-memory domain object collection. Client objects construct query
specifications declaratively and submit them to Repository for satisfaction.
Objects can be added to and removed from the Repository, as they can from a
simple collection of objects, and the mapping code encapsulated by the
Repository will carry out the appropriate operations behind the scenes.
Conceptually, a Repository encapsulates the set of objects persisted in a
data store and the operations performed over them, providing a more
object-oriented view of the persistence layer. Repository also supports the
objective of achieving a clean separation and one-way dependency between the
domain and data mapping layers.

So, here is a simple approach of implenting the repository pattern:

repositories/dtos/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
module Repositories
  module Dtos
    class User < ::ActiveRecord::Base
      validates(:name, presence: true)

      before_save :encrypt_pass

      def encrypt_pass
        ...
      end
    end
  end
end
repositories/mappers/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module Repositories
  module Mappers
    class User
      def to_entity(user_dto, user_entity = Entities::User.new)
        return Entities::User.nil unless user_dto

        user_entity.tap do |entity|
          entity.id = user_dto.id
          entity.name = user_dto.name
          entity.email = user_dto.email
        end
      end

      def to_dto(user_entity, user_dto)
        user_dto.tap do |dto|
          dto.id = user_entity.id
          dto.name = user_entity.name
          dto.email = user_entity.email
        end
      end
    end
  end
end
repositories/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module Repositories
  class User
    def find(id)
      user_dto = find_user_dto(id)
      mapper.to_entity(user_dto)
    end

    def save(user)
      user_dto = map_to_dto(user)
      user_dto.save!
      mapper.to_entity(user_dto, user)
    end

    private

    def find_user_dto(id)
      Repositories::Dtos::User.find_by_id(id)
    end

    def map_to_dto(user)
      user_dto = find_user_dto(user.id) || Repositories::Dtos::User.new
      mapper.to_dto(user, user_dto)
    end

    def mapper
      Repositories::Mappers::User.new
    end
  end
end
entities/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module Entities
  class User
    include Async
    include Upload

    attr_accessor :id, :name, :email

    def nil_user?
      id.blank? && name.blank? && email.blank?
    end

    def self.nil
      @user ||= User.new
    end
  end
end

Like I said, it is just a simple example and it can be better written, but the point here is to show you how the layers of persistence and business are well defined and decoupled. If you implement anything under the persistence layer it will not affect the implementation of your business object and you can keep using the ActiveRecord to query and to save into the database, but it won’t represent the abstraction of the business model in your application anymore. The entity, the new representation of the business model, has the responsibility of carrying some business rules and its relevant data now, becoming a simple PORO (Plain Old Ruby Object).

The biggest problem of this approach is that you may need to rewrite part of your code, since the majority of the current gems, that you might be using, were implemented to be used with an ActiveRecord class.

You can find this implementation at Ruby repository pattern example

Comments