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
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
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
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.
Top comments (0)