It’s no secret that Ruby trivializes many classic design patterns. What might take 30 lines in Java can often be a one-liner in Ruby. But there’s more to learning a design pattern than reading some pseudo code about Widgets and AbstractFlyweightDecoratorFactory classes. The Command pattern is an abstraction that will be familiar to most experienced Ruby programmers, but the goal of this article is to explore some of the secondary considerations about employing the pattern.
Before we dive in too deep, it would be helpful to define what we mean by the Command pattern. As originally described, the Command pattern is a way of representing a request as an object and separating the act of invoking from the act of executing, typically in the context of a GUI application. The invoker passes along any necessary data to the command, and the command is responsible for executing some logic. We’ve found great success employing the pattern in a standard Rails app.
An object provides a convenient boundary for bundling together related state and logic. Suppose I have a command for building a car:
class BuildCar attr_reader :engine_type, :transmission_type, :factory def initialize(engine_type:, transmission_type:, factory:) @engine_type = engine_type @transmission_type = transmission_type @factory = factory end def execute validate! engine = FindEngine.new(type: engine_type).execute transmission = FindTransmission.new(type: transmission_type).execute factory.build(engine: engine, transmission: transmission) end def validate! raise ArgumentError, “Engine type not valid” unless %w[rotary v6].includes?(engine_type) raise ArgumentError, “Transmission type not valid” unless %w[at mt].includes?(transmission_type) end end
Let’s save the PR review feedback for Github. The point is, all of this is code you don’t necessarily want in your controller or model. We want our controllers to focus on coordinating between our views and models. By shrinking the scope of what they have to deal with, they become smaller, easier to maintain, and easier to reason about. The same kind of thinking applies to our ActiveRecord models: by keeping them focused on database interactions and validations, we avoid bloating them with complex business logic. The helpers are all encapsulated to this object, and any intermediate state you need to keep around won’t be exposed and pollute your namespace.
You may have noticed the validate! method in the snippet above. This is an easy way of making assertions about the shape of the data we’re getting. For something a little more structured you could include ActiveModel::Validations
.
Command objects also give you an opportunity to provide a more consistent interface for errors. Instead of an amalgamation of errors from different libraries, you can wrap them into your own exception hierarchy.
Because command objects are so well encapsulated, they are ideal candidates for unit testing. All the required state is passed in through the constructor, so there’s no need to mock out globals. This makes reasoning about the effects of a command much simpler. You can just permutate all of the different possible inputs.
Commands also offer a convenient boundary for mocking. If you have a command object that delegates to other command objects, then in your unit test you can mock out those sub-commands (assuming they have adequate test coverage themselves). When dealing with external API calls, this can be much more convenient than mocking out at the HTTP layer.
For example, say we want to test our BuildCar
command. We don’t care about houw FindEngine
or FindTransmission
work.
describe “BuildCar” do let(:engine) { double(“engine”) } let(:transmission) { double(“transmission”) } before do FindEngine.any_instance.stub(:execute).and_return(engine) FindTransmission.any_instance.stub(:execute).and_return(transmission) end it “constructs a car” do car = BuildCar.new(engine_type: “rotary”, transmission_type: “mt”).execute expect(car.engine.name).to equal(engine.name) expect(car.transmission.name).to equal(transmission.name) end end
Another advantage of command objects is that they are easy to serialize. Everything that’s needed to run is passed in. If you have a command that takes a long time to resolve, then you can serialize it and send it to a background worker queue.
class BuildCarJob < ApplicationJob def perform(engine_type:, transmission_type:, factory_type:) factory = GetFactory.new(type: factory_type).execute BuildCar.new(engine_type: engine_type, transmission_type: transmission_type, factory: factory).execute end end BuildCarJob.perform_later(engine_type: “rotary”, transmission_type: “mt”)
Here we run into an issue: we do not want to serialize the factory, so we have to use a string representing which factory we want. The advantage of building it this way is that in case our implementation for a factory ever changes, we don’t have to worry about older versions being serialized in a queue somewhere.
This does add complexity, but you get all the benefits of asynchronous computation (predictable retry behavior, decoupled systems, better scaling properties). A good tip is to only pass in plain types like strings and numbers; trying to serialize and deserialize Ruby objects can lead to headaches.
One of the touted benefits of the command pattern is how it allows you to easily undo or roll back a change you’ve made. In theory, you have all the data you need to revert the changes you’ve made. But in practice, undoing a command isn’t as simple as reversing the execution steps. The preconditions might be different, and the way you want to handle errors may change.
It doesn’t make sense to undo building a car, so say we had a command to increment a car’s odometer:
class IncrementOdometer attr_reader :car, :distance def initialize(car:, distance:) @car = car @distance = distance end def execute car.mileage += distance end def undo car.mileage -= distance end end
I bet car owners would love to have an undo! This example is a little contrived, but an undo method could deallocate resources or rollback to an earlier version.
You’ll discover that testing, refactoring, and maintenance are simpler if you practice good encapsulation. Your command objects should not reference global state. Pass it in. You should think of your command objects as black boxes—a client doesn’t care what’s going on inside. They only care what goes in and what comes out.
Commands can call other commands! This can be a powerful abstraction for helping your command focus on one specific domain and abstraction layer. Sub-commands can be easily mocked, simplifying your test cases.
Another abstraction that seems appealing is inheritance. Since commands are Ruby classes, you can use inheritance to build an object hierarchy. Composition versus inheritance is often debated in the Ruby community. In general, inheritance gives you tighter coupling between implementations, meaning less code. But sometimes loose coupling gives you space to refactor more aggressively later. For this reason we prefer composition over inheritance.
Don’t let your command objects stick around too long. The longer they stick around, the more likely it is that their internal state will become outdated. Trying to synchronize command object state with global state can turn into a headache. Think of a command object as a one-off process.
Users of your command object shouldn’t need to understand how it works. This goes hand in hand with practicing good encapsulation, but one example you might miss is dealing with API responses. If you’re using Faraday to make API calls, you don’t necessarily want to make your users deal with HTTP status codes or parse response bodies. Have your client return user-friendly response objects that make sense for the domain you’re trying to represent.
Similarly, it may make sense to wrap exceptions. You may be using two or three libraries each that have their own implementations of a timeout error, but really your user just needs to know it’s something that can be retried later.
Commands are easy to copy and paste into a terminal or console. It’s helpful to maintain a document of frequently used commands when you’re trying to debug an issue or explore an API. A command can offer a much simpler interface compared to making a direct HTTP call.
Since commands lend themselves to continual refactoring, you may find it useful to use keyword arguments everywhere. They have a number of advantages: they’re self-documenting, they’re position-independent, and they offer more flexibility for providing defaults.
That isn’t to say that the command pattern is a panacea. They make some things easier, but other things become harder or more complicated.
If you have one file per command, then you will quickly find yourself drowning in extra files. This is one of the costs of highly factored code. You can use modules and subdirectories to manage the complexity, and some higher level patterns like Mediator to provide a simpler interface to users.
Sometimes it may feel like a command isn’t doing much. When it’s just a thin wrapper around an API client, bothering with a command may feel redundant. But it still provides some value: an extra layer of indirection allows you to easily swap out alternative implementations. And you can provide a uniform interface for dealing with responses and exceptions.
It may help to design more intentional commands: instead of a generic UpdateAppointment command, how about a RescheduleAppointment or CancelAppointment? This allows you to make stronger validations and reduce the scope of what can go wrong.
Sometimes you need to use a command in a way that’s orthogonal to the hierarchy you’ve created. One good example of this is concurrency. Suppose we had a command to build several cars.
class BuildCarFleet attr_reader :engine_type, :transmission_type, :quantity def initialize(engine_type:, transmission_type:, quantity:) @engine_type = engine_type @transmission_type = transmission_type @quantity = quantity end def execute quantity.times do BuildCar.new(engine_type: engine_type, transmission_type: transmission_type).execute end end end
There’s a great deal of redundancy buried underneath BuildCar now. There’s no need to repeatedly call FindEngine and FindTransmission; we could call them both once at the start of the command. Furthermore, suppose BuildCar makes a remote call and is IO-bound. There’s no sense in calling it sequentially then when we can multiplex it.
class BuildCarFleetOptimized attr_reader :engine_type, :transmission_type, :quantity def initialize(engine_type:, transmission_type:, quantity:, factory:) @engine_type = engine_type @transmission_type = transmission_type @quantity = quantity @factory = factory end def execute validate! engine = FindEngine.new(type: engine_type).execute transmission = FindTransmission.new(type: transmission_type).execute promises = quantity.times.map do Concurrent::Promises.future do factory.build(engine: engine, transmission: transmission) end end Concurrent::Promises.zip(*promises) end def validate! raise ArgumentError, “Engine type not valid” unless %w[rotary v6].includes?(engine_type) raise ArgumentError, “Transmission type not valid” unless %w[at mt].includes?(transmission_type) end end
This version only calls FindEngine and FindTransmission once, and it leverages Concurrent Ruby to build the cars concurrently. But by not using BuildCar, we’ve introduced a great deal of code duplication. As is often the case, there are tradeoffs between maintainability and performance. This version minimizes the number of API calls and can parallelize the work, but it is more complex, reuses less code, and is harder to refactor.
Hopefully that gives you some idea of what you can and cannot do with this pattern. You could even combine it with some other common patterns: strategy would let you swap out implementations, decorator lets you add functionality in a modular way, and chain of responsibility lets you decouple and decentralize logic.
Got a creative use of the command pattern? Send us an email at info@carbonfive.com. We’d love to write a follow up post with your feedback… and maybe even cover another popular design pattern in Ruby!