Functional command objects in Ruby
In our last visit we discussed the benefits that immutable value objects bring to your Ruby web app. Today, we’ll look at actually making some changes. If our entities are immutable, how do we do this? We use command objects!
What are command objects? They’re things you might’ve seen pop up in Rails projects whenever you need to achieve something of significant complexity. These objects go by many names: service classes, interactors, use cases, and more. Whatever you call them, I don’t think we use them nearly enough. Any web app will be composed of a great many commands, and mixing these together in the “C” layer of MVC is a poor way to encapsulate them. In this article, we’ll take an approach to building command objects that is lightweight and easy enough for you to model every part of your app’s functionality.
The example we’ll use for our command is a common one: validating and creating an article. We’re actually going to be working with 3 objects here: a validation object, an object to persist data to our database, and our command object itself. Let’s start by looking at how we wouldn’t write this:
class CreateArticle
attr_reader :params
def initialize(params = {})
@params = params
end
def call
result = ValidateArticle.call(params)
if result.success?
PersistArticle.call(params)
end
end
end
create_article = CreateArticle.new("title" => "Hello World")
create_article.call
There are a few problems here:
- We’re keeping specific article data as the object’s state. This means that every instance of
CreateArticle
is throwaway. We can never use it again for any other article’s params. - We’re also tightly coupling our object to other concrete classes in our application. Widespread, this kind of coupling will make any application very difficult to change over time. It’ll also make it harder to cleanly unit test this object.
We can do better:
class CreateArticle
attr_reader :validate_article, :persist_article
def initialize(validate_article, persist_article)
@validate_article = validate_article
@persist_article = persist_article
end
def call(params)
result = validate_article.call(params)
if result.success?
persist_article.call(params)
end
end
end
create_article = CreateArticle.new(validate_article, persist_article)
create_article.call("title" => "Hello World")
create_article.call("title" => "Hello Dave")
This is a much nicer arrangement. The object’s collaborators are passed in via its initializer, which reduces their coupling: this object doesn’t care about their concrete implementation, merely their interface. The collaborators are also the object’s only local state, with the article params now being passed directly to the stateless #call
method instead. This means that a single create_article
object can be used over and over for different articles.
Let’s take a second look at that #call
method. It takes an input, makes no side effects, then returns an output. The same input should always result in the same output. You know what this feels like? A function. In this way, our choice of #call
is intentional: it makes this functional object interchangeable with Ruby’s own language-level procs, lambdas and method objects. With functional objects, you can start to do with your objects all the things you could otherwise do with plain functions.
One such benefit is that functional objects are a breeze to compose. This is exactly what we did above with the validate_article
and persist_article
objects. Because we don’t need to worry about changes to their state after initialization, we can pass them around as dependencies to any number of objects, knowing they’ll work exactly as intended in each context.
We’ve provided these dependencies via constructor dependency injection, a classical object-oriented programming technique that blends in perfectly here. I know this isn’t widely used in “conventional” Ruby, and for this same reason I avoided it for many years, but when spread through an app, this makes for a wonderfully decomposed codebase. It makes it easy to break your app’s logic apart into smaller, reusable pieces that are easy to understand in a single glance.
Built this way, your app’s command objects become very easy to test. You can focus on their simple #call
interface, vary the input when needed, and test for the output. Since the dependencies are externally provided, you can replace them with fakes or mock objects to simulate different conditions. In this example, you can see we’ve swapped them for simple lambdas as substitutes for the full objects.
describe CreateArticle do
subject(:create_article) { CreateArticle.new(validate, persist) }
let(:validate) {
-> input { double("result", success?: input["title"].length > 0) }
}
# In a real app, this will write to the DB. Ignore for our little example.
let(:persist) { -> input { input } }
context "successful validation" do
let(:input) { {"title" => "Hello world"} }
it "persists the article" do
expect(create_article.call(input)).to eq input
end
end
context "unsuccessful validation" do
let(:input) { {"title" => ""} }
it "returns false" do
expect(create_article.call(input)).to eq false
end
end
end
At this point, you might be wondering how your app can orchestrate the passing around of the dependencies to make this cornucopia of functional objects really sing. It turns out this is easy with an inversion of control container. This is one of the things that really flipped my Ruby style on its head, and I’m excited to share it with you in our next article!
Until then, if you’re interested in learning more about functional objects and blending FP and OOP in general, I recommend you watch Piotr Solnica’s seminal talk on this topic, Blending Functional and OO Programming in Ruby.
Read part 1 of this series: My past and future Ruby
Read part 2: Inactive records: the value objects your app deserves