
The Command Pattern is an excellent way to simplify and encapsulate business logic into simple objects that do one thing extremely well. I first started seeing advanced use of the Command Pattern around the time Hanami came onto the scene back when it was known as Lotus. In fact, Hanami builds upon the Command Pattern, devoting an entire architecture section to what they call interactors. Dry RB also played a pivotal role.
With the above context in mind, let’s delve into why the Command Pattern is important and how to make effective use of it.
Fundamentals
The Command Pattern is fundamental to unlocking the power of
Functional Programming in Ruby. This
power stems from the fact that Ruby closures — also known as functions — are always
callable which means you can send the call
message to them. A good primer for getting up to speed on these fundamentals can be found in this functional composition article. Start there and then return here, it’ll be worth your time.
Advantages
There are several benefits to using the Command Pattern, including:
-
Commands adhere to the Single Responsiblity Principle which is the S in SOLID design.
-
The public interface consists of two methods only:
-
#initialize
- Allows an object to be constructed with minimum defaults using Dependency Injection which is the D in SOLID design. -
#call
- Provides a single, actionable, public method for messaging the object.
-
-
Each command can be swapped out for or used in conjunction with a proc or lambda, which share the same
call
interface. Additionally, commands can be used in case equality (i.e.#===
), functional composition (i.e.<<
or>>
), etc. which provides a myriad of flexible options. -
The private interface encapsulates the objects injected during construction, uses the Barewords Pattern, and aids in keeping the public interface simple.
-
Commands enable reuse of existing functionality elsewhere in the architecture thus reducing duplication of code and keeping things DRY.
-
The architecture of a complex system is significantly improved when built upon simple objects.
-
Improves removal of objects when no longer needed.
Disadvantages
There are a few disadvantages I often hear complaints about when teaching this pattern or receiving feedback during code reviews, such as:
-
Commands create more objects to trace through when studying the architecture of an existing system.
-
Lots of small objects means lots of files loaded in your source editor.
-
Juggling more objects in your head when working with the implementation can add additional cognitive load.
While the above are concerns, I’d argue the advantages of the Command Pattern far outweigh the disadvantages, especially when working in complex systems which will exhibit all of the above concerns regardless. Do you want to deal with a complex system made of simple objects or larger more complex objects? Having lived with the latter, I much prefer the former.
Guidelines
When using the Command Pattern, it’s important to adhere to the following guidelines:
-
All objects names should be nouns ending in er or or if possible. In some situations this might not be possible, though. For example, let’s say you have a collection of commands which process different configurations like JSON, YAML, XML, etc. In that case, you can gather these loaders into a single namespace:
Loaders::JSON
,Loaders::YAML
,Loaders::XML
, etc. without violating this guideline. With the snippet above, the situation is simple, so we have a singleDownloader
object which specializes in downloading data. -
The public interface should only consist of
#initialize
and#call
. -
#initialize
should take one (minimum) to three (maximum) arguments. -
#call
should only take zero (minimum) to three (maximum) arguments. -
The private interface must encapsulate objects injected during construction, which aids in keeping the public interface simple too.
-
Avoid reaching for functionality provided by gems like Interactor for implementing the Command Pattern. Use of these gems introduce more complexity via
before
,around
, andafter
callbacks as well as other functionality that cause unintended side effects. Instead, you can use any Plain Old Ruby Object (PORO) to implement the Command Pattern without introducing further complexity.
Usage
I’ll walk you through getting started with basic usage and then wrap up with advanced usage.
Basic
For context, here’s the implementation shown at the top of this article:
# frozen_string_literal: true
require "net/http"
class Downloader
def initialize client: Net::HTTP
@client = client
end
def call(from, to) = client.get(from).then { |content| to.write content }
private
attr_reader :client
end
We can construct and message the downloader as follows:
require "uri"
require "pathname"
Downloader.new.then do |downloader|
downloader.call URI("https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png"),
Pathname(%(#{ENV["HOME"]}/Downloads/git.png))
end
With the above, as a simple use case, we are able to download the
Git Logo to our local machine as git.png
.
Now imagine if you need this capability in multiple aspects of your architecture. With a command, you have a simple object that can be reused with ease.
Specs
As a bonus, writing specs is straight forward because we can describe expected behavior by stubbing the injected objects.
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Downloader, :temp_dir do
subject(:downloader) { described_class.new client: client }
let(:client) { class_double Net::HTTP }
describe "#call" do
let(:source) { temp_dir.join "input.txt" }
let(:from) { URI "file://#{source}" }
let(:to) { temp_dir.join "output.txt" }
it "downloads content" do
source.write "Test content."
allow(client).to receive(:get).and_return(source.read)
downloader.call from, to
expect(to.read).to eq("Test content.")
end
end
end
💡 Use of the temp_dir
RSpec metadata is a Pathname
folder provided by
Gemsmith which ensures there is a tmp/rspec
folder for dealing with
temporary files. After running the specs, all temporary folders are automatically cleaned up.
Granted, the above provides basic test coverage. You might also want to consider use cases where input is invalid or corrupt.
Advanced
Argument forwarding is not a feature that should be used lightly but does provide an advantage when it comes to the Command Pattern. Especially when wanting to enhance the syntactic sugar of your object’s public API.
Let’s look at our basic implementation again except, this time, enhanced with argument forwarding:
# frozen_string_literal: true
require "net/http"
class Downloader
def self.call(from, to, ...) = new(...).call(from, to)
def initialize client: Net::HTTP
@client = client
end
def call(from, to) = client.get(from).then { |content| to.write content }
private
attr_reader :client
end
Notice the introduction of the .call
class method. By adding the leading from
and to
arguments, which will be given to #call
, followed by ...
to forward the remaining arguments to
#new
, we can now message Downloader
without having to type #new
each time:
Downloader.call URI("https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png"),
Pathname(%(#{ENV["HOME"]}/Downloads/git.png))
Even better, we can swap out our HTTP client — by using the
HTTP gem instead of Net::HTTP
— all via the power of
argument forwarding:
require "http"
Downloader.call URI("https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png"),
Pathname(%(#{ENV["HOME"]}/Downloads/git.png)),
client: HTTP
Conclusion
I hope this explanation paints a useful picture of what the Command Pattern is and how you can leverage this pattern in your own code. 🎉