Design Patterns: Command and Concierge in Life and Ruby

Hotel concierge desk

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:

  1. The service menu presented you with a variety of "commands" or requests you could make
  2. You, the client, selected the commands you wanted and supplied them to the concierge
  3. The concierge encapsulated these requests as order objects and put them in a request queue
  4. The concierge then processed the queued requests by invoking them on the appropriate receivers (kitchen, housekeeping, etc.)
  5. 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!

Similar Posts