Design Pattern: Command and Concierge

Design Pattern: Command and Concierge

Design Patterns in life and Ruby — gain an intuitive understanding of OO design patterns by linking them with real-life examples.

 

The Command Pattern’s definition is a stressful one to look at.

The Command Pattern:

 

- encapsulates a request as an object,

 

- thereby letting you parameterize other objects with different requests, queue or log requests, and support undoable operations.

 

Let’s forget about it for a second and take a trip to Hawaii.

And live in a luxury hotel.

 

We spent the day on the beach, scuba dived, and did some sightseeing. It's time to get back to the hotel to chill, eat, and plan for the next day.

 

After getting back to the hotel, we want to:

  1. Get room service for dinner
  2. Get laundry service because we didn’t bring extra clothes
  3. Get a travel guide for Kauai, the island we are going to tomorrow

 

We check out the hotel’s service menu and find three service items matching our needs.

We then call the front desk to place these three requests. A concierge picks up our call, write down our list of requests, and acts on each service request as instructed by the service menu.

Then each staff member executes according to each specific request:

  1. The chef in the kitchen starts cooking
  2. The cleaning department send a staff to our room to pick up our clothes
  3. The staff in the lobby grabs a travel guide and delivers it to our room

 

Let’s recap what just happened.

  1. We selected the services we wanted from the menu and submitted them to a concierge.
  2. The concierge wrote these service requests down as a list.
  3. After we hung up, instructed by the service menu, the concierge sent our requests to corresponding departments.
  4. Each department executed on the given request.

 

Let’s see the actions in Ruby.

1. We submitted these three requests to the concierge:

# a. We submitted our requests one by one to a concierge.
we.submit_request_to(concierge, 'dinner_room_service')
we.submit_request_to(concierge, 'laundry_service')
we.submit_request_to(concierge, 'travel_guide')

2. These requests went into a list the concierge keeps track of:

class Concierge
  attr_reader :request_list
  
  def initialize
    @request_list = []
  end
end

class We
  def submit_request_to(concierge, request)
    concierge.request_list << request
  end
end

 

Let’s see that in action (console):

As we can see, after we submitted three requests, these requests were in a request_list taking care by concierge.

3. Instructed by the service menu, the concierge sent our requests to corresponding departments.

class Concierge
  attr_reader :request_list
  
  def initialize
    @request_list = []
  end
  
  def act_on_requests
    @request_list.each do |request|
      case request[:service]
        when 'room_service'
          Kitchen.execute(request[:data])
        when 'laundry_service'
          CleaningDepartment.execute(request[:data])
        when 'trip_planning_service'
          TripAdvisor.execute(request[:data])
        else
          raise 'Request Not Supported'
    end
  end
end

 

The code above should work fine.
Except for one thing….
It smells bad.

Specifically, the part where we have the switch cases:

Why does this part smell bad?

  1. If the hotel offers twenty services, instead of three, the method will be really long.
  2. Each time we want to offer new services or remove an existing service, we have to open the Concierge class and redefine the act_on_requests method.

The method knows too much and requires frequent changes. Having these two combinations together is almost always a bad thing.

Why?

A method that requires frequent changes is a method you need to update often and each time you update a piece of code is an opportunity to introduce new bugs to the system. When the method also knows a tone, a.k.a. it's long, the chances of messing it up when updating increases significantly. That's the reasoning behind a design principle we talked about earlier––encapsulate what varies.

 

Time to Refactor!

There must be a better way than this:

Take a closer look and think about it.

 

Let’s rephrase what the code is doing in English. We loop through the requests on the request list. For each request, according to its service type, we give the corresponding department related data and execute the request. Essentially, we loop through the requests and execute on each of them accordingly.

But what if each request actually knows how to execute itself?

Then the method can be as simple as:

Instead of letting the act_on_requests method decides how each request should be handled, we distribute that responsibility and knowledge back to each request and let it decide how to itself should be handled.

With that being said, our requests could look like this:

class RoomService
  attr_reader :data, :kitchen
  
  def initialize(data)
    @data = data
    @kitchen = Kitchen.new
  end
  
  def execute
    kitchen.cook_for(data)
  end
end

class LaundryService
  attr_reader :data, :cleaning_dpt
  
  def initialize(data)
    @data = data
    @cleaning_dpt = CleaningDepartment.new
  end
  
  def execute
    cleaning_dpt.do_laundry_for(data)
  end
end

class TripPlanningService
  attr_reader :data, :tripAdvisor
  
  def initialize(data)
    @data = data
    @tripAdvisor = TripAdvisor.new
  end
  
  def execute
    tripAdvisor.plan_for(data)
  end
end

 

And the updated Concierge will look like:

class Concierge
  attr_reader :request_list
  
  def initialize
    @request_list = []
  end
  
  def act_on_requests
    @request_list.each do |request|
      request.execute
    end
  end
end

With the updated codes, here is how we, customers of the hotel, send requests to the concierge.

It is pretty easy to create another service.
For example, the hotel also allows us to reserve SPA:

class SpaReservationService
  attr_reader :data, :spa_center
  
  def initialize(data)
    @data = data
    @spa_center = SpaCenter.new
  end
  
  def execute
    spa_center.reserve(data)
  end
  
  def undo
    spa_center.cancel(data)
  end
end

The service not only supports execute (making a spa reservation) but also undo (canceling the reservation).

 

Let’s say the hotel also provides another way to request services without having to call the concierge — a service requesting panel:

We can just press the button and the service with a default setting will be delivered to our room.
Here is the code for the ServicePanel:

class ServicePanel
  attr_reader :button_a1, :button_a2, :button_b1, :button_b2
  
  def initialize(a1_service, a2_service, 
    b1_service, b2_service)
    @button_a1 = Button.new(a1_service)
    @button_a2 = Button.new(a2_service)
    @button_b1 = Button.new(b1_service)
    @button_b2 = Button.new(b2_service)
  end
end

class Button
  attr_reader :service
  
  def initialize(service)
    @service = service
  end
  
  def on_button_click
    service.execute
  end
end

 

And here is how we can create a service panel:

 

??We are using the Command Pattern!??

Let’s revisit the definition of the Command Pattern:

The Command Pattern:

 

- encapsulates a request as an object,

 

- thereby letting you parameterize other objects with different requests, queue or log requests, and support undoable operations.

 

1. “encapsulates a request as an object,”

Each of the services class we created, RoomService, LaundryService, TripPlanningService, and SpaReservationService, is an example of encapsulating a request as an object.

Recap:

2. “thereby letting you parameterize other objects with different requests,”

The ServicePanel is an example of parameterizing an object with different requests.

Recap:

3. “queue or log requests,”

Our requests were queued while the concierge was taking them over the phone.

Recap:

4. and support undoable operations.

SpaReservationService supports undo.

Recap:

 

Advantages of The Command Pattern

The Command Pattern lets you:

 

- parameterize other objects with different requests,

 

- queue or log requests,

 

- and support undoable operations.

Yup, the advantages of the command pattern is encapsulated into its definition. ?

 

Takeaways

The Command Pattern:

 

- encapsulates a request as an object,

 

- thereby letting you parameterize other objects with different requests, queue or log requests, and support undoable operations.

 

Thanks for reading!

Don't forget to subscribe. 😀

Enjoyed the article?

My best content on Software Design, Rails, and Career in Dev. Delivered weekly.

Unsubscribe at anytime. I'll never spam you. Powered by ConvertKit

 

21 Comments Design Pattern: Command and Concierge

Leave A Comment

Your email address will not be published. Required fields are marked *