In Progress
Unit 1, Lesson 1
In Progress

Query to Command

“Tell, don’t ask”. You’ve heard the advice. But it’s one thing to accept the wisdom of a guideline, and another to put it into practice. In today’s episode, you’ll discover a shift in perspective that will help you take this OO maxim from theory to practice.

Video transcript & code

Say we have a customer object.

We also have a notice that we need to add to the customer's account.

The code to apply the notice looks like this. [See below]

Only active customer accounts get notices, so that the first check that we make.

Next, we check to see if the notice is a quarterly one. If so, we need to first clear any previous quarterly notices from the customer account.

Finally we tell the customer object to add the notice.

require "./setup"

@customer
# => #<Customer:0x0000000204d420>
@notice
# => #<QuarterlyNotice:0x0000000204d3d0>

if @customer.active?
  if @notice.quarterly?
    @customer.clear_quarterly_notices
  end
  @customer.add_notice(@notice)
end

This code has a very common shape, which you will probably find familiar. First, we ask an object if it is in a particular state. Then, depending on the answer to that question, we tell it to alter its state.

Or, putting it another way, first we query, then we command.

There is an often cited object-oriented design guideline known as the "principle of command/query separation". The advice this rule gives us is right there in the name: we should keep commands and queries separate.

And let's be clear: the Customer class has obeyed this rule. It has pure query methods, such as active?, and it has pure commands, like add_notice.

There's another, related design guideline that says: "tell, don't ask". When we see calling code that clumps together commands and queries like this, it's a violation of this second rule. We're not just telling the @customer what to do. Instead, we're asking and then telling.

Like a lot of code that violates this principle,it also exhibits another code smell: it has "feature envy".

What is feature envy? It's when code spend more time sending messages to other objects than it does using methods and attributes of the current "self" object. In this case, the customer object is sent messages three times in six lines of code.

So we have a principle that is being violated, and we have a code smell. Together, what these indicators are telling us is that we aren't letting the customer be an intelligent, self-contained object. We are trying to dictate rules for customer object behavior from somewhere outside the customer class. This sets us up for pain down the road, because it means that customer-centric logic is scattered around in places other than the customer class.

But how do we convert this code from asking and telling, to just telling?

Here's a simplistic way we might do it.

We can add a new method to the customer class, called when_active.

In it, we yield to a block when the status of the customer is "active". We pass a reference to self as a block argument.

class Customer
  # ...
  def when_active
    yield(self) if active?
  end
  # ...
end

Then, we replace the conditional that switches on customer active status with an invocation of when_active. Inside the block, we use the block argument to refer to the customer object.

@customer.when_active do |c|
  if @notice.quarterly?
    c.clear_quarterly_notices
  end
  c.add_notice(@notice)
end

Technically, we have converted this code to telling instead of asking. instead of asking whether the customer is active, we say: "here is an action to perform on yourself when you are in the active state."

But this almost seems like cheating. The shape of the code remains the same. And all this customer related logic remains external to the customer class.

And what if the customer class has other bits of status that we often switch on? Are we going to add one of these when_ methods for every possible state a customer account might be in?

All this goes to show that it is possible to perform a mechanical transformation to technically satisfy the "tell, don't ask" principal while still violating the spirit of the idea.

The thing about object-oriented guidelines like "tell, don't ask" or "command/query separation" is that they are typically asking us to do more than just performing a mechanical refactoring. They are prods to think about our program design in a different way.

Let's go back to the original version of this code.

if @customer.active?
  if @notice.quarterly?
    @customer.clear_quarterly_notices
  end
  @customer.add_notice(@notice)
end

The shape of this code suggests a particular way of thinking about objects. It's a way of thinking in which objects are treated as glorified data structures, upon which we perform various actions.

In this paradigm, what we're really doing is classic procedural programming. We have a procedure, add_notice. It receives two arguments to operate on: @customer and @notice.

add_notice(@customer, @notice)

The only difference that object orientation brings to the table is that we move the first argument to the left of the procedure name.

@customer.add_notice(@notice)

Unfortunately, this view of object orientation—where methods are really just procedures with some special syntax sugar for the data structure they are most closely associated with— is a fairly widespread one. I have sometimes seen it taught as the "true meaning" of object oriented programming, and some programming languages even encode this idea directly into their syntax.

How do we move on beyond this simplistic view of what it means to program with objects, and get at the real aim of principles like "tell, don't ask"? In order to do that, we need to reflect on a concept that isn't often associated with programming: the idea of agency.

According to Wikipedia,

In social science, agency is the capacity of individuals to act independently and to make their own free choices.

In order to get past the procedural, feature-envyious, ask-then-tell design thinking implicit in this code, we're going to have to grant the @customer some agency. How do we do that? Well, as with most changes in mindset, some new terminology will help us.

We'll add a method to the Customer class: accept_notice. The naming of this method is key. This is not a way to add a notice to a customer. It's a way to pass along a notice to the customer. What will the customer actually do with the notice? The naming of this method leaves the answer to that question a little ambiguous—and that's by design.

Inside the method, we reiterate the logic that up till now was external to the customer class. We start with a guard clause, short-circuiting early if the customer count is not active. Next we check to see if the notice is quarterly. If so, we first clear any quarterly notices. Finally, since we already dispensed with the possibility that this is an inactive customer, we go ahead and add the notice.

class Customer
  # ...
  def accept_notice(notice)
    return unless active?
    if notice.quarterly?
      clear_quarterly_notices
    end
    add_notice(notice)
  end
  # ...
end

Notice that our "feature envy" code smell has now vanished. The majority of the messages sent here are now sent to self.

Our original code becomes a one-liner, where we ask the customer object to accept a new notice. What will it do with this notice? For that, we defer to the customer's agency and wisdom.

@customer.accept_notice(@notice)

In some cases, we might want to make the language even less imperative, and give this method a name like on_notice. You might recognize this naming convention from Episode #332, where we talked about the idea of "notify, don't tell".

@customer.on_notice(@notice)

It may seem like we have said quite a lot about what is, admittedly, a small and fairly commonplace refactoring.

But I believe this refactoring reflects, in microcosm, the mental paradigm shift that is necessary to program "with the grain" in an object-oriented language like Ruby. In order to cleanly and equitably separate concerns, we have to grant objects the agency to make decisions for themselves. The requires treating methods as more than just procedures operating imperatively on data. And it requires new language that embraces the fact that we can tell an object to do something, but it's the object responsibility to decide whether and how to act on that message.

Before I go, I want to extend a very special thanks to Michael Feathers. It was his article "Converting Queries to Commands" that inspired this episode, and he very graciously gave his permission to use the theme and the code examples as the basis for it. One way you can say thanks to Michael is by purchasing his classic and indispensable book, Working Effectively with Legacy Code.

Happy hacking!

Responses