This chapter covers
Besides retrieving information from objects, you can use objects to perform a variety of tasks for you:
The following sections provide rules for methods that perform tasks like these.
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.
public function sendReminderEmail( EmailAddress recipient, // ... ): void { // ... } public function saveRecord(Record record): void { // ... }
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:
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.
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:
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.
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()); } }
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.
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
Using events like this has several advantages:
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:
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); } }
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.
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
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.
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.
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; } }
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.
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.
interface ProductRepository { public function save(Product product): void; }
interface Cache { public function set(string key, string value): void; }
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.
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).
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(); }
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.
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.
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.
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); } }
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.
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.
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.
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, /* ... */); }
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.
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() ); }
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?
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?
18.220.120.161