Share This Article
This article will show how to implement the Command Pattern in ruby.
The command pattern is a behavioral design pattern that encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
Creating the Command Pattern in Ruby
We first need to create a base Command
class from which all the other commands will inherit.
Let’s start with a basic implementation:
class Command
def initialize(object, method, *args)
@object = object
@method = method
@args = args
end
def execute
@object.send(@method, *@args)
end
end
As you can see, the Command
class takes an object, a method name, and a variable number of arguments.
It also has an execute
method that invokes the method on the object with the given arguments.
Now that we have a base Command
class, let’s implement a few concrete commands.
First, we’ll implement a CreateUserCommand
that takes a UserRepository
object, and a user attributes hash and creates a new user.
class CreateUserCommand < Command
def initialize(user_repository, attributes)
super(user_repository, :create, attributes)
end
end
As you can see, the CreateUserCommand
class inherits from the Command
class, overriding the initialize
method to set the object, method, and arguments.
Next, we’ll implement a DeleteUserCommand
that takes a UserRepository
object and a user id, and deletes the user with the given id.
class DeleteUserCommand < Command
def initialize(user_repository, id)
super(user_repository, :delete, id)
end
end
Now that we have our two commands implemented let’s see how we can use them.
Implementing the Command Pattern in Ruby
First, we need to implement a UserRepository
class that takes a user’s hash in the constructor and provides an create
and delete
methods.
class UserRepository
def initialize(users)
@users = users
end
def create(attributes)
@users[attributes[:id]] = { name: attributes[:name] }
end
def delete(id)
@users.delete(id)
end
end
Next, we need to create a CommandExecutor
class that takes command and executes it.
We’ll also create a main
method that creates a UserRepository
, a CreateUserCommand
, aDeleteUserCommand
, and a CommandExecutor
.
class CommandExecutor
def initialize(command)
@command = command
end
def call
@command.execute
end
end
users = {}
user_repository = UserRepository.new(users)
create_user_command = CreateUserCommand.new(user_repository, id: 1, name: "Lorem Ipsum")
delete_user_command = DeleteUserCommand.new(user_repository, 1)
command_executor = CommandExecutor.new(create_user_command)
puts command_executor.call
# => nil
puts users
# => {:id=>1, :name=>"Lorem Ipsum"}
command_executor = CommandExecutor.new(delete_user_command)
puts command_executor.call
# => nil
puts users
# => {}
If we run the code, we should see the following output:
nil
{1=>{:name=>"Lorem Ipsum"}}
nil
{}
As you can see, the Command
objects are responsible for doing the actual work.
The CommandExecutor
class is responsible for executing the command, and the UserRepository
class is responsible for storing the users.
Undoable Commands
If you want to add support for undoing commands, you must make a few changes to the Command
class.
First, you need to add an undo
method that’ll be responsible for undoing the command.
def undo
@object.send(@method, *@args, undo: true)
end
You also need to ensure that the execute
and undo
methods are called within a transaction so that you don’t end up with inconsistent data.
One way to do that is to use ActiveRecord::Base.transaction
, if you are using ActiveRecord.
require "active_record"
class Command
attr _reader :description
def initialize(object, method, *args)
@object = object
@method = method
@args = args
end
def execute
repository.transaction do
@object.send(@method, *@args)
@description = "#{@method} #{@args.first}"
end
end
def undo
@object.send(@method, *@args, undo: true)
end
def repository
@repository ||= ActiveRecord::Base
end
end
Now that we have updated the Command
class, let’s see how we can use it.
Implementing Undoable Commands
First, we need to update the UserRepository
class to ensure that the create
and delete
methods are undoable.
class UserRepository
def initialize(users)
@users = users
end
def create(attributes)
if attributes[:undo] == true
@users.delete(attributes[:id])
else
@users[attributes[:id]] = { name: attributes[:name] }
end
end
def delete(id, attributes = {})
if attributes[:undo] == true
@users[id] = { name: nil }
else
@users.delete(id)
end
end
end
As you can see, the create
method checks if the undo
attribute is present, and if it is, it restores the user.
Exploring Undoable Commands
The delete
method also checks for the undo
attribute and sets the user’s name to nil
.
Next, we need to update the CommandExecutor
class to add support for undoing commands.
class CommandExecutor
def initialize(command)
@command = command
end
def call
@command.execute
end
def undo
@command.undo
end
end
As you can see, we added an undo
method that invokes the undo
method on the command.
Now, let’s see how we can use the updated CommandExecutor
class.
First, we need to create a User
class that’ll be responsible for storing the data for a user.
class User
attr_reader :id, :name
def initialize(attributes)
@id = attributes[:id]
@name = attributes[:name]
end
end
Next, we need to create a UserRepository
class that’ll be responsible for storing the users.
require "active_record"
class UserRepository
def initialize(users)
@users = users
end
def create(attributes)
if attributes[:undo] == true
@users.delete(attributes[:id])
else
@users[attributes[:id]] = { name: attributes[:name] }
end
end
def delete(id, attributes = {})
if attributes[:undo] == true
@users[id] = { name: nil }
else
@users.delete(id)
end
end
def find(id)
User.new(@users[id]) if @users[id]
end
end
As you can see, the UserRepository
class has a find
method that returns a User
object with the given id.
Next, we need to create a CommandExecutor
class that’ll be responsible for executing the commands.
class CommandExecutor
def initialize(command)
@command = command
end
def call
@command.execute
end
def undo
@command.undo
end
end
Final Results
Finally, we need to write a main
method to put everything together.
require "active_record"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
class User
attr_reader :id, :name
def initialize(attributes)
@id = attributes[:id]
@name = attributes[:name]
end
end
class UserRepository
def initialize(users)
@users = users
end
def create(attributes)
if attributes[:undo] == true
@users.delete(attributes[:id])
else
@users[attributes[:id]] = { name: attributes[:name] }
end
end
def delete(id, attributes = {})
if attributes[:undo] == true
@users[id] = { name: nil }
else
@users.delete(id)
end
end
def find(id)
User.new(@users[id]) if @users[id]
end
end
users = {}
user_repository = UserRepository.new(users)
create_user_command = CreateUserCommand.new(user_repository, id: 1, name: "Lorem Ipsum")
delete_user_command = DeleteUserCommand.new(user_repository, 1)
command_executor = CommandExecutor.new(create_user_command)
command_executor.call
puts user_repository.find(1).name
# => Lorem Ipsum
command_executor.undo
puts user_repository.find(1)
# => nil
If we run the code, we should see the following output:
Lorem Ipsum
nil
As you can see, the CommandExecutor
class is responsible for executing the commands, and the UserRepository
class is responsible for storing the users.
The Command
objects are responsible for doing the actual work.
Summary
In this article, you’ve learned about the command pattern and how to implement it in Ruby.
The command pattern is very useful in many situations, so you should use it whenever it makes sense.
One disadvantage of the command pattern is that it can add a lot of small classes to your project. So use it only when it makes the code more readable & easy to maintain.