Dividing responsibilities - Part 1

Posted on by Matthias Noback

I'm happy to share with you an excerpt of my latest book, which is currently part of Manning's Early Access Program. Take 37% off Object Design Style Guide by entering fccnoback into the discount code box at checkout at manning.com.

Chapter 7: Dividing responsibilities

We've looked at how objects can be used to retrieve information, or perform tasks. The methods for retrieving information are called query methods, the ones that perform tasks are command methods. Service objects may combine both of these responsibilities. For instance, a repository (like the one in Listing 1) could perform the task of saving an entity to the database, and at the same time it would also be capable of retrieving an entity from the database.

Listing 1. The PurchaseOrderRepository can save and retrieve a PurchaseOrder entity.

interface PurchaseOrderRepository
{
    /**
     * @throws CouldNotSavePurchaseOrder
     */
    public function save(PurchaseOrder purchaseOrder): void;

    /**
     * @throws CouldNotFindPurchaseOrder
     */
    public function getById(int purchaseOrderId): PurchaseOrder;
}

Since saving and retrieving an entity are more or less each other's inverse operations, it's only natural to let one object have both responsibilities. However, in most other cases you will find that performing tasks and retrieving information are better off being divided amongst different objects.

Separate write models from read models

As we saw earlier, there are services, and other objects. Some of these other objects can be characterized as Entities, which model a particular domain concept. In doing so, they contain some relevant data, and offer ways to manipulate that data in valid and meaningful ways. Entities can also expose data, allowing clients to retrieve information from them, whether that is exposed internal data (like the date on which an order was placed), or calculated data (like the total amount of the order).

In practice, it turns out that different clients use entities in different ways. Some clients will want to manipulate an entity's data using its command methods, while others just want to retrieve a piece of information from it using its query methods. Nevertheless, all these clients will share the same object, and potentially have access to all the methods, even when they don't need them, or shouldn't even have access to them.

You should never pass an entity that can be modified to a client that isn't allowed to modify it. Even if the client doesn't modify it today, one day it might, and then it will be hard to find out what happened. That's why the first thing you should do to improve the design of an entity, is separate the Write model from the Read model.

We'll find out how to accomplish this by looking at an example of a PurchaseOrder entity (Listing 2). A purchase order represents the fact that a company buys a product from one of its suppliers. Once the product has been received, it's shelved in the company's warehouse. From that moment on the company has this product in stock. We'll use the same example for the remaining part of this chapter and work out different ways to improve it.

Listing 2. The PurchaseOrder entity.

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

    private function __construct()
    {
    }

    public static function place(                
        int purchaseOrderId,
        int productId,
        int orderedQuantity
    ): PurchaseOrder {
        /*
         * For brevity, we use primitive type values, while in 
         * practice, the use of value objects is recommended.
         */

        purchaseOrder = new self();

        purchaseOrder.productId = productId;
        purchaseOrder.orderedQuantity = orderedQuantity;
        purchaseOrder.wasReceived = false;

        return purchaseOrder;
    }

    public function markAsReceived(): void
    {
        this.wasReceived = true;
    }

    public function purchaseOrderId(): int
    {
        return this.purchaseOrderId;
    }

    public function productId(): int
    {
        return this.productId;
    }

    public function orderedQuantity(): int
    {
        return this.orderedQuantity;
    }

    public function wasReceived(): bool
    {
        return this.wasReceived;
    }
}

In the current implementation, the PurchaseOrder entity exposes methods for creating and manipulating the entity (place() and markAsReceived(), as well as for retrieving information from it (productId(), orderedQuantity() and wasReceived()). Now take a look at how different clients use this entity. First, the ReceiveItems service (Listing 3), which will be called from a controller, passing in a raw purchase order ID.

Listing 3. The ReceiveItems service.

final class ReceiveItems
{
    private PurchaseOrderRepository repository;

    public function __construct(PurchaseOrderRepository repository)
    {
        this.repository = repository;
    }

    public function receiveItems(int purchaseOrderId): void
    {
        purchaseOrder = this.repository.getById(purchaseOrderId);

        purchaseOrder.markAsReceived();

        this.repository.save(purchaseOrder);
    }
}

Note that this service doesn't use any of the getters on PurchaseOrder. It's only interested in changing the state of the entity. Next, let's take a look at a controller which renders a JSON-encoded data structure detailing how much of a product the company has in stock (Listing 4).

Listing 4. The StockReportController class.

final class StockReportController
{
    private PurchaseOrderRepository repository;

    public function __construct(PurchaseOrderRepository repository)
    {
        this.repository = repository;
    }

    public function execute(Request request): Response
    {
        allPurchaseOrders = this.repository.findAll();

        stockReport = [];

        foreach (allPurchaseOrders as purchaseOrder) {
            /*
             * We didn't receive the items yet, so we shouldn't add 
             * them to the quantity-in-stock. */
            if (!purchaseOrder.wasReceived()) {                
                continue;
            }

            if (!isset(stockReport[purchaseOrder.productId()] )) {
                /*
                 * We didn't see this product before...
                 */ 
                stockReport[purchaseOrder.productId()] = 0;
            }

            /*
             * Add the ordered (and received) quantity to the
             * quantity-in-stock.
             */
            stockReport[purchaseOrder.productId()]
                +== purchaseOrder.orderedQuantity;
        }

        return new JsonResponse(stockReport);
    }
}

This controller doesn't make any change to a PurchaseOrder. It just needs a bit of information from all of them. In other words, it isn't interested in the write part of the entity, only in the read part. Besides the fact that it is undesirable to expose more behavior to a client than it needs, it isn't very efficient to loop over all purchase orders of all times, to find out how much of a product the company has in stock.

The solution is to divide the entity's responsibilities. First, we create a new object that can be used for retrieving information about a purchase order. Let's call it PurchaseOrderForStockReport (Listing 5).

Listing 5. The PurchaseOrderForStockReport class.

final class PurchaseOrderForStockReport
{
    private int productId;
    private int orderedQuantity;
    private bool wasReceived;

    public function __construct(
        int productId,
        int orderedQuantity,
        bool wasReceived
    ) {
        this.productId = productId;
        this.orderedQuantity = orderedQuantity;
        this.wasReceived = wasReceived;
    }

    public function productId(): ProductId
    {
        return this.productId;
    }

    public function orderedQuantity(): int
    {
        return this.orderedQuantity;
    }

    public function wasReceived(): bool
    {
        return this.wasReceived;
    }
}

This new PurchaseOrderForStockReport object can be used inside the controller as soon as there is a repository which can provide it. A quick and dirty solution would be to let PurchaseOrder return an instance of PurchaseOrderForStockReport, based on its internal data, like in Listing 6).

Listing 6. A quick solution: PurchaseOrder generates the report.

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

    // ...

    public function forStockReport(): PurchaseOrderForStockReport
    {
        return new PurchaseOrderForStockReport(
            this.productId,
            this.orderedQuantity,
            this.wasReceived
        );
    }
}

final class StockReportController
{
    private PurchaseOrderRepository repository;

    public function __construct(PurchaseOrderRepository repository)
    {
        this.repository = repository;
    }

    public function execute(Request request): Response
    {
        /*
         * For now, we still load `PurchaseOrder` entities.
         */
        allPurchaseOrders = this.repository.findAll();           

        /*
         * But we immediately convert them to 
         * `PurchaseOrderForStockReport` instances.
         */
        forStockReport = array_map(                                 
            function (PurchaseOrder purchaseOrder) {
                return purchaseOrder.forStockReport();
            },
            allPurchaseOrders
        );

        // ...
    }
}

We can now remove pretty much all of the query methods (productId(), orderedQuantity(), wasReceived()) from the original PurchaseOrder entity (see Listing 7). This makes it a proper write model; it isn't used by clients who just want information from it anymore.

Listing 7. PurchaseOrder with its getters removed.

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

    private function __construct()
    {
    }

    public static function place(
        int purchaseOrderId,
        int productId,
        int orderedQuantity
    ): PurchaseOrder {
        purchaseOrder = new self();

        purchaseOrder.productId = productId;
        purchaseOrder.orderedQuantity = orderedQuantity;

        return purchaseOrder;
    }

    public function markAsReceived(): void
    {
        this.wasReceived = true;
    }
}

Removing these query methods won't do any harm to the existing clients of PurchaseOrder that use this object as a write model, like the ReceiveItems service we saw earlier (Listing 8).

Listing 8. Existing clients use PurchaseOrder as a write model.

final class ReceiveItems
{
    // ...

    public function receiveItems(int purchaseOrderId): void
    {
        /*
         * This service doesn't use any query method of 
         * `PurchaseOrder`.
         */
        purchaseOrder = this.repository.getById(            
            PurchaseOrderId.fromInt(purchaseOrderId)
        );

        purchaseOrder.markAsReceived();

        this.repository.save(purchaseOrder);
    }
}

Some clients use the entity as a write model, but still need to retrieve some information from it. For instance in order to make decisions, perform extra validations, etc. Don't feel blocked to add more query methods in these cases; query methods aren't by any means forbidden. The point of this chapter is that clients that solely use an entity to retrieve information from it, should use a dedicated read model instead of a write model.

Create read models that are specific for their use cases

In the previous section, we decided to split the PurchaseOrder entity into a write and a read model. The write model still carries the old name, but we called the read model PurchaseOrderForStockReport. The extra qualification ForStockReport indicates that this object now serves a specific purpose. The object will be suitable for use in a very specific context, namely the context where we arrange the data in such a way that we can produce a useful stock report for the user. The proposed solution isn't optimal yet, because the controller still needs to load all the PurchaseOrder entities, then convert them to PurchaseOrderForStockReport instances by calling forStockReport() on them (see Listing 9). This means that the client still has access to that write model, even though our initial goal was to prevent that from happening:

Listing 9. Creating a stock report still relies on the write model.

public function execute(Request request): Response
{
    /*
     * We still rely on `PurchaseOrder` instances here.
     */
    allPurchaseOrders = this.repository.findAll();     

    forStockReport = array_map(
        function (PurchaseOrder purchaseOrder) {
            return purchaseOrder.forStockReport();
        },
        allPurchaseOrders
    );

    // ...
}

There is another aspect of the design that isn't quite right: even though we now have PurchaseOrderForStockReport objects, we still need to loop over them and build up yet another data structure, before we can present the data to the user. What if we had an object whose structure completely matched the way we intend to use it? Concerning the name of this object, there's already a hint in the name of the read model (ForStockReport). So let's call this new object StockReport, and assume it already exists. The controller would become much simpler now, as shown in Listing 10).

Listing 10. StockReportController can retrieve the stock report directly.

final class StockReportController
{
    private StockReportRepository repository;

    public function __construct(StockReportRepository repository)
    {
        this.repository = repository;
    }

    public function execute(Request request): Response
    {
        stockReport = this.repository.getStockReport();

        /*
         * `asArray()` is expected to return an array like we the one 
         * we created manually before.
         */
        return new JsonResponse(stockReport.asArray());    
    }
}

Besides StockReport we may create any number of read models which correspond to each of the application's specific use cases. For instance, we could create a read model that's used for listing purchase orders only. It would expose just the ID and the date on which it was created. We could then have a separate read model that provides all the details needed to render a form that allows the user to update some of its information; and so on.

Behind the scenes, the StockReportRepository could still create the StockReport object based on PurchaseOrderForStock objects provided by the write model entities. But there are much better and more efficient alternatives to do it. We'll cover some of them in the following sections.

Continue reading part 2

If you want to learn more about the book, check it out here on liveBook. Stay tuned for part two.

PHP object design CQRS