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)