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.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
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.