The Command Pattern is an excellent way to simplify and encapsulate business logic into simple objects that do one thing extremely well. 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 stems from the fact that Ruby 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.
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 the Dependency Inversion Principal 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
callinterface. Additionally, commands can be used in case equality (i.e.#===), functional composition (i.e.<<or>>), etc. which provides a myriad of capabilities. -
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 to keep everything DRY (Don’t Repeat Yourself).
-
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, 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. 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 singleDownloaderobject which specializes in downloading data. -
The public interface should only consist of
#initializeand#call. -
The
#initializemethod should only take one (minimum) to three (maximum) arguments. -
The
#callmethod 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, andaftercallbacks 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
Let’s walk 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"
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
it "downloads content" do
source = temp_dir.join "input.txt"
from = URI "file://#{source}"
to = temp_dir.join "output.txt"
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
You’ve learned, through use the power of the #call method, how to tap into the functional side of Ruby. Now you can more easily intersperse your objects with your functions. This is a major step in unlocking the functional side of Ruby.