Design Patterns: Command and Concierge in Life and Ruby
The Command design pattern is a behavioral pattern that aims to encapsulate a request as an object, thereby allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. This definition may seem a bit abstract at first glance, so let‘s start by looking at a real-world example to understand the pattern more intuitively.
Imagine you are staying at a luxurious hotel for a relaxing getaway. After a day of sightseeing and activities, you return to your room and decide to use some of the hotel‘s services. You open up the menu and see options for ordering room service, scheduling laundry pickup, requesting a trip planning consultation, making spa reservations, and so on. You call the front desk, where a friendly concierge answers and takes down your list of requests. The concierge then relays your order details to the appropriate hotel staff members to fulfill – the kitchen starts preparing your food, housekeeping comes to get your laundry, and a trip planning guide is sent to your room.
Let‘s break down what happened here from a design pattern perspective:
- The service menu presented you with a variety of "commands" or requests you could make
- You, the client, selected the commands you wanted and supplied them to the concierge
- The concierge encapsulated these requests as order objects and put them in a request queue
- The concierge then processed the queued requests by invoking them on the appropriate receivers (kitchen, housekeeping, etc.)
- Each receiver executed the command using the details provided
We can model this scenario in Ruby code to further solidify our understanding. First, let‘s think about how we might naively implement the hotel service request system without utilizing the Command pattern:
class Concierge
def initialize
@request_queue = []
end
def place_request(request)
@request_queue << request
end
def process_requests
@request_queue.each do |request|
case request[:type]
when :room_service
puts "Kitchen is preparing #{request[:order]}"
when :laundry
puts "Housekeeping is picking up laundry"
when :trip_planning
puts "Trip planning guide sent to room"
end
end
@request_queue.clear
end
end
In this implementation, the Concierge class maintains a queue of requests, which are modeled as simple hashes specifying the request type and details. To handle a request, we use a case statement to check the request type and perform the appropriate action.
While this code will work, it has a few notable drawbacks. The Concierge‘s process_requests
method will grow in size and complexity as more service types are added to the system. The Concierge also has to know all the details of how to perform each type of request, making it tightly coupled to the receivers. If we want to add new request types or change how they are fulfilled, we‘d have to go in and modify this method directly.
According to the book "Design Patterns: Elements of Reusable Object-Oriented Software", which first formally described the Command pattern, this type of code is inflexible because it "couples the class that sends the request to the one that performs it." By avoiding these dependencies, the Command pattern lets you change the requestor and performer independently, following the open-closed principle.
We can refactor this code to utilize the Command pattern and alleviate these coupling issues:
class Command
def execute
raise NotImplementedError
end
end
class RoomServiceCommand < Command
def initialize(order)
@order = order
end
def execute
puts "Kitchen is preparing #{@order}"
end
end
class LaundryCommand < Command
def execute
puts "Housekeeping is picking up laundry"
end
end
class TripPlanningCommand < Command
def execute
puts "Trip planning guide sent to room"
end
end
class SpaReservationCommand < Command
def initialize(treatment, time)
@treatment = treatment
@time = time
end
def execute
puts "Spa reservation for a #{@treatment} at #{@time} confirmed"
end
def unexecute
puts "Spa reservation for a #{@treatment} at #{@time} canceled"
end
end
class Concierge
def initialize
@request_queue = []
end
def place_request(command)
@request_queue << command
end
def process_requests
@request_queue.each do |command|
command.execute
end
@request_queue.clear
end
def undo_last_request
@request_queue.pop.unexecute
end
end
Each type of service request is now encapsulated in its own command class that inherits from the abstract Command class and implements the execute
method. This method contains the details of how to perform that specific request. Some commands, like SpaReservationCommand
, may take additional data in their constructors to parameterize their behavior.
The Concierge‘s role is now simplified – it just maintains a queue of commands and executes them in sequence, without needing to know the specifics of each request type. New request types can easily be added by creating additional command classes, without having to modify the Concierge.
We‘ve also added support for undoable operations, a key feature of the Command pattern. The SpaReservationCommand
includes an unexecute
method that reverses the effect of the command. The Concierge provides an undo_last_request
method to pop the last command off the queue and unexecute it if needed.
Let‘s see an example usage of our refactored code:
concierge = Concierge.new
concierge.place_request(RoomServiceCommand.new("Cheeseburger and fries"))
concierge.place_request(LaundryCommand.new)
concierge.place_request(TripPlanningCommand.new)
concierge.place_request(SpaReservationCommand.new("Deep tissue massage", "3:00pm"))
concierge.process_requests
# Output:
# Kitchen is preparing Cheeseburger and fries
# Housekeeping is picking up laundry
# Trip planning guide sent to room
# Spa reservation for a Deep tissue massage at 3:00pm confirmed
concierge.undo_last_request
# Output:
# Spa reservation for a Deep tissue massage at 3:00pm canceled
We place several service requests by instantiating the appropriate command objects and passing them to the concierge. The concierge stores these in its request queue. When we call process_requests
, the concierge iterates through the queue and executes each command in order. We can also undo the last request using undo_last_request
.
In addition to allowing us to queue requests, the Command pattern also lets us parameterize other objects with different requests. For example, the hotel could provide an in-room service panel with preset buttons for common requests:
class ServicePanel
def initialize(concierge)
@concierge = concierge
end
def press_button(type)
case type
when :room_service
@concierge.place_request(RoomServiceCommand.new("Margherita Pizza"))
when :laundry
@concierge.place_request(LaundryCommand.new)
when :trip_planning
@concierge.place_request(TripPlanningCommand.new)
end
end
end
panel = ServicePanel.new(concierge)
panel.press_button(:laundry)
panel.press_button(:trip_planning)
concierge.process_requests
# Output:
# Housekeeping is picking up laundry
# Trip planning guide sent to room
The ServicePanel acts as another client of the Concierge. It‘s parameterized with preset commands that are invoked when the corresponding button is pressed. This provides a simplified interface for guests to access common services without having to provide the full details each time.
While we‘ve used a hotel service request system as our guiding example, the Command pattern‘s applicability extends to many other domains, especially in software development:
- GUI buttons and menu items that perform actions on click
- Macro recording and playback in productivity software
- Job queues and task scheduling systems
- Undo/redo stacks in text editors and graphic design tools
- Database transactions and rollbacks
- Remote procedure calls and messaging systems between microservices
In web development, the Command pattern is often used behind the scenes in web frameworks and libraries to encapsulate user actions or API requests. For example, in Ruby on Rails, controller actions serve as commands that are invoked in response to HTTP requests:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: ‘User was successfully created.‘
else
render :new
end
end
# Other CRUD actions...
private
def user_params
params.require(:user).permit(:name, :email)
end
end
Here the create
action is a command object encapsulating the request to create a new user. The action is parameterized with the user_params
extracted from the request. When a POST request comes in to the /users
route, Rails automatically instantiates the UsersController
, invokes the create
action, and returns the appropriate response.
Many Javascript frameworks like Redux and Angular also utilize commands or actions to modify application state in a predictable way. By treating state changes as explicit command objects instead of scattered mutations, these frameworks promote deterministic state management that‘s easier to understand and test.
At an even higher architectural level, the Command pattern forms the basis for the Command-Query Responsibility Segregation (CQRS) pattern. CQRS is an architectural pattern that separates read and update operations for a data store into separate models – commands for updates and queries for reads. Embracing this separation can maximize performance, scalability, and security. Many modern web applications use CQRS to handle complex domains and high throughput scenarios.
Another common use case for the Command pattern is implementing undo/redo functionality, especially in conjunction with the Memento pattern. The Memento pattern is used to capture and externalize an object‘s internal state so that the object can be restored to this state later. By storing a stack of command mementos, each encapsulating the state change made by executing the command, we can easily roll back or replay changes as needed:
class TextEditor
attr_accessor :text
def initialize
@text = ""
@undo_stack = []
@redo_stack = []
end
def execute(command)
@redo_stack.clear
@undo_stack.push(command)
command.execute(self)
end
def undo
return if @undo_stack.empty?
command = @undo_stack.pop
@redo_stack.push(command)
command.unexecute(self)
end
def redo
return if @redo_stack.empty?
command = @redo_stack.pop
@undo_stack.push(command)
command.execute(self)
end
end
class InsertTextCommand
def initialize(offset, text)
@offset = offset
@text = text
@memento = nil
end
def execute(editor)
@memento = editor.text.dup
editor.text.insert(@offset, @text)
end
def unexecute(editor)
editor.text = @memento
end
end
editor = TextEditor.new
editor.execute(InsertTextCommand.new(0, "Hello "))
editor.execute(InsertTextCommand.new(6, "world!"))
puts editor.text #=> "Hello world!"
editor.undo
puts editor.text #=> "Hello "
editor.redo
puts editor.text #=> "Hello world!"
In this example, the TextEditor
class maintains separate undo and redo stacks containing command objects. Each command object encapsulates a specific editing operation like inserting or deleting text. When a command is executed, it saves a memento of the editor‘s current text. To undo a command, the memento is restored, reverting the editor‘s text to its previous state. Redoing a command simply re-executes it, reapplying the text change.
It‘s worth noting that the Command pattern is very similar to the Strategy pattern in that both encapsulate an algorithm or operation into a separate object. The key difference is intent – strategies tend to be stable, interchangeable behaviors that are configured at runtime, while commands are typically one-off operations that may change an object‘s state when executed. Commands also often need access to the internal details of the receiver object they operate on, while strategies aim to be more loosely coupled.
When applying the Command pattern in practice, there are a few best practices and considerations to keep in mind:
- Aim to keep command classes small, focused, and adherent to the Single Responsibility Principle. If a command class starts getting too large, it‘s often a sign that it should be split into multiple smaller commands.
- Consider providing a fluent interface or builder for constructing complex commands with many parameters or setup steps. This can greatly improve readability and ease of use.
- Be mindful of overusing the Command pattern, especially for simple operations that don‘t benefit from the added flexibility. Sometimes a basic function or method is sufficient, and introducing commands would only add unnecessary complexity.
- Pay attention to error handling and exception safety, especially for long-running or asynchronous commands. Ensure that a failing command doesn‘t leave the system in an inconsistent or corrupted state.
- Profile and optimize the performance overhead of creating and executing many small command objects if needed. Object pooling, flyweights, and other optimization techniques can help mitigate the impact.
To sum up, the Command pattern is a versatile and powerful tool in a developer‘s design toolbox. By encapsulating requests or operations as objects, it provides a flexible way to parameterize clients, queue and log requests, and support undo/redo functionality. It promotes loose coupling, extensibility, and separation of concerns in our code. The pattern also enables higher-level architectural approaches like CQRS and event sourcing that can greatly improve an application‘s design.
While the hotel concierge analogy is a fitting mental model to understand the pattern, the Command pattern‘s true power shines in the realm of software development. From something as simple as a button click in the GUI to a complex database transaction spanning multiple microservices, commands provide a unified and robust way to model and execute these operations. The next time you‘re tasked with designing a system that needs to perform a varied set of actions or track a history of changes, consider reaching for the Command pattern – it just might be the right tool for the job!