Chapter 6. Retrieving information

This chapter covers

  • Using query methods for retrieving information
  • Using single, specific return types
  • Designing an object to keep internal data to itself
  • Introducing abstractions for query calls
  • Using test doubles for query calls

An object can be instantiated and sometimes modified. An object may also offer methods for performing tasks or retrieving information. This chapter describes how to implement methods that return information. In chapter 7 we’ll look at methods that perform a task.

6.1. Use query methods for information retrieval

Earlier, we briefly discussed command methods. These methods have a void return type and can be used to produce a side effect: change state, send an email, store a file, etc. Such a method shouldn’t be used for retrieving information. If you want to retrieve information from an object, you should use a query method. Such a method does have a specific return type, and it’s not allowed to produce any side effects.

Take a look at the Counter class.

Listing 6.1. The Counter class
final class Counter
{
    private int count = 0;

    public function increment(): void
    {
        this.count++;
    }

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

counter = new Counter();
counter.increment();

assertEquals(1, counter.currentCount());

According to the rules for command and query methods, it’s clear that increment() is a command method, because it changes the state of a Counter object. currentCount() is a query method, because it doesn’t change anything; it just returns the current value of count. The good thing about this separation is that given the current state of a Counter object, calling currentCount() will always return the same answer.

Consider the following alternative implementation of increment().

Listing 6.2. An alternative implementation of increment()
public function increment(): int
{
    this.count++;

    return this.count;
}

This method makes a change and returns information. This is confusing from a client perspective; the object changes even if you just want to look at it.

It’s better to have safe methods that can be called any time (and, in fact, can be called any number of times), and other methods that are “unsafe” to call. There are two ways to achieve this:

  • Follow the rule that a method should always be either a command or a query method. This is called the command/query separation principle (CQS).[1] We applied it in the initial implementation of Counter (listing 6.1): increment() was a command method, currentCount() a query method, and none of the methods of Counter were both command and query methods at the same time.

    1

    Martin Fowler, “CommandQuerySeparation” (2005), https://martinfowler.com/bliki/CommandQuerySeparation.html.

  • Make your objects immutable (as has been previously advised for almost all objects in your application).

If Counter were implemented as an immutable object, increment() would become a modifier method, and a better, more declarative name for it would be incremented().

Listing 6.3. An alternative Counter implementation
final class Counter
{
    private int count = 0;

    public function incremented(): Counter
    {
        copy = clone this;

        copy.count++;

        return copy;
    }

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

assertEquals(
    1,
    (new Counter()).incremented().currentCount()
);
assertEquals(
    2,
    (new Counter()).incremented().incremented().currentCount()
);
Is a modifier method a command or a query method?

A modifier method doesn’t really return the information you’re after. In fact, it returns a copy of the whole object, and once you have that copy, you can ask it questions. So modifiers don’t seem to be query methods, but they aren’t traditional command methods either. A command method on an immutable object would imply that it changes the object’s state after all, which isn’t the case. It produces a new object, which isn’t far from just answering a query.

Although it’s stretching the concept a bit, the incremented() method in listing 6.3 could answer the query “give me the current count, but incremented by 1.“

Exercises

  1. Which methods are expected to be query methods?

    1. name(): string
    2. changeEmailAddress(string emailAddress): void
    3. color(bool invert): Color
    4. findRecentMeetups(Date today): array

6.2. Query methods should have single-type return values

When a method returns a piece of information, it should return a predictable thing. No mixed types are allowed. Most languages don’t even support mixed types, but PHP, being a dynamically typed language does. Take, for example, the following isValid() method, which omits a return type, allowing several types of things to be returned. This will be very confusing for its users.

Listing 6.4. isValid() is a confusing method
/**
 * @return string|bool
 */
public function isValid(string emailAddress)
{
    if (/* ... */) {
        return 'Invalid email address';
    }

    return true;
}

If the provided email address is valid, isValid() will return true; otherwise it will return a string. This makes it hard to use the method. Instead, make sure always to return values of a single type.

There’s another situation to discuss here. Take a look at the following method that doesn’t have multiple return types (its single return type is a Page object), but it may alternatively return null:

public function findOneBy(type): Page?
{
}

This puts a burden on the caller: they will always have to check whether the returned value is a Page object or if it’s null, and they will need to deal with that.

if (page instanceof Page) {
    // ...                    1
} else {
    // ...                    2
}

  • 1 page is a Page object and can be used as such.
  • 2 page is null, and we have to decide what to do with it.

Returning null from a method isn’t always a problem. But you have to make sure that the clients of the method will deal with this situation. For PHP, static analysis tools like PHPStan (https://github.com/phpstan/phpstan) and Psalm (https://github.com/vimeo/psalm) can verify this, and your IDE may also help you with it, telling you when you are risking a possible null pointer exception. For Java, there’s the Checker Framework (https://checkerframework.org/), which will provide you with compile-time warnings that clients don’t deal with a possible null value.

In most cases, though, it pays to consider alternatives to returning null. For example, the following getById() method, which is supposed to retrieve a User entity by its ID, shouldn’t return null if it can’t find the user. It should throw an exception. After all, a client expects the User to exist; it’s even providing the user’s ID. It won’t take null for an answer.

Listing 6.5. getById() returns a User or throws an exception
public function getById(id): User
{
    user = /* ... */;

    if (!user instanceof User) {
        throw UserNotFound.withId(id);
    }

    return user;
}

Another alternative would be to return an object that can represent the null case. Such an object is called a null object. Clients won’t have to check for null, since the object has the correct type, as defined in the method signature.

Listing 6.6. Return a null object if it makes sense
public function findOneByType(PageType type): Page
{
    page = /* ... */;           1
 
    if (!page instanceof Page) {
        return new EmptyPage();
    }

    return page;
}

  • 1 Try to find the Page.
Show the uncertainty in the name of the method

You can let a method name indicate the uncertainty about whether or not the method will return a value of the expected type. In the previous examples, we used getById() instead of findById(), to communicate to the client that the method will “get” the User, instead of trying to find it and possibly returning empty-handed.

One last alternative for returning null is to return a result of the same type, but representing the empty case. If the method is expected to find and return a number of things in an array, return an empty array if you couldn’t find anything.

Listing 6.7. Instead of null, return an empty list
public function eventListenersForEvent(string eventName): array
{
    if (!isset(this.listeners[eventName])) {
        return [];
    }

    return this.listeners[eventName];
}

Other return types will have different “empty” cases. For instance, if the return type is int, the empty return value might be 0 (or maybe 1), and for strings it might be '' (or maybe 'N/A').

If you find yourself using an existing method that mixes return types, or that returns null when it should return something more reliable, it might be a good idea to write a new method that sets things straight. In the following example, the existing findOneByType() method returns a Page object or null. If we want to make sure that clients don’t have to deal with the null case and will actually get a Page object, we could wrap a call to findOneByType() in a new method called getOneByType.

Listing 6.8. getOneByType() wraps findOneByType(), which could return null
public function getOneByType(PageType type): Page
{
    page = this.findOneByType(type);

    if (!page instanceof Page) {
        throw PageNotFound.withType(type);     1
    }

    return page;
}

  • 1 Don’t return null; throw an exception instead.
Exercises

  1. What is true about a query method?

    1. It should produce an observable side effect.
    2. It should not return a mixed type (e.g., bool|int).
    3. It sometimes makes sense to return null from it.
    4. It sometimes makes sense to return nothing (void) from it.

6.3. Avoid query methods that expose internal state

The simplest implementation for query methods is usually to return a property of the object. These methods are known as getters, and they allow clients to “get” the object’s internal data.

“But according to the JavaBean conventions, every property needs a getter!”

Indeed, the JavaBean conventions prescribe objects to have a zero-argument constructor and to define both a getter and a setter for every property (http://mng.bz/1woy). As you might guess, after reading the previous chapters, every style guide or rule proposed in this book is incompatible with this convention. A zero-argument constructor leads to objects that can be created with an invalid starting state. Allowing every setter to be called separately leads to equally invalid intermediate states. And allowing all of an object’s internal data to get out makes it hard to change anything about that data without breaking its clients. The only type of object that could be designed as a JavaBean is a data transfer object (we discussed those in sections 3.13 and 4.3).

For clients, usually the reason to get data is to use it for further calculations, or to make a decision based on it. Since objects are better off keeping their internals to themselves, you should keep an eye on these simple getters and how their return values are used by clients. The things a client does with the information that an object provides can often be done by the object itself.

A first example is the following getItems() method, which returns the items in a shopping basket just so the client can count them. Instead of directly exposing the items, the basket could provide a method that counts the items for the client.

Listing 6.9. Providing an alternative to counting items
// Before:

final class ShoppingBasket
{
    // ...

    public function getItems(): array
    {
        return this.items;
    }
}

count(basket.getItems());

// After

final class ShoppingBasket
{
    // ...

    public function itemCount(): int
    {
        return count(this.items);
    }
}

basket.itemCount();

The naming of query methods is important too. We didn’t use getItemCount() or countItems() because these method names sound like commands, telling the object to do something. Instead, we named the method itemCount(), which makes it look like the item count is an aspect of a shopping basket you can find out about.

How do you handle ambiguous naming?

What if your object has a name property? The getter for this property would be called name(), but “name” can also be a verb. In fact, we already encountered the same potential confusion when we used the word “count,” which can be a verb too.

Although the intended meaning of words will always be up for debate, most of the ambiguity can be resolved by setting the right context. Once you establish a clear difference between query and command methods, it’ll be easy to notice when a method is meant to return a piece of information (e.g., return the value of the name property), or is meant to change the state of the object (e.g., change the value of the name property and return nothing). This will provide the reader with an important clue as to whether the word should be interpreted as a verb (which is most often the case with a command method) or as a noun (which is often the sign of a query method).

With small rewrites like this, you can make an object absorb more and more logic, thereby keeping the knowledge about the concept it represents inside it, instead of scattering little bits of this knowledge all over the code base.

Here’s another example: clients call a query method on an object, and then call another method based on the return value of that first call.

Listing 6.10. Clients use the getters of Product to make decisions
final class Product
{
    public function shouldDiscountPercentageBeApplied(): bool      1
    {
        // ...
    }

    public function discountPercentage(): Percentage
    {
        // ...
    }

    public function fixedDiscountAmount(): Money
    {

    }
}

amount = new Money(/* ... */);
if (product.shouldDiscountPercentageBeApplied()) {                 2
    netAmount = product.discountPercentage().applyTo(amount);
} else {
    netAmount = amount.subtract(product.fixedDiscountAmount());
}

  • 1 A product has a setting that defines whether a discount percentage should be applied to it. If there’s no discount percentage, there can still be a fixed discount.
  • 2 Clients of Product can calculate a net amount by calling applyDiscountPercentage() first, and using its answer to either apply a discount percentage or a fixed discount.

One way to keep the information about how a discount should be calculated for a given product is to introduce a method with a name that matches this intention: calculateNetAmount().

Listing 6.11. calculateNetAmount() offers a better alternative
final class Product
{
    public function calculateNetAmount(Money amount): Money
    {
        if (this.shouldDiscountPercentageBeApplied()) {
            return this.discountPercentage().applyTo(amount);
        }

        return amount.subtract(this.fixedDiscountAmount());
    }

    private function shouldDiscountPercentageBeApplied(): bool   1
    {
        // ...
    }

    private function discountPercentage(): Percentage            1
    {
        // ...
    }
    private function fixedDiscountAmount(): Money                2
    {

    }
}

amount = new Money(/* ... */);
netAmount = product.calculateNetAmount(amount);

  • 1 These methods can stay private now, or maybe we could use the properties they expose.
  • 2 These methods can stay private now, or maybe we could use the properties they expose.

Besides no longer needing to repeat this logic at different call sites, this alternative has two more advantages. First, we can stop exposing internal data like the discount percentage and the fixed discount. Second, when the calculation changes, it can be changed and tested in one place.

In short, always look for ways to prevent the need for query methods that expose the object’s internal data:

  • Make the method smarter, and adapt it to the actual need of its clients.
  • Move the call inside the object, letting it make its own decisions.

These approaches will let the object keep its internals to itself, and force clients to use its explicitly defined public interface (see figure 6.1).

Figure 6.1. An object can be seen as having boundaries. Instead of allowing clients to cross those boundaries to retrieve information from the object, define explicitly which data and which behaviors should be available to clients.

A naming convention for getters

You may have noticed that I don’t use the traditional “get” prefix for getters, as in discountPercentage() (listing 6.11). This convention shows that the method isn’t a command method, but that it simply provides a piece of information. The method name is a description of the piece of information we’re looking for, not an instruction for the object to “go get it” for us.

Exercises

  1. Take a look at the following Order and Line classes and how they allow clients to get all the information they need to calculate a total amount for the entire order:
    final class Line
    {
        private int quantity;
        private Money tariff;
    
        // ...
    
        public function quantity(): int
        {
            return this.quantity;
        }
    
        public function tariff(): Money
        {
            return this.tariff;
        }
    }
    
    final class Order
    {
        /**
         * @var Line[]
         */
        private array lines = [];
    
        // ...
    
        /**
         * @return Line[]
         */
        public function lines(): array
        {
            return this.lines;
        }
    }
    
    totalAmount = new Money(0);
    foreach (order.lines() as line) {
        totalAmount = totalAmount.add(
            new Money(
                line.quantity() * line.tariff()
            )
    
        );
    }
    Rewrite Order and Line in such a way that Order is no longer forced to expose its internal lines array, any of the Line objects that are in this array, or the Line’s tariff and quantity.

6.4. Define specific methods and return types for the queries you want to make

When you need a specific bit of information, make sure you have a specific question, and that you know what the answer should look like. As an example, if you’re working on a piece of code and you need today’s exchange rate for USD to EUR, you may discover that there are web services you can call to figure that out, like https://fixer.io/. So you might jump in and write a bit of code that makes the call.

Listing 6.12. The CurrencyConverter class
final class CurrencyConverter
{
    public function convert(Money money, Currency to): Money
    {
        httpClient = new CurlHttpClient();
        response = httpClient.get(
            'http://data.fixer.io/api/latest?access_key=...' .
                '&base=' . money.currency().asString() .
                '&symbols=' . to.asString()
        );
        decoded = json_decode(response.getBody());
        rate = (float)decoded.rates[to.asString()];

        return money.convert(to, rate);
    }
}

There are many issues with this tiny bit of code (we won’t deal with the possibility of a network failure, an error response, invalid JSON, a modified response structure, and the fact that a float isn’t the most reliable data type to use when dealing with amounts of money). At a conceptual level, we’re making much too large jumps as well. All we needed at this point in the code is an answer to a question: “What’s the current exchange rate for USD to EUR currency conversion?”

Rewriting this question in code results in two new classes: FixerApi and Exchange-Rate. The first has a single method, exchangeRateFor(), which represents the question that the CurrencyConverter wants to ask. The second class, ExchangeRate, represents the answer.

Listing 6.13. The FixerApi and ExchangeRate classes
final class FixerApi
{
    public function exchangeRateFor(         1
        Currency from,
        Currency to
    ): ExchangeRate {
        httpClient = new CurlHttpClient();
        response = httpClient.get(/* ... */);
        decoded = json_decode(response.getBody());
        rate = (float)decoded.rates[to.asString()];

        return ExchangeRate.from(from, to, rate);
    }
}

final class ExchangeRate                   2
{
    public static function from(
        Currency from,
        Currency to,
        float rate
    ): ExchangeRate {
        // ...
    }
}

final class CurrencyConverter              3
{
    private FixerApi fixerApi;

    public function __construct(FixerApi fixerApi)
    {
        this.fixerApi = fixerApi;
    }

    public function convert(Money money, Currency to): Money
    {
        exchangeRate = this.fixerApi
            .exchangeRateFor(
                money.currency(),
                to
            );

        return money.convert(exchangeRate);
    }
}

  • 1 We introduce FixerApi.exchangeRateFor() to represent the question being asked: “What’s the current exchange rate for converting from … to … ?”
  • 2 This new class will represent the answer to the question.
  • 3 CurrencyConverter will get a FixerApi instance injected, so it can find out the current exchange rate when it needs to.

The “answer” class, ExchangeRate, should be designed to be as useful as possible for the client that needs it. Potentially, this class can be reused at other call sites, but it doesn’t have to be.

The important part is that the introduction of the exchangeRateFor() method with a specific return type improves the conversation that’s going on in the code. When reading the code of convert(), we can clearly see that there’s a need for information, a question being asked, and an answer being returned, which is then used to do some more work. Note that so far we’ve only refactored the code; its structure has been improved, but it still has the same behavior.

6.5. Define an abstraction for queries that cross system boundaries

The question “What’s the current exchange rate?” from the previous section is a question that the application itself can’t answer, based on what it has in memory. It needs to cross a system boundary to find the answer. In this case, it has to connect to a remote service, reachable through the network. Another example of crossing a system boundary would be when an application reaches out to the filesystem to load or store a file. Or when it uses the system clock to find out the current time.

As soon as an application crosses a system boundary, you should introduce an abstraction, allowing you to hide the low-level communication details of the calls that are going on behind the scenes.

Abstraction in this case means two things, and it can only be successful when you have both ingredients:

  • Using a service interface instead of a service class
  • Leaving out the implementation details

Introducing a proper abstraction will make it possible to run your code in a test scenario, without making the actual network or filesystem calls. It will also make it possible to swap out implementations without having to modify the client code; you only need to write a new implementation of the service interface.

First we’ll discuss an example that fails to introduce a proper abstraction. Let’s take another look at the FixerApi class. It makes a network call directly, using the CurlHttpClient class.

Listing 6.14. Using a CurlHttpClient instance to connect to the API
final class FixerApi
{
    public function exchangeRateFor(
        Currency from,
        Currency to
    ): ExchangeRate {
        httpClient = new CurlHttpClient();
        response = httpClient.get(/* ... */);
        decoded = json_decode(response.getBody());
        rate = (float)decoded.rates[to.asString()];

        return ExchangeRate.from(from, to, rate);
    }
}

Instead of instantiating and using this specific class, we could define an interface for it and inject an instance of it into the FixerApi class, as follows.

Listing 6.15. Adding an HttpClient interface and using it in FixerApi
interface HttpClient                                     1
{
    public function get(url): Response;
}

final class CurlHttpClient implements HttpClient         2
{
    // ...
}

final class FixerApi
{
    public function __construct(HttpClient httpClient)   3
    {
        this.httpClient = httpClient;
    }

    public function exchangeRateFor(
        Currency from,
        Currency to
    ): ExchangeRate {
        response = this.httpClient.get(/* ... */);       4
        decoded = json_decode(response.getBody());
        rate = (float)decoded.rates[to.asString()];

        return ExchangeRate.from(from, to, rate);
    }
}

  • 1 First we introduce an interface for HTTP clients.
  • 2 We also make sure the existing CurlHttpClient implements this new HttpClient interface.
  • 3 We inject the interface, not the concrete class.
  • 4 We have to change the code a bit to use the new interface and its get() method.

We can now swap out HttpClient implementations because we rely on the interface, not the concrete implementation. This could be useful if you may want to switch to a different HTTP client implementation some day. But we haven’t abstracted the most important part yet. What happens if we want to switch to a different API? It’s not likely that a different API will send the same JSON response. Or maybe we will want to start maintaining our own local database table with exchange rates. In that case, we wouldn’t need an HTTP client anymore.

To remove the low-level implementation details, we need to pick a more abstract name that stands for what we’re doing. We’re looking for a way to retrieve exchange rates. Where would we get them from? From something that can “provide” them. Or from something that manages them, like a “collection.” A good name for this abstraction could be ExchangeRateProvider, or simply ExchangeRates if we look at this service like a collection of known exchange rates. The following listing shows what this would look like.

Listing 6.16. Introducing the abstract ExchangeRates service
/**
 * We extract the "question" method and make it a public method on
 * an abstract `ExchangeRates` service:
 */
interface ExchangeRates
{
    public function exchangeRateFor(
        Currency from,
        Currency to
    ): ExchangeRate;
}

final class FixerApi implements ExchangeRates                     1
{
    private HttpClient httpClient;

    public function __construct(HttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    public function exchangeRateFor(
        Currency from,
        Currency to
    ): ExchangeRate {
        response = this.httpClient.get(/* ... */);
        decoded = json_decode(response.getBody());
        rate = (float)decoded.data.rate;

        return ExchangeRate.from(from, to, rate);
    }
}

final class CurrencyConverter
{
    private ExchangeRates exchangeRates;

    public function __construct(ExchangeRates exchangeRates)      2
    {
        this.exchangeRates = exchangeRates;
    }

    // ...

    private function exchangeRateFor(
        Currency from,
        Currency to
    ): ExchangeRate {
        return this.exchangeRates.exchangeRateFor(from, to);      3
    }
}

  • 1 The existing FixerApi class should implement the new ExchangeRates interface.
  • 2 Instead of a Fixer object, we can now inject an ExchangeRates instance.
  • 3 We use the new service here to get the answer we’re looking for.

As a final improvement, we should inline any existing calls to the private exchange-RateFor() method because it’s just a proxy to the ExchangeRates service now.

By defining an interface for the existing class, we performed the first step of a successful abstraction. By hiding all the implementation details behind the interface, we also performed the second step, meaning we now have a proper abstraction for retrieving exchange rates. This comes with two advantages:

  • We can easily switch to a different exchange rate provider. As long as the new class implements the ExchangeRates interface correctly, the Currency-Converter won’t have to be modified because it depends on the ExchangeRates abstraction.
  • We can write a unit test for CurrencyConverter and inject a test double for ExchangeRates—one that doesn’t make an internet connection. This will keep our test fast and stable.

By the way, if you know about the SOLID principles, you’ve already encountered a similar rule for abstraction of service dependencies, known as the dependency inversion principle. You can read more about it in books and articles by Robert C. Martin.[2]

2

For instance, Robert C. Martin, “The Dependency Inversion Principle,” http://mng.bz/9woa. Other articles about the SOLID principles can be found on http://mng.bz/j50y.

Not every question deserves its own service

In the previous examples, it was clear that the question, “What’s the exchange rate?” deserved its own service. It was a question the application itself couldn’t answer. In most situations, though, asking a question shouldn’t immediately cause a new object to be introduced. Consider these alternatives too:

  1. You could introduce better variable names to improve the conversation that’s going on inside the code.
  2. You could extract a private method, which represents the question and its answer (like we just did by moving logic to the private exchangeRateFor() method).

Only if the method becomes too large, needs to be tested separately, or crosses a system boundary should you create a separate class for it. This should keep the number of objects involved limited, and will help keep the code readable; you won’t have to click through lots of classes to find out what’s going on.

Exercises

  1. Which two things would you need to do to create an abstraction for a service?

    1. Create an abstract class for the service.
    2. Create an interface for the service.
    3. Choose higher-level names that leave room for implementers to keep lower-level implementation details to themselves.
    4. Provide at least two implementations for the abstract service.

6.6. Use stubs for test doubles with query methods

The moment you introduce an abstraction for your queries, you create a useful extension point. You can easily change the implementation details of how the answer will be found. Testing this logic will be easier too. Instead of only being able to test the CurrencyConverter service when an internet connection (and the remote service) is available, you can now test the logic by replacing the injected ExchangeRates service with one that already has the answers and will supply them in a predictable manner.

Listing 6.17. Testing CurrencyConverter with ExchangeRatesFake
final class ExchangeRatesFake implements ExchangeRates                  1
{
    private array rates = [];

    public function __construct(
        Currency from,
        Currency to,
        float rate
    ) {
        this.rates[from.asString()][to.asString()] =
            ExchangeRate.from(from, to, rate);
    }

    public function exchangeRateFor(
        Currency from,
        Currency to
    ): ExchangeRate {
        if (!isset(this.rates[from.asString()][to.asString()])) {
            throw new RuntimeException(
                'Could not determine exchange rate from [...] to [...]
            );
        }
        return this.rates[from.asString()][to.asString()];
    }
}

/**
 * @test
 */
public function it_converts_an_amount_using_the_exchange_rate(): void   2
{
    exchangeRates = new ExchangeRatesFake();                            3
    exchangeRates.setExchangeRate(
        new Currency('USD'),
        new Currency('EUR'),
        0.8
    );

    currencyConverter = new CurrencyConverter(exchangeRates);           4
 
    converted = currencyConverter
        .convert(new Money(1000, new Currency('USD')));

    assertEquals(new Money(800, new Currency('EUR')), converted);
}

  • 1 This is a “fake” implementation of the ExchangeRates service, which we can set up to return whatever exchange rates we provide it with.
  • 2 We can use this fake in the unit test for CurrencyConverter.
  • 3 Set up the fake ExchangeRates service.
  • 4 Inject the fake service into CurrencyConverter.

By setting up the test like this, we focus only on the logic of the convert() method, instead of all the logic involved in making the network connection, parsing the JSON response, etc. This makes the test deterministic and therefore stable.

Naming test methods

In listing 6.17, the test method has a name in so-called snake case: lower case with underscores as word separators. If we followed the standard for naming methods, it would have been itConvertsAnAmountUsingTheExchangeRate(). Most standards would also suggest using relatively short names, but it_converts_an_amount_using_the_exchange_rate() is anything but short. Because the purpose of test methods is different from that of regular methods, the solution is not to submit test method names to the same standard, but to set a different standard for them:

  1. Test method names describe object behaviors. The best description is an actual sentence.
  2. Because they are sentences, test method names will be longer than regular method names. It should still be easy to read them (so use snake case instead).

If you’re not used to these rules, a good way to ease into them is to start a test method name with it_. This should get you in the right mood for describing a particular object behavior. Although it’s a good starting point, you’ll notice that not every test method makes sense starting with it_. For instance, when_ or if_ could work too.

A fake is one kind of test double, which can be characterized as showing “somewhat complicated” behavior, just like the real implementation that will be used in production. When testing, you could also use a stub to replace a real service. A stub is a test double that just returns hardcoded values. So whenever we’d call the exchangeRateFor() method, it would return the same value, as follows.

Listing 6.18. ExchangeRatesStub always returns the same value
final class ExchangeRatesStub                       1
{
    public function exchangeRateFor(
        Currency from,
        Currency to
    ): ExchangeRate {
        return ExchangeRate.from(from, to, 1.2);    2
    }
}

  • 1 This is a sample stub implementation of ExchangeRates.
  • 2 The return value is hardcoded.

An important characteristic of stubs and fakes is that in a test scenario, you can’t and shouldn’t make any assertions about the number of calls made to them, or the order in which those calls are made. Given the nature of query methods, they should be without side effects, so it should be possible to call them any number of times, even zero times. Making assertions about calls made to query methods leads to tests that don’t keep sufficient distance from the implementation of the classes they’re testing.

The opposite is the case for command methods, where you do want to verify that calls have been made, how many have been made, and potentially in what order. We’ll get back to this in the next chapter.

Don’t use mocking tools for creating fakes and stubs

Mocking frameworks are often used to build test doubles on the fly. I recommend against using these frameworks for creating fakes and stubs. They may save you a few lines of boilerplate, but at the cost of code that is hard to read and maintain.

Even if you still prefer to use these mocking tools, I recommend using them only for creating dummies (that is, test doubles that don’t return anything meaningful and are only there to be passed as unused arguments). For stubs and fakes, mocking tools usually get in the way of good design. They will verify if and how many times a query method has been called, but they often make refactoring harder, because method names often have to be provided as strings, and your refactoring tool may not recognize them as actual method names.

We shouldn’t forget to also test the real implementation that uses an HTTP connection to retrieve exchange rates. We have to test that it works correctly. But at that point we’re no longer worried about testing the conversion logic itself, but only that the implementation knows how to communicate well with the external service.

Listing 6.19. The test for FixerApi will be an integration test
/**
 * @test
 */
public function it_retrieves_the_current_exchange_rate(): void
{
    exchangeRates = new FixerApi(new CurlHttpClient());

    exchangeRate = exchangeRates.exchangeRateFor(
        new Currency('USD'),
        new Currency('EUR')
    );

    // Verify the result here...
}

You will find that this kind of test still needs some effort to make it more stable. You may have to set up your own exchange rate server that replicates the real one. Or you may be able to use a sandbox environment provided by the maintainers of the real service.

Note that this test doesn’t count as a unit test anymore: it doesn’t test the behavior of an object in memory. You could call this an integration test instead, since it tests the integration of an object with the thing in the world outside that it relies on.

6.7. Query methods should use other query methods, not command methods

As we discussed, command methods can have side effects. Command methods change something, save something, send an email, etc. Query methods, on the other hand, won’t do anything like that. They will just return a piece of information. Usually, a query method needs some collaborating objects to build up the requested answer. If we get the division between command and query methods right in our code, a chain of calls that starts with a query won’t contain a call to a command method. This has to be true, because queries are supposed to have no side effects, and calling a command method somewhere in the chain will violate that rule.

Figure 6.2. There should be no calls to command methods hidden behind a query.

There are some exceptions to this rule. Consider a controller method for a web application, which can be called to register a new user. This method will have a side effect: somewhere down the chain of commands it will store a new user record in the database. This would normally force us to use a void return type for the controller itself, but a web application should always return an HTTP response. So the controller will have to return at least something.

Listing 6.20. A controller will always return something
final class RegisterUserController
{
    private RegisterUser registerUser;

    public function __construct(
        RegisterUser registerUser
    ) {
        this.registerUser = registerUser;
    }

    public function execute(Request request): Response
    {
        newUser = this.registerUser
            .register(request.get('username'));

        return new Response(200, json_encode(newUser));
    }
}

Technically speaking, the controller violates the command/query separation principle, but there’s no way around that. At the very least, we should return an empty 200 OK response or something like that. But that won’t be very useful for the frontend, which makes the “register user” POST request, and would like to be given a response with a JSON structure representing the newly created user.

To solve this case, you should divide the controller’s action into two parts: registering the new user and returning it. Preferably you’d also determine the new ID of the user before calling the RegisterUser service, so the service doesn’t have to return anything at all and can be a true command method. This is demonstrated in the following listing.

Listing 6.21. A controller can be separated into command and query parts
final class RegisterUserController
{
    private UserRepository userRepository;
    private RegisterUser registerUser;
    private UserReadModelRepository userReadModelRepository;

    public function __construct(
        UserRepository userRepository,
        RegisterUser registerUser,
        UserReadModelRepository userReadModelRepository
    ) {
        this.userRepository = userRepository;
        this.registerUser = registerUser;
        this.userReadModelRepository = userReadModelRepository;
    }

    public function execute(Request request): Response
    {
        userId = this.userRepository.nextIdentifier();

        this.registerUser
            .register(userId, request.get('username'));            1
 
        newUser = this.userReadModelRepository.getById(userId);    2
 
        return new Response(200, json_encode(newUser));
    }
}

  • 1 register() is a command method.
  • 2 getById() is a query method.
Sometimes, CQS does not make sense

In almost all cases I find that it’s best to follow the command/query separation principle, but it should not become a rule that you can’t deviate from. In fact, no programming rule should ever be like that.

A common situation where CQS stops being a good rule is in the realm of concurrency. An example would be the following nextIdentity() method. It generates a unique ID for an entity that you’re going to save. The ID is the next available number in the sequence 1, 2, 3, etc.

final class EntityRepository
{
    public function nextIdentity(): int
    {
        // ...
    }
}

Two clients that call this method shouldn’t receive the same ID, since that might result in them overwriting each other’s entity data. Calling nextIdentity() should return an integer and at the same time mark the returned integer as “used.” However, that would make the method violate CQS: it returns information and performs a task, thereby influencing the state of the system in an observable way. Calling the method again will give you a different answer.

You could figure out a way to still follow CQS, but I think that would complicate your code a lot.[a] In a case like this, feel free to let go of CQS and just implement the method in a way that makes sense.

a

To find out more about this particular situation and possible solutions, take a look at Mark Seemann’s article “CQS versus server generated IDs” (2014), http://mng.bz/Q0nQ.

Summary

  • A query method is a method you can use to retrieve a piece of information. Query methods should have a single return type. You may still return null, but make sure to look for alternatives, like a null object or an empty list. Possibly throw an exception instead. Let query methods expose as little of an object’s internals as possible.
  • Define specific methods and return values for every question you want to ask and every answer you want to get. Define an abstraction (an interface, free of implementation details) for these methods if the answer to the question can only be established by crossing the system’s boundaries.
  • When testing services that use queries to retrieve information, replace them with fakes or stubs you write yourself, and make sure not to test for actual calls being made to them.

Answers to the exercises

  1. Correct answers: a, c, and d. Answer b has a void return type, so it’s not a query method.
  2. Correct answers: b and c. A query method is explicitly not supposed to produce a side effect. A query method always has a return value, even if it’s null, so it can’t have a void return type.
  3. Suggested answer:
 final class Line
 {
     // ...                            1
  
     public function amount(): Money
     {
         return new Money(
             line.quantity() * line.tariff()
         );
     }
 }

 final class Order
 {
     // ...                            2
  
     public function totalAmount(): Money
     {
         totalAmount = new Money(0);

         foreach (this.lines() as line) {
             totalAmount = totalAmount.add(
                 line.amount()
             );
         }

         return totalAmount;
     }
 }

  • 1 It’s safe to remove tariff() and quantity() now, keeping this data private.
  • 2 It’s safe to remove lines() too, keeping the lines array and the Line objects private.

  1. Correct answers: b and c. An abstract class is not preferable, since it leaves part of the implementation defined, and it will be the same for any concrete subclass. Also, you don’t have to provide more than one implementation of the interface.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.162.204