Chapter 7. Performing tasks

This chapter covers

  • Using command methods to perform tasks
  • Using events and event listeners to split up larger tasks
  • Dealing with failure in command methods
  • Introducing abstractions for commands
  • Creating test doubles for command calls

Besides retrieving information from objects, you can use objects to perform a variety of tasks for you:

  • Send a reminder email
  • Save a record in the database
  • Change the password of a user
  • Store something on disk
  • And so on . . .

The following sections provide rules for methods that perform tasks like these.

7.1. Use command methods with a name in the imperative form

We already discussed query methods and how you should use them to retrieve information. Query methods have a specific return type and no side effects, meaning that it’s safe to call them several times, and the application’s state won’t be any different afterwards.

For performing tasks, you should always use a command method, which has a void return type. The name of such a method should indicate that the client can order the object to perform the task that the method name indicates. When looking for a good name, you should always use the imperative form. The following listing shows some examples.

Listing 7.1. Some command methods with imperative names
public function sendReminderEmail(
    EmailAddress recipient,
    // ...
): void {
   // ...
}

public function saveRecord(Record record): void
{
   // ...
}

7.2. Limit the scope of a command method, and use events to perform secondary tasks

When performing a task, make sure you don’t do too much in one method. These are some guiding questions to determine if a method is too large:

  • Should or does the method name have “and” in it, to indicate what else it does besides its main job?
  • Do all the lines of code contribute to the main job?
  • Could a part of the work that the method does be performed in a background process?

The following listing shows a method that does too much. It changes the user’s password, but it also sends them an email about it.

Listing 7.2. changePassword () does too much
public function changeUserPassword(
    UserId userId,
    string plainTextPassword
): void {
    user = this.repository.getById(userId);
    hashedPassword = /* ... */;
    user.changePassword(hashedPassword);
    this.repository.save(user);
    this.mailer.sendPasswordChangedEmail(userId);
}

This is a very common scenario where the answers to the guiding questions are “yes” in all cases:

  • The method name hides the fact that besides changing the user’s password it will also send an email. It might as well have been named changeUserPassword-AndSendAnEmailAboutIt().
  • Sending the email can’t be considered the main job of this method; changing the password is.
  • The email could easily be sent in some other process that runs in the background.

One solution would be to move the email-sending code to a new public sendPassword-ChangedEmail() method. However, this would transfer the responsibility of calling that method to the client of changeUserPassword(). Considering the bigger picture, these two tasks really belong together; we just don’t want to mix them in one method.

The recommended solution is to use events as the link between changing the password and sending an email about it.

Listing 7.3. Using an event to split a task into multiple parts
final class UserPasswordChanged                   1
                                 
{
    private UserId userId;

    public function __construct(UserId userId)
    {
        this.userId = userId;
    }

    public function userId(): UserId
    {
        return this.userId;
    }
}

public function changeUserPassword(
    UserId userId,
    string plainTextPassword
): void {
    user = this.repository.getById(userId);
    hashedPassword = /* ... */;
    user.changePassword(hashedPassword);
    this.repository.save(user);

    this.eventDispatcher.dispatch(                2
         new UserPasswordChanged(userId)
    );
}
final class SendEmail
{
    // ...

    public function whenUserPasswordChanged(     3
         UserPasswordChanged event
    ): void {
        this.mailer.sendPasswordChangedEmail(event.userId());
    }
}

  • 1 The fact that a user changed their password can be represented by a UserPasswordChanged event object.
  • 2 After changing the password, dispatch a UserPasswordChanged event so other services can respond to it.
  • 3 SendEmail is an event listener for the UserPasswordChanged event. When notified of the event, this listener will send the email.

You still need an event dispatcher that allows event listener services like SendEmail to be registered. Most frameworks have an event dispatcher that you can use, or you could write a simple one yourself, like the following.

Listing 7.4. A sample EventDispatcher implementation
final class EventDispatcher
{
    private array listeners;

    public function __construct(array listenersByType)
    {
        foreach (listenersByType as eventType => listeners) {
            Assertion.string(eventType);
            Assertion.allIsCallable(listeners);
        }

        this.listeners = listenersByType;
    }

    public function dispatch(object event): void
    {
        foreach (this.listenersFor(event.className) as listener) {
            listener(event);
        }
    }

    private function listenersFor(string event): array
    {
        if (isset(this.listeners[event])) {
            return this.listeners[event];
        }

        return [];
    }
}
listener = new SendEmail(/* ... */);
dispatcher = new EventDispatcher([
    UserPasswordChanged.className =>
        [listener, 'whenUserPasswordChanged']
]);

dispatcher.dispatch(new UserPasswordChanged(/* ... */));      1

  • 1 Because we’ve registered SendEmail as an event listener for the UserPasswordChanged event, dispatching an event of that type will trigger a call to SendEmail.whenUserPasswordChanged().

Using events like this has several advantages:

  • You can add even more effects without modifying the original method.
  • The original object will be more decoupled because it doesn’t get dependencies injected that are only needed for effects.
  • You can handle the effects in a background process if you want.

A possible disadvantage of using events is that the primary action and its secondary effects may be implemented in remote parts of the code base. This could make it hard for a future reader of the code to understand what’s going on. You should do two things to overcome this problem:

  • Make sure everybody knows that events are used to decouple parts of the application. Someone who tries to understand what the code is doing will then look out for event objects and use the IDE’s “find usages” functionality to find other services that are interested in these events.
  • Make sure that events are always explicitly dispatched, as is done in listing 7.3. The call to EventDispatcher.dispatch() is a strong signal that more is about to happen.
Exercises

  1. Which parts of the following command method could be considered secondary effects that could be handled in an event listener?
    final class RegisterUser
    {
        // ...
    
        public function register(
            EmailAddress emailAddress
            PlainTextPassword plainTextPassword
        ): void {
            hashedPassword = this.passwordHasher
                .hash(plainTextPassword);
    
            userId = this.userRepository.nextIdentity();
            user = User.create(userId, emailAddress, hashedPassword);
    
            this.mailer.sendEmailAddressConfirmationEmail(
              emailAddress
            );
    
            this.userRepository.save(user);
    
            this.uploadService.preparePersonalUploadFolder(userId);
        }
    }

    1. Hashing the plain-text password
    2. Creating the User entity
    3. Sending the email address confirmation mail
    4. Saving the User entity
    5. Preparing a personal upload folder for the user

7.3. Make services immutable from the outside as well as on the inside

We already covered the rule that it should be impossible to change anything about a service’s dependencies or configuration. Once it’s been instantiated, a service object should be reusable for performing multiple different tasks in the same way, but using different data or a different context. There shouldn’t be any risk that its behavior changes between calls. This is true for services that offer query methods, but also for ones that offer command methods.

Even if you don’t offer clients a way to manipulate a service’s dependencies or configuration, command methods may still change a service’s state in such a way that behavior will be different for subsequent calls. For example, the following Mailer service sends out confirmation emails, but it also remembers which users have already received such an email. No matter how many times you call the same method, it will only send out an email once.

Listing 7.5. A Mailer service that keeps a list of previous recipients
final class Mailer
{
    private array sentTo = [];

    // ...

    public function sendConfirmationEmail(
        EmailAddress recipient
    ): void {
        if (in_array(recipient, this.sentTo)) {
            return;                                              1
        }
        // Send the email here...

        this.sentTo[] = recipient;
    }
}

mailer = new Mailer(/* ... */);
recipient = EmailAddress.fromString('[email protected]');

mailer.sendConfirmationEmail(recipient);                         2
 
mailer.sendConfirmationEmail(recipient);                         3

  • 1 We don’t send the email again.
  • 2 This will send out a confirmation email.
  • 3 The second call won’t send another email.

Make sure none of your services update internal state that influences its behavior like this.

A guiding question when deciding whether your service behaves properly in this respect is, “Would it be possible to reinstantiate the service for every method call, and would it still show the same behavior?” For the preceding Mailer class, this obviously isn’t true: reinstantiating it would cause multiple emails to be sent to the same recipient.

In the case of the stateful Mailer service, the question is, “How can we prevent duplicate calls to sendConfirmationEmail()?” Somehow the client isn’t smart enough to take care of this. What if, instead of providing just one EmailAddress, the client could provide an already deduplicated list of EmailAddress instances? They could use something like the following Recipients class.

Listing 7.6. Recipients can provide a list of deduplicated email addresses
final class Recipients
{
    /**
     * @var EmailAddress[]
     */
    private array emailEmailAddresses;

    /**
    * @return EmailAddress[]
     */
    public function uniqueEmailAddresses(): array
    {
        // Return a deduplicated list of of addresses...
    }
}

final class Mailer
{
    public function sendConfirmationEmails(
        Recipients recipients
    ): void {
        foreach (recipients.uniqueEmailAddresses()
            as emailAddress) {
            // Send the email...
        }
    }
}

This would certainly solve the problem and make the Mailer service stateless again. But instead of letting Mailer make that special call to uniqueEmailAddresses(), what we’re actually looking for is a list of Recipients that couldn’t contain duplicate email addresses. You could most elegantly protect this domain invariant inside the Recipients class itself.

Listing 7.7. A more effective implementation of Recipients
final class Recipients
{
    /**
     * @var EmailAddress[]
     */
    private array emailAddresses;

    private function __construct(array emailAddresses)
    {
        this.emailAddresses = emailAddresses;
    }

    public static function emptyList(): Recipients                 1
    {
        return new Recipients([]);
    }

    public function with(EmailAddress emailAddress): Recipients    2
    {
        if (in_array(emailAddress, this.emailAddresses)) {
            return this;                                           3
        }

        return new Recipients(
            array_merge(this.emailAddresses),
            [emailAddress]
        );
    }

    public function emailAddresses(): array                        e
    {
        return this.emailAddresses;
    }
}

  • 1 Always start with an empty list.
  • 2 Any time a client wants to add an email address to it, it will only be added if it’s not already on the list.
  • 3 No need to add the email address again.
  • 4 There’s no need for a uniqueEmailAddresses() method anymore.
Immutable services and service containers

Service containers are often designed to share all service instances once they have been created. This saves the runtime from instantiating the same service again, should it be reused as a dependency of some other service. However, if a service is immutable (as it should be), this sharing isn’t really needed. You could instantiate the service over and over again.

Of course, there are services in a service container that shouldn’t be instantiated again every time they’re used as a dependency. For instance, a database connection object or any other kind of reference to a resource that needs to be created once and then shared between dependent services. In general, however, your services shouldn’t need to be shared. If you’ve followed all of the advice so far, you’re doing well already, because immutable services don’t need to be shared. They can, but they don’t have to.

Exercises

  1. What would prevent a service from being immutable?

    1. Allowing an optional dependency to be injected by calling a method on it.
    2. Allowing a configuration value to be changed by calling a method on it.
    3. Offering a query method that itself calls a command method.
    4. Having too many constructor arguments.
    5. Changing some kind of internal state when a client calls a method on it.

7.4. When something goes wrong, throw an exception

The same rule for retrieving information also counts for performing tasks: when something goes wrong, don’t return a special value to indicate it; throw an exception instead. As discussed earlier, a method can have precondition checks that throw Invalid-ArgumentExceptions or LogicExceptions. For the remainder of the failure scenarios, we can’t determine upfront if they will occur, so we throw a RuntimeException. We’ve already discussed the other important rules for using exceptions in section 5.2.

Exercises

  1. What type of exception would you expect save() to throw if it couldn’t store a Product entity because its ID was already used?
    interface ProductRepository
    {
        public function save(Product product): void;
    }

    1. An InvalidArgumentException, because the client has provided an invalid Product argument.
    2. A RuntimeException, because whether or not a Product entity with that ID already exists can’t be decided by just inspecting the arguments.
  2. What type of exception would you expect set() to throw if an empty string were provided for key?
    interface Cache
    {
        public function set(string key, string value): void;
    }

    1. An InvalidArgumentException, because the client has provided an invalid argument.
    2. A RuntimeException, because the client may decide at runtime what the value for key should be.

7.5. Use queries to collect information and commands to take the next steps

Earlier, when we discussed query methods, we saw how a chain of method calls that starts with a call to a query method won’t have a call to a command method inside of it. The command method may produce a side effect, which violates the rule that a query method shouldn’t have any side effects.

Now that we’re looking at command methods, we should note that the other way around, there’s no such rule. When a chain of calls starts with a command method, it’s possible that you’ll encounter a call to a query method down the line. For instance, the changeUserPassword() method we saw earlier starts with a query to the user repository.

Listing 7.8. changeUserPassword() starts with a query, then performs a task
public function changeUserPassword(
    UserId userId,
    string plainTextPassword
): void {
    user = this.repository.getById(userId);
    hashedPassword = /* ... */;
    user.changePassword(hashedPassword);
    this.repository.save(user);
    this.eventDispatcher.dispatch(
        new UserPasswordChanged(userId)
    );
}

The next method call is changePassword() on the user object, then another command on the repository. Inside the repository implementation, there may again be calls to command methods, but it’s also possible that query methods are being called there (see figure 7.1).

Figure 7.1. Inside a command method, you may call query methods to retrieve more information.

However, when looking at how objects call each other’s command and query methods, be aware of the pattern illustrated in figure 7.2. This pattern of calls often indicates a little conversation between objects that could have been happening inside the called object only. Consider the following example:

if (obstacle.isOnTheRight()) {
    player.moveLeft();
} elseif (obstacle.isOnTheLeft()) {
    player.moveRight();
}
Figure 7.2. Calling a query method, then a command method on the same object

The following is an improvement on this piece of code, where the knowledge about which action to take is now completely inside the object.

player.evade(obstacle);

This object is able to keep this knowledge to itself, and its implementation can evolve freely, whenever it needs to show more complicated behavior.

7.6. Define abstractions for commands that cross system boundaries

If a command method has code that reaches out across the application’s own boundaries (that is, if it uses a remote service, the filesystem, a system device, etc.), you should introduce an abstraction for it. For instance, the following listing shows a piece of code that publishes a message to a queue, so background consumers can tune into important events inside the main application.

Listing 7.9. SendMessageToRabbitMQ publishes messages on a queue
final class SendMessageToRabbitMQ
{
    // ...

    public function whenUserChangedPassword(
        UserPasswordChanged event
    ): void {
        this.rabbitMqConnection.publish(
            'user_events',
            'user_password_changed',
            json_encode([
                'user_id' => (string)event.userId()
            ])
        );
    }
}

The publish() method will reach out to the RabbitMQ server and publish a message to its queue, which is outside of the application’s boundaries, so we should come up with an abstraction here. As discussed earlier, this requires an interface and a higher-level concept. For example, preserving the notion that we want to queue a message, we could introduce the following Queue abstraction.

Listing 7.10. Queue is an abstraction used by SendMessageToRabbitMQ
interface Queue                                      1
{
    public function publishUserPasswordChangedEvent(
        UserPasswordChanged event
    ): void;
}

final class RabbitMQQueue implements Queue           2
{
    // ...

    public function publishUserPasswordChangedEvent(
        UserPasswordChanged event
    ): void {
        this.rabbitMqConnection.publish(
            'user_events',
            'user_password_changed',
            json_encode([
                'user_id' => (string)event.userId()
            ])
        );
    }
}

final class SendMessageToRabbitMQ                   3
{
    private Queue queue;

    public function __construct(Queue queue)
    {
        this.queue = queue;
    }

    public function whenUserPasswordChanged(
        UserPasswordChanged event
    ): void {
        this.queue.publishUserPasswordChangedEvent(event);
    }
}

  • 1 Queue is the abstraction.
  • 2 The standard Queue implementation is RabbitMQQueue, which contains the code we already had.
  • 3 The event listener that is supposed to publish a message to the queue whenever a UserPasswordChanged event occurs will use the new abstraction as a dependency.

The first step was to introduce an abstraction. Once you start adding more publish …Event() methods to Queue, you may start noticing similarities between these methods. Then you could apply generalization to make these methods more generic. You may need to implement a standard interface for all events.

Listing 7.11. A CanBePublished interface for publishable events
interface CanBePublished
{
    public function queueName(): string;
    public function eventName(): string;
    public function eventData(): array;
}

final class RabbitMQQueue implements Queue
{
    // ...

    public function publish(CanBePublished event): void
    {
        this.rabbitMqConnection.publish(
            event.queueName(),
            event.eventName(),
            json_encode(event.eventData())
        );
    }
}

It’s generally a good idea to start with the abstraction and leave the generalization until you’ve seen about three cases that could be simplified by making the interface and object types involved more generic. This prevents you from abstracting too early and having to revise the interface and any of its implementations for every new case you want your abstraction to support.

Exercises

  1. Why does the task of saving an entity to the database need its own abstraction?

    1. Because one day you may not have that entity anymore.
    2. Because having an abstraction allows you to replace the implementation in a test scenario.
    3. Because you may want to reuse that abstraction to store other types of data.
    4. Because an abstraction uses a higher-level concept to explain what’s going on, which makes it easier to read the code, because you can ignore all the lower-level details.

7.7. Only verify calls to command methods with a mock

We already discussed that query methods shouldn’t be mocked. In a unit test, you shouldn’t verify the number of calls made to them. Queries are supposed to be without side effects, so you could make them many times if you want to. Allowing the implementation to do so increases the stability of the test. If you decide to call a method twice instead of remembering its result in a variable, the test won’t break.

However, when a command method makes a call to another command method, you may want to mock the latter. After all, this command is supposed to be called at least once (you want to verify that, because it’s part of the job), but it shouldn’t be called more than once (because you don’t want to have its side effects being produced more than once too). This is demonstrated in the following listing.

Listing 7.12. Unit testing the ChangePasswordService using a mock
final class ChangePasswordService
{
    private EventDispatcher eventDispatcher;
    // ...

    public function __construct(
        EventDispatcher eventDispatcher,
        // ...
    ) {
        this.eventDispatcher = eventDispatcher;

        // ...
    }

    public function changeUserPassword(
        UserId userId,
        string plainTextPassword
    ): void {
        // ...

        this.eventDispatcher.dispatch(
            new UserPasswordChanged(userId)
        );
    }
}

/**
 * @test
 */
public function it_dispatches_a_user_password_changed_event(): void
{
    userId = /* ... */;

    eventDispatcherMock = this.createMock(EventDispatcher.className);     1
    eventDispatcherMock
        .expects(this.once())
        .method('dispatch')
        .with(new UserPasswordChanged(userId));

    service = new ChangePasswordService(eventDispatcherMock, /* ... */);

    service.changeUserPassword(userId, /* ... */);
}

  • 1 This defines a true mock object: we verify how many times we expect a method to be called (once), and with which arguments. We don’t make assertions about the return value, since dispatch() is a command method.

There are no regular assertions at the end of this test method, because the mock object itself will verify that our expectations were met. The test framework will ask all mock objects that were created for a single test case to do this.

If you prefer to have some actual assertions in your test case, you could use a spy as a test double for EventDispatcher. In the most generic form, a spy will remember all method calls that were made to it, including the arguments used. However, in our case, a really simple EventDispatcher implementation would suffice.

Listing 7.13. An EventDispatcher spy
final class EventDispatcherSpy implements EventDispatcher
{
    private array events = [];

    public function dispatch(object event): void
    {
        this.events[] = event;                  1
    }

    public function dispatchedEvents(): array
    {
        return this.events;
    }
}

/**
 * @test
 */
public function it_dispatches_a_user_password_changed_event(): void
{
    // ...
    eventDispatcher = new EventDispatcherSpy();
    service = new ChangePasswordService(eventDispatcher, /* ... */);

    service.changeUserPassword(userId, /* ... */);

    assertEquals(                               2
        [
            new UserPasswordChanged(userId)
        ],
        eventDispatcher.dispatchedEvents()
    );
}

  • 1 The spy just keeps a list of the events that were dispatched to it.
  • 2 Now we can make an assertion instead of waiting for the test framework to verify the method calls on our mock.
Exercises

  1. Given the following interface,
    interface UserRepository
    {
        public function save(User user): void;
    }
    if you write a unit test for a class that calls save() on its UserRepository dependency, what type of test double could you use?

    1. Dummy
    2. Stub
    3. Fake
    4. Mock
    5. Spy
  2. Given the following interface,
    interface UserRepository
    {
        public function getById(UserId userId): User;
    }
    if you write a unit test for a class that calls getById() on its UserRepository dependency, what type of test double could you use?

    1. Dummy
    2. Stub
    3. Fake
    4. Mock
    5. Spy

Summary

  • Command methods should be used to perform tasks. These command methods should have imperative names (“Do this,” “Do that”) and they should be limited in scope. Make a distinction between the main job and the effects of this job. Dispatch events to let other services perform additional tasks. While performing its task, a command method may also call query methods to collect any information needed.
  • A service should be immutable from the outside, as well as on the inside. Just as with services for retrieving data, services that perform tasks should be reusable many times. If something goes wrong while performing a task, throw an exception (as soon as you know it).
  • Define an abstraction for commands that cross a system boundary (commands that reach out to some remote service, database, etc.). When testing command methods that themselves call command methods, you can use a mock or a spy to test calls to these methods. You can use a mocking tool for this or write your own spies.

Answers to the exercises

  1. Correct answers: c and e. All the other options should be considered part of the primary action. c and e are secondary actions or effects of the primary action.
  2. Correct answers: a, b, and e. Switching out dependencies or configuration values makes a service object mutable. The number of constructor arguments doesn’t have any effect on immutability. The same goes for collaborations with other objects.
  3. Correct answer: b. The reason has been provided in the answer itself.
  4. Correct answer: a. The reason has been provided in the answer itself.
  5. Correct answers: b and d. The reason has been provided in the answers. Answer a is wrong because if you get rid of the entity, you also get rid of its repository. Answer c is wrong because that would require us to make the repository generic as well, which is not what we were after here.
  6. Correct answers: d and e. save() is a command method, so we use a mock (to assure that a call to that method was made) or a spy (to later find out if that method call was made).
  7. Correct answers: a, b, and c. getById() is a query method, so we provide a dummy (a nonfunctional object with only the correct type), a stub (an object with the correct type, which can return a previously configured value), or a fake (a more evolved object, with some logic of its own). We don’t want to verify actual function calls being made, which is why we don’t use a mock or a spy.
..................Content has been hidden....................

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