Black computer keyboard

Take Command of Your Ruby Code with the Command Pattern

Albert Yi ·

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.

 

Carbon Five is hiring

 

Error Handling & Validation

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.

Testing

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

Asynchronous

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.

Undo

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.

Best Practices

Encapsulation

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. 

Composition

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.

Ephemeral

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.

Don’t Expose Implementation Details

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.

Console Friendly

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.

Keyword Arguments

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.

 


Downsides

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.

File Bloat

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.

Redundancy

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.

Orthogonality

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.

Conclusion

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!

 


 

Carbon Five is hiring