DEV Community

loading...

Patterns to refactor thick ActiveRecord models

Vivek Kumar
Software Engineer with a deep interest in programming languages and paradigms.
・2 min read

There are several ways to refactor your thick active record models, but in this post, we will be talking only about one pattern which has helped me a lot in taking out responsibilities from the model.

This pattern is known as the Decorator Pattern.

In simple terms, this pattern is about adding responsibility to an object at runtime.

Let's see an example to understand this pattern in Rails context.

Suppose you have a project model with different types of projects like in-house projects and third-party projects. A project can be started if certain conditions are met, otherwise it should not start and the condition for starting a project depends on its type.

How would we go about writing this logic?

One approach is to define a start method in the project model and check the type of project and then make a decision whether to start the project or not.

# project.rb
class Project < ApplicationRecord
  def start
    if type == 'in-house'
      # do checking whether the project can be started or not for an in-house project
    elsif type == 'third-party'
      # do checking whether the project can be started or not for third-party project
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

But there is one problem with this approach, it will violate the open-closed principle when new project types will come. Moreover, it will also make the project model thick with more project types.

So what can we do about it?

We can move the logic for the start method to another object and just wrap the project object with this object to check whether the project can be started or not.

This pattern will not solve the open-closed violation(more on that in a later post...) but it will help in making the project model a bit thin.

To do that first we have to define a decorate method in the project model.

# project.rb
def decorate(klass = ProjectDecorator)
  klass.new(self)
end
Enter fullscreen mode Exit fullscreen mode

And then, we would obtain the decorated project object by calling this method from wherever we wanted to check whether we can start the project or not.

# project_start_service.rb
def execute
  project.start if project.decorate.can_start?
end

#project_decorator.rb
class ProjectDecorator < BaseDecorator
  def can_start?
    if type == 'in-house'
      # check start constraint for in-house project
    elsif type == 'third-party'
      # check start constraint for third-party project
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In the above example, we are checking whether we can start the project or not from the project_start_service.rb by calling project.decorate.can_start?, the can_start? method is defined on the project decorator and contains the logic of checking whether to start the project or not.

With the above pattern, we would be able to move out related responsibilities out of our model. This would improve the readability and these small decorator objects will have only a single well defined responsibilities.

Discussion (0)