DEV Community

loading...

TellDontAsk and CQRS - Object Design Guide Summary 2

migueldevelopez profile image MiguelDevelopez ・7 min read

Welcome back to the second part of the Summary of Object design style guide by Matthias Noback, where I will try to synthesize the differences between query and command methods, CQS, TellDontAsk principle, and more things from the book.
And as for the first post, I highly recommend you to read the full book to find more examples in detail.
image

Let’s start!
There are two kinds of methods in an object, these are:

  • Retrieve a piece of information: getters, format, calc, “select” from the DB...
  • Perform tasks: sending an email, “update” or “delete” from the DB… You may know about the Command Query Responsibility Segregation (CQRS) principle, this principle is to define exactly what I was talking about (do not mix with the Command from CQRS with the Command Pattern ).

1º Retrieving information with query methods

To name these methods just call them what they are going to return or as the action, they are going to perform getting the information. Examples: itemCount, discountPercentage, calculateNetAmount, exchangeRateFor...
Query methods should have a single return type. You may still return null, but make sure to look for alternatives, like a null object, an empty list if your method will return an array, or throw an exception instead.
Here some examples:
BAD WAY, imagine what will be the code that receives this response.

/**
* @return string|bool
*/
public function isValid(string $emailAddress)
{
    if (/* ... */) {
        return 'Invalid email address';
    }
    return true;
}

Enter fullscreen mode Exit fullscreen mode

There are many better ways to deal with it, but all of them have considerations:

  • Return a type or null.
public function findOneBy(string $type): ?Page { }
    $page = this->findOneBy($type);
    if (page instanceof Page) {
        // page is a Page object and can be used as such.
    } else {
        // page is null, and we have to decide what to do with it.
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Throw an exception and capture it in the client.
public function getById($id): User
{
    $user = User::find($id);
    if (!$user instanceof User) {
        throw UserNotFound->withId($id);
    }
    return $user;
}

Enter fullscreen mode Exit fullscreen mode

Now the author talks about the good practice of not showing the internal functionality to the client, which reminds me of the principle Tell-Don’t-Ask, this principle encourages us to move behavior inside of an object, here an example:
BAD WAY

final class Product
{
     public function shouldDiscountPercentageBeApplied(): bool { }
     public function discountPercentage(): Percentage { }
     public function discountAmount(?Percentage $percentage): Money { }

     // Apply the logic and calculations in the client is not the best practice
}

// # main.php or Client.php or whatever
$product = new Product(...);
if($product->shouldDiscountPercentageBeApplied()) {
    $percentage = $product->discountPercentage();
    $money = $product->discountAmount($percentage);
} else {
    $money = $product->discountAmount();
}

$money->...
Enter fullscreen mode Exit fullscreen mode

BETTER WAY

final class Product
{
     public function calculateFinalAmount(): Money
     {
        // use here the logic of the private methods, instead use them in the client
        // Let query methods expose as little of an object’s internals as possible.
     }
     private function shouldDiscountPercentageBeApplied(): bool { }
     private function discountPercentage(): Percentage { }
     private function discountAmount(?Percentage $percentage): Money { }
}

// # main.php or Client.php or whatever
$product = new Product(...);
$money = $product->calculateFinalAmount();
$money->...
Enter fullscreen mode Exit fullscreen mode

In the bad way, the client has to use all the public methods to calculate the final amount of the product, so the client needs to know how to use the public methods, and that’s not the best solution.
With the better way, the client just has to call the calculateFinalAmount and it will execute all the logic, making this easier, decoupled, and centralized.
image

These recommendations should not become a rule that you can’t deviate from. In fact, no programming rule should ever be like that.

Query methods shouldn’t use command methods inside them to avoid having side effects. There are some exceptions, for example in a method of a controller, imagine a method that creates a user and returns it to the front, or some specific methods, for example, a method named nextId(), if two clients called this method at the same time it will return the same ID, so this method should make a side effect to avoid this potential error.

There are more tips related to the inversion dependencies and how to test with fakes or stubs, but I would like to deal with it in another different and specific post related to tests and the advantages of this practice.
Basically, when a query method needs to cross the system’s boundary (using DB, API call...) use an abstraction with an interface to be able to change and test it easily in the future.

2º Performing tasks with command methods

To name this kind of method the author recommends in the imperative form, for example, sendReminderEmail, saveRecord… and the command methods are easy to recognize because they always should return null.
But what happens if a command has to do more than one thing? Something like:

public function changeUserPassword(
    UserId $userId,
    string $plainTextPassword
): void {
    $user = $this->repository->getById($userId);
    $hashedPassword = $this->passwordHasher->hash($plainTextPassword);
    $user->changePassword($hashedPassword);
    $this->repository->save($user);
    $this->mailer->sendPasswordChangedEmail(userId);
}
Enter fullscreen mode Exit fullscreen mode

First of all, as you can see there is no problem using a query method in a command method, and on the other hand, the changeUserPassword hides the fact that this method sends an email when a user changes the password, how can we deal with it properly? The best solution would be to dispatch an event, which allows us to have an object more decoupled, handle some effects in the background… but we have to take care and dispatch it explicitly to not lose track of where it is dispatched.
$this->eventDispatcher->dispatch(new UserPasswordChanged($userId));

As we do in the query methods, throwing an exception if something goes wrong is the best approach. You may be tempted to return a string in command, but remember that command must return null.

What should we do if a command needs to cross a system boundary (commands that reach out to some remote service, database, etc)? It’s the same as I mentioned in the queries, abstracting with interfaces. This will allow us to test command methods easily using a mock or a spy to test calls to these methods, avoiding sending emails or using the DB when the test runs. You can use a mocking tool for this or write your own spies.

3º Dividing responsibilities

In this chapter, the book talked about CQRS, the benefits, and why we should apply it. Personally, I think the CQRS will be overkill if your application won’t need to be maintained in a very long-term period (scalability matters), if the size is small or medium, simple user interface e.g. CRUD style, simple business logic…
In summary, CQRS allows you to separate the load from writes and reads allowing you to scale independently, for example, we’ll split a class like this:

final class PurchaseOrder
{
    private int $purchaseOrderId;
    private bool $wasReceived;
    private int $productId;
    private int $orderedQuantity;

    public function purchaseOrderId(): int
    {
        return $this->purchaseOrderId;
    }

    public function markAsReceived(): void
    {
        $this->wasReceived = true;
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

In a class for reading methods and another class for saving methods.
The class with command methods:

final class PurchaseOrder
{
    private int $purchaseOrderId;
    private bool $wasReceived;
    private int $productId;
    private int $orderedQuantity;

    public function markAsReceived(): void
    {
        this->wasReceived = true;
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

And to extract the query methods there are several ways to do that, for example:

  • Specific to a use case.
  • Directly from their data source.
  • Domain events.

Some advantages are:

  • Avoid exposing more behavior to a client than it needs.
  • Allow the read and write workloads to scale independently, and may result in fewer lock contentions.
  • The read side can use a schema that is optimized for queries, while the write side uses a schema that is optimized for updates

4º Changing the behavior of services

Some suggestions and recommendations about how to refactor a service. Some of these suggestions were covered before like, for example, use event listener for additional behavior, compose with abstractions (interfaces) to achieve more complex and easy to configure behavior because you will be able to replace instead of modifying the current one, introduce constructor arguments to make behavior configurable…
And something that I wanted to talk about in the first part… composition over inheritance.
The main reason for it is because with the inheritance you lose the flexibility and reconfigurability that brings to you the composition with interfaces. If you try to change the behavior of an existing object comes with many downsides:

  • Subclass and parent class become tied together, making your code more coupled, maybe with methods that you won’t use at all. Example: the typical BaseController.
  • Subclasses can override protected but also public methods, so a lot of the internals of the object are now exposed.

A better approach, according to the author, to reusing code is a trait. A trait is plain code reuse of a compiler-level copy/paste of code.

interface RecordsEvents
{
    public function releaseEvents(): array;
    public function clearEvents(): void;
}

trait EventRecordingCapabilities
{
    private array $events;
    private function recordThat(object $event): void
    {
        $this->events[] = $event;
    }
    public function releaseEvents(): array
    {
        return $this->events;
    }
    public function clearEvents(): void
    {
        $this->events = [];
    }
}

final class Product implements RecordsEvents
{
    use EventRecordingCapabilities;
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Don’t forget to close all your classes down for inheritance:

  • Mark them as final and make all properties
  • Methods private, unless they are part of the public interface of the class

And that’s all, I hope you enjoy it and the last part of this series will be published soon!

Sources and more info

Discussion (0)

pic
Editor guide