Command Pattern for Rails Application
// PUBLISHED:
• // DURATION: 3 min read
•
DRAFT_ENTRY
Few months ago, I worked on a feature where I implemented Command pattern. I started from https://github.com/nslocum/design-patterns-in-ruby/blob/master/commands/command.rb and from there I went to implement more matured base class of Command pattern for my Rails application.
I started from this simple and straight forward implementation of Command class.
class Command
attr_reader :description
def initialize(description)
@description = description
end
def execute
raise NotImplementedError
end
def unexecute
raise NotImplementedError
end
end
I needed more things as I started using this. My requirements were -
- Generic way to provide validation framework to check whether a command can be performed without any side effects.
- If command is invalid then code should be executed to address such case.
- Most of my commands were actually changing the state of the records in database. I wanted atomic execution of command .
Finally, I came up with this base command class.
module CommandSuite
class Command
# initializes command with a
# hash of parameters
def initialize(h = {})
h.each {|k,v| send("#{k}=",v)}
end
# Subclass should override this
# if it returns false action will not be
# executed and #respond_to_invalidate will be
# called.
def validate
end
# Try not to override this method as
# this the central logic of base.
def execute
if validate
ActiveRecord::Base.transaction do
action
end
post_action_tasks
else
respond_to_invalidate
end
rescue => e
handle_error(e)
end
# Handle error.
def handle_error(e)
# Log error
end
# Override this method.
# this is the actual operation
# which should be done by command class.
# This action is wrapped in active record
# transaction which makes it atomic.
def action
end
# Logical separation of code which should
# be executed if action is
# executed successfully.
def post_action_tasks
end
# This method will be called if validate
# returns false in execute method.
def respond_to_invalidate
end
# responsible for undoing action
def undo
end
end
end
This is my sample command.
module CommandSuite
class SampleChangeAppCommand < Command
attr_accessor :app_id, :profile_id, :prev_app_id
def validate
return false if @app_id.blank?
@profile = Profile.find_by_id(@profile_id)
!@profile.blank?
end
def action
@prev_app_id = @profile.app_id
@profile.app_id = @app_id
@profile.save!
end
def undo
@profile.update(app_id: @prev_app_id)
end
def respond_to_invalidate
puts "app_id not present or profile not found"
end
end
end
This is how it works.
For successful case.
2.2.0 :090 > command = CommandSuite::SampleChangeAppCommand.new(app_id: 1, profile_id: 1)
=> #<CommandSuite::SampleChangeAppCommand:0x00000005f6ba30 @app_id=1, @profile_id=1>
2.2.0 :091 > command.execute
=> true
When validation fails,
2.2.0 :122 > command = CommandSuite::SampleChangeAppCommand.new(app_id: 1)
=> #<CommandSuite::SampleChangeAppCommand:0x000000070df140 @app_id=1>
2.2.0 :123 > command.execute
app_id not present or profile not found
This setup really helped me to kick start my feature. I hope others will find this useful.
// BROADCAST_POST: