In the previous parts of this series we looked at how to get rid of complexity at the level of algorithms. After discussing the problem of nulls in your code, we looked atobject lifecycles and how to encapsulate them properly. Now that we have objects that can be constructed and changed only in valid ways, we need to look at how they communicate with each other and how we can improve our code with regard to that aspect.

OBJECT COMMUNICATION

METHOD CALLS

Whenever one object calls a method on another object, they communicate: there is a sender, a receiver, and a message being passed. The message is of a certain type and has certain values, which make the message unique. When we call doSomething()on the object in $a, we send a message to it, which will be processed by the object in$a:

$a->doSomething($b, $c, $d);

When we call the same method again, even if we supply the same arguments, the message will be processed again, which makes the message unique:

// same values, new message:
$a->doSomething($b, $c, $d);

This becomes more clear when we apply the Introduce parameter object refactoring:

$message1 = new Message($b, $c, $d);
$a->doSomething($message1);
$message2 = new Message($b, $c, $d);
$a->doSomething($message2);

Note that even if we don't supply any arguments, calling a method is still about communicating a message:

$a->doSomethingElseWhichRequiresNoInformation();

RETURN VALUES

When the receiver of a message wants to send some message back to the sender, it can do so by defining a return value:

function whereIs(Person $person) {
    ...
    return new Location(...);
}

The return value itself is again a message with a certain type and a particular value.

MESSAGE CATEGORIES

Besides having a type and a particular value, messages can also be categorized:

  • command message is a request for change. One object wants to make the other object do something.
  • query message is a request for information. One object wants to get something from the other object.
  • document message is the response from the other object, based on the querythat was sent to it.

We saw examples of these message categories in the code samples above. When we called $a->doSomething(...) we actually sent a command message to it. And calling whereIs(...) means sending a query message. The response of whereIs()- a Location object, is a document message.

Each category comes with its own, mutually exclusive intentions:

  • command message comes with the intention to change the state of the application.
  • query message comes with the intention to get some information about the current state, not to change the state.
  • document message is actually neutral with regard to its intention.

COMMAND/QUERY SEPARATION PRINCIPLE

Once we know and understand the different types of messages that are being communicated between objects, we can improve the design of these objects by being explicit about whether a method supports command or query methods, and - when they are a query - whether they return something. We have to be strict about this in the implementation of our methods:

  • command method accepts a request for change, but doesn't return anything.
  • query method accepts a request for information, and returns that information (which will be a document message). It doesn't change the observable state of the object.

This is known as the Command/query separation principle, abbreviated as CQS. When strictly applied this has several advantages:

  • You can be sure that there won't be any surprises (i.e. side effects) if you call a query method.
  • You can optimize a query method, for example by caching the return value.
  • You can call query methods in any order, because you don't rely on side-effects (anymore).

IMPLEMENTING COMMANDS

Practical implications for implementing command functions/methods according to the CQS principle are:

  • A command function has no return value.
  • A command function is supposed to execute successfully. If you want to communicate "failure", throw an exception.

A command function looks like this:

function doSomething(/* any number of parameters here */) {
    if (/* problematic situation */) {
        throw new Exception(...);
    }
    if (/* nothing to do */) {
        return;
    }
    // happy path, do things
    ...
    // nothing here
}

Some suggestions for implementing command functions:

  • Pick a function name with an imperative word in it (like "do", "persist", etc.). The name of the function should reflect the fact that it takes orders.
  • Throw exceptions that are as specific as possible or needed. At least make a distinction between logical exceptions, like configuration mistakes, or invalid arguments, and runtime exceptions, like connection problems.
  • When returning early, don't return null;, just return;, thereby expressing the fact that there is no specific return value; you're just breaking the regular execution flow.

When documenting these functions, you should mention the fact that it has no return value:

/**
 * @return void
 */

You may optionally mention exceptions that can be thrown by this function:

/**
 * @throws SomeSpecificException
 */

Usually only interfaces need such a detailed specification of the exceptions that can be thrown, offering some suggestions to the implementer.

QUERIES

The opposite of command methods are query methods. The implementation of a query methods has the following characteristics:

  • A query function has a specific return value.
  • A query function is supposed to return a value of that specific type. If it's unable to do so, it throws an exception.

The general structure of a query method looks like this:

/**
 * @return SomeSpecificTypeOfValue
 */
function getSomething(/* any number of parameters */) {
    if (/* we can't satisfy our client */) {
        throw new Exception(...);
    }
    return $somethingOfTheExpectedType;
}

Instead of throwing an exception you could also choose to offer a fallback value, which is nevertheless of the expected type, like an empty array:

/**
 * @return SomeSpecificTypeOfValue[]
 */
function getThoseThings(/* any number of parameters */) {
    if (/* we can't satisfy our client */) {
        return [];
    }
    return [...];
}

You may recognize these patterns from a previous post on ways to get rid of null

DOCUMENTS

Anything that is returned by a query function should be considered a documentmessage. You can model it in any way: a primitive (in PHP: scalar) value, an array, or an object. As mentioned previously it should be considered a neutral message: it doesn't come with any intention for the recipient. The receiver of a document message isn't required to do anything with it.

Whenever you return document messages, you need to be careful about the mutability of the message itself. In general, you shouldn't allow the receiver to modify the message. Maybe the sender still carries a reference to the original message object, or maybe the message is sent to other receivers as well. As mentioned in theprevious article, only make objects mutable that are consciously designed to undergo changes.

COMMAND QUERY RESPONSIBILITY SEGREGATION

We've discussed CQS (Command Query Separation) already: object methods should be either command or query methods. Another design principle, called CQRS (Command Query Segregation Principle), takes this one step further. It brings the separation of command and query methods to the object level: objects have either command methods or query methods. This means there will be objects that accept change requests and there are different objects which supply information:

class Entity
{
    private $id;
    private $something;
    public function changeSomething($newValue)
    {
        $this->something = $something;
    }
    ...
}
class SummarizedRepresentationOfTheEntity
{
    private $id;
    private $something;
    public function getId()
    {
        return $this->id;
    }
    public function getSomething()
    {
        return $this->something;
    }
    ...
}

The ultimate application of CQRS is in the separation of write from read models, including their underlying storage facilities. The main question is of course: how will the correct data, provided when calling changeSomething() eventually end up being returned by getSomething()?

This supposed dilemma can be solved by introducing events.

EVENTS

Events should be considered a fourth category of messages, separate from command, query and document messages. An event message is used to record whatever has changed when a command message was processed. That's why they are much likedocument messages. Events are different though, since they are not the answer to a query message.

Events are used to record changes and then broadcast them. Event listeners can respond to new events and project the changes they represent onto read models.

For example, inside our Entity class, a call to changeSomething() causes an event to be recorded:

class Entity
{
    private $id;
    private $events = [];
    public function changeSomething($newValue)
    {
        $this->events[] = new SomethingChanged($this->id, $newValue);
    }
    public function getRecordedEvents()
    {
        return $this->events;
    }
}

The event class itself might look something like this:

class SomethingChanged
{
    private $id;
    private $something;
    public function __construct($id, $something)
    {
        $this->id = $id;
        $this->something = $something;
    }
    public function id()
    {
        return $this->id;
    }
    public function something()
    {
        return $this->something;
    }
}

An event listener could respond to the SomethingChanged event and replace corresponding read model instances:

class UpdateSummarizedRepresentationOfTheEntity
{
    public function whenSomethingChanged(SomethingChanged $event)
    {
        $listingObject = $this->readModelRepository
            ->getById($event->id());
        // create a new SummarizedRepresentationOfTheEntity instance
        $replaceWith = ...;
        $this->readModelRepository
            ->save($replaceWith);
    }
}

There are some interesting remarks to be made on the above examples:

  • An event contains historical data: what happened (i.e. changed) at a certain point is represented by the type of and values inside the event object.
  • Because of their historical nature, event objects are immutable: what happened can not be changed.
  • The event object carries the data from the command. It also carries an identifier, which allows one to correlate events with the object in which they occurred.
  • We could add any number of event listeners which each update their own read models. Since the read models are the ones with the query methods, we can make them as simple or performant as needed.
  • The changes made to an object are not instantly reflected by the values returned by any of the available read models. This means that data is eventually consistent.

EVENT SOURCING

Recording changes in "command" objects allows for some very interesting opportunities. Instead of only having access to the current state of things, you could remember any change that happened to this object, ever. The only thing you need to do is store the event objects in something appropriately called an event store. Whenever you reconstruct an object, you just replay any previous event inside the object to bring it back to the state in which you left it. Then you can call a command method on the object, which will cause a new event to be created, which will be appended to the event stream of the object.

One of the biggest advantages of this approach is that you can get invaluable insights from all the historical information that is now available.

CONCLUSION

In this article we've equated calling methods with sending messages. Command messages are supposed to cause a change inside an object. Query messages aren't supposed to change anything, only to return some information. That information should be considered a document message. We correctly apply the Command Query Separation principle when we strictly distinguish command functions from query functions.

When a change occurs inside an object, an event message may be sent to inform interested parties of that change. Separating objects that undergo changes from objects that give information is called Command Query Segregation principle.

https://www.ibuildings.nl/blog/2016/02/programming-guidelines-part-4-messages

PROGRAMMING GUIDELINES – PART 4: MESSAGES
标签: