service
You already know what entities and value objects are.As basic building blocks, they should contain the vast majority of business logic for any application.However, there are some scenarios where entity and value objects are not the best solution.Let's take a look at what Eric Evans mentioned in his book Domain Driven Design: A Way to Cope with Software Core Complexity:
When an important process or transformation in a domain is not the natural responsibility of an entity or value object, an operation is added to the model as a separate interface and defined as a service.Define the interface according to the model language and ensure that the operation noun is part of the common language.Make the service stateless.
Therefore, you should consider modeling these operations as services when there are operations that need to be reflected and entity and value objects are not the best choice.In domain-driven design, you will encounter three typical different types of services:
- Application services: Operate scalar types and convert them to domain types.Title types can be considered unknown to any domain model.This includes basic data types and types that are not domain-wide.This is only an overview in this chapter. If you need a deeper understanding of this topic, please review Chapter 11: Applications.
- Domain services: Operate only services that belong to a domain.They contain meaningful concepts that can be found in a common language and contain operations that are not appropriate for value objects or entities.
- Basic Services: Some operations that meet infrastructure concerns, such as sending mail and logging meaningful log data.For hexagonal architectures, they exist outside the boundaries of the domain.
application service
Application services are middleware between external and domain logic.The purpose of this mechanism is to convert external commands into meaningful domain commands.
Let's take a look at the example of User signs up to our platform.Starting with the in-table approach (delivery mechanism), we need to enter requests for the portfolio of domain operations.Using a framework like Symfony as a delivery mechanism, the code would look like this:
class SignUpController extends Controller { public function signUpAction(Request $request) { $signUpService = new SignUpUserService( $this->get('user_repository') ); try { $response = $signUpService->execute(new SignUpUserRequest( $request->request->get('email'), $request->request->get('password') )); } catch (UserAlreadyExistsException $e) { return $this->render('error.html.twig', $response); } return $this->render('success.html.twig', $response); } }
As you can see, we've created a new instance of an application service to communicate all the dependent needs - in this case, a UserRepository.UserRepository is an interface that can be implemented using any specified technology such as MySQL, Redis, ElasticSearch.Next, we build a request object for the application service to abstract the delivery mechanism - in this case, a web request from business logic.Finally, we execute the application service, get the reply, and render the result with the reply.On the domain side, we test one possible implementation of the application service by coordinating logic to satisfy the User signs up use case:
class SignUpUserService { private $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function execute(SignUpUserRequest $request) { $user = $this->userRepository->userOfEmail($request->email); if ($user) { throw new UserAlreadyExistsException(); } $user = new User( $this->userRepository->nextIdentity(), $request->email, $request->password ); $this->userRepository->add($user); return new SignUpUserResponse($user); } }
The code is all about the area we want to solve, not the specific technology we use to solve it.In this way, we can decouple high-level abstraction from low-level implementation details.Communication between delivery mechanisms and domains is through a data structure called DTO, which we have described in Chapter 2: Architectural Style:
class SignUpUserRequest { public $email; public $password; public function __construct($email, $password) { $this->email = $email; $this->password = $password; } }
For reply object creation, you can use getters or open instance variables.Application services should be aware of transaction scope and security.However, you need to go into Chapter 11: App Services, and explore more about these and other app services.
Domain Services
In your conversation with domain experts, you will encounter concepts in the common language that are not well represented as an entity or a value object:
- Users can log on to the system by themselves
- Shopping cart can be an order
The above two examples are very specific concepts, and neither of them can be naturally bound to an entity or value object.To further emphasize this curiosity, we can try to model this behavior as follows:
class User { public function signUp($aUsername, $aPassword) { // ... } } class Cart { public function createOrder() { // ... } }
In the first implementation, it is impossible to know the association between a given user name and password and the last user instance invoked.Obviously, this operation is not appropriate for the current entity.Instead, it should be extracted as a separate class with clear intentions.
With this in mind, we can create a domain service whose sole responsibility is to authenticate users:
class SignUp { public function execute($aUsername, $aPassword) { // ... } }
Similarly, in the second example, we could create a domain service that specifically creates orders from a given shopping cart:
class CreateOrderFromCart { public function execute(Cart $aCart) { // ... } }
A domain service can be defined as an operation that does not naturally satisfy a domain task for an entity or value object.As a concept to represent domain operations, clients should use domain name services regardless of their running history.The domain service itself does not hold any state, so it is a stateless operation.
Domain Services and Infrastructure Services
Infrastructure dependency is a common problem when modeling services in the domain.For example, in an authentication mechanism that needs to handle password hashes.In this case, you can use a separate interface that defines multiple hashing mechanisms.Using this model will still allow you to clearly separate your concerns between the domain and infrastructure:
namespace Ddd\Auth\Infrastructure\Authentication; class DefaultHashingSignUp implements Ddd\Auth\Domain\Model\SignUp { private $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function execute($aUsername, $aPassword) { if (!$this->userRepository->has($aUsername)) { throw UserDoesNotExistException::fromUsername($aUsername); } $aUser = $this->userRepository->byUsername($aUsername); if (!$this->isPasswordValidForUser($aUser, $aPassword)) { throw new BadCredentialsException($aUser, $aPassword); } return $aUser; } private function isPasswordValidForUser( User $aUser, $anUnencryptedPassword ) { return password_verify($anUnencryptedPassword, $aUser->hash()); } }
Here is another MD5-based implementation:
namespace Ddd\Auth\Infrastructure\Authentication; use Ddd\Auth\Domain\Model\SignUp class Md5HashingSignUp implements SignUp { const SALT = 'S0m3S4lT'; private $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function execute($aUsername, $aPassword) { if (!$this->userRepository->has($aUsername)) { throw new InvalidArgumentException( sprintf('The user "%s" does not exist.', $aUsername) ); } $aUser = $this->userRepository->byUsername($aUsername); if ($this->isPasswordInvalidFor($aUser, $aPassword)) { throw new BadCredentialsException($aUser, $aPassword); } return $aUser; } private function salt() { return md5(self::SALT); } private function isPasswordInvalidFor( User $aUser, $anUnencryptedPassword ) { $encryptedPassword = md5( $anUnencryptedPassword . '_' . $this->salt() ); return $aUser->hash() !== $encryptedPassword; } }
Choosing this approach enables us to implement services in a variety of areas at the infrastructure level.In other words, we end up with a variety of infrastructure services.Each infrastructure service handles a different hashing mechanism.Depending on the implementation, usage can be easily managed through a dependency injection container (for example, through a dependency injection component of symfony):
<?xml version="1.0"?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="sign_in" alias="sign_in.default"/> <service id="sign_in.default" class="Ddd\Auth\Infrastructure\Authentication \DefaultHashingSignUp"> <argument type="service" id="user_repository"/> </service> <service id="sign_in.md5" class="Ddd\Auth\Infrastructure\Authentication \Md5HashingSignUp"> <argument type="service" id="user_repository"/> </service> </services> </container>
If, in the future, we want to work with a new hash type, we can simply start by implementing interfaces in the implementation realm.Then you declare the service in the dependency injection container and replace the service alias dependency with a new type.
A Code Reuse Issue
Although the previous implementation description explicitly defined "separation of concerns", each time we want to implement a new hashing mechanism, we have to repeat the implementation of the password validation algorithm.One solution is to separate these two responsibilities to improve code reuse.Instead, we can use a policy pattern to put the extract password hash algorithm logic into a custom class for all defined hash algorithms.This is open to extensions and closed to modifications:
namespace Ddd\Auth\Domain\Model; class SignUp { private $userRepository; private $passwordHashing; public function __construct( UserRepository $userRepository, PasswordHashing $passwordHashing ) { $this->userRepository = $userRepository; $this->passwordHashing = $passwordHashing; } public function execute($aUsername, $aPassword) { if (!$this->userRepository->has($aUsername)) { throw new InvalidArgumentException( sprintf('The user "%s" does not exist.', $aUsername) ); } $aUser = $this->userRepository->byUsername($aUsername); if ($this->isPasswordInvalidFor($aUser, $aPassword)) { throw new BadCredentialsException($aUser, $aPassword); } return $aUser; } private function isPasswordInvalidFor(User $aUser, $plainPassword) { return !$this->passwordHashing->verify( $plainPassword, $aUser->hash() ); } } interface PasswordHashing { /** * @param $plainPassword * @param string $hash * @return boolean */ public function verify($plainPassword, $hash); }
Defining different hash algorithms is as easy as implementing the PasswordHasing interface:
namespace Ddd\Auth\Infrastructure\Authentication; class BasicPasswordHashing implements \Ddd\Auth\Domain\Model\PasswordHashing { public function verify($plainPassword, $hash) { return password_verify($plainPassword, $hash); } } class Md5PasswordHashing implements Ddd\Auth\Domain\Model\PasswordHashing { const SALT = 'S0m3S4lT'; public function verify($plainPassword, $hash) { return $hash === $this->calculateHash($plainPassword); } private function calculateHash($plainPassword) { return md5($plainPassword . '_' . $this->salt()); } private function salt() { return md5(self::SALT); } }
Test Domain Services
Given an example of user authentication for service implementations in multiple domains, it is obviously beneficial to service testing.However, it is often cumbersome to test template method implementations.Therefore, we use a common password hash implementation for testing purposes:
class PlainPasswordHashing implements PasswordHashing { public function verify($plainPassword, $hash) { return $plainPassword === $hash; } }
Now we can test all the cases in the domain service:
class SignUpTest extends PHPUnit_Framework_TestCase { private $signUp; private $userRepository; protected function setUp() { $this->userRepository = new InMemoryUserRepository(); $this->signUp = new SignUp( $this->userRepository, new PlainPasswordHashing() ); } /** * @test * @expectedException InvalidArgumentException */ public function itShouldComplainIfTheUserDoesNotExist() { $this->signUp->execute('test-username', 'test-password'); } /** * @test * @expectedException BadCredentialsException */ public function itShouldTellIfThePasswordDoesNotMatch() { $this->userRepository->add( new User( 'test-username', 'test-password' ) ); $this->signUp->execute('test-username', 'no-matching-password'); } /** * @test */ public function itShouldTellIfTheUserMatchesProvidedPassword() { $this->userRepository->add( new User( 'test-username', 'test-password' ) ); $this->assertInstanceOf( 'Ddd\Domain\Model\User\User', $this->signUp->execute('test-username', 'test-password') ); } }
Anemia domain model VS. Hyperemia domain model
You must be careful not to overuse domain service abstraction in your system.Going this way strips away all the behavior of your entities and value objects, making them pure data containers.This is in contrast to the goal of object-oriented programming, which encapsulates data and behavior into a semantic unit called an object to express real-world concepts and problems.Overuse of domain services is considered an antipattern, which leads to anemia models.
Typically, when starting a new project and new functionality, it is easiest to fall into the trap of data modeling first.This generally includes the idea that each data table has a one-to-one object representation.However, this may or may not be the case.
Suppose our task is to set up an order processing system.If we start with data modeling, we may get the following SQL scripts:
CREATE TABLE `orders` ( `ID` INTEGER NOT NULL AUTO_INCREMENT, `CUSTOMER_ID` INTEGER NOT NULL, `AMOUNT` DECIMAL(17, 2) NOT NULL DEFAULT '0.00', `STATUS` TINYINT NOT NULL DEFAULT 0, `CREATED_AT` DATETIME NOT NULL, `UPDATED_AT` DATETIME NOT NULL, PRIMARY KEY (`ID`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Thus, it is relatively easy to create a representation of an order class.This representation includes the required accessor methods used to set up or obtain data from database tables:
class Order { const STATUS_CREATED = 10; const STATUS_ACCEPTED = 20; const STATUS_PAID = 30; const STATUS_PROCESSED = 40; private $id; private $customerId; private $amount; private $status; private $createdAt; private $updatedAt; public function __construct( $customerId, $amount, $status, DateTimeInterface $createdAt, DateTimeInterface $updatedAt ) { $this->customerId = $customerId; $this->amount = $amount; $this->status = $status; $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; } public function setId($id) { $this->id = $id; } public function getId() { return $this->id; } public function setCustomerId($customerId) { $this->customerId = $customerId; } public function getCustomerId() { return $this->customerId; } public function setAmount($amount) { $this->amount = $amount; } public function getAmount() { return $this->amount; } public function setStatus($status) { $this->status = $status; } public function getStatus() { return $this->status; } public function setCreatedAt(DateTimeInterface $createdAt) { $this->createdAt = $createdAt; } public function getCreatedAt() { return $this->createdAt; } public function setUpdatedAt(DateTimeInterface $updatedAt) { $this->updatedAt = $updatedAt; } public function getUpdatedAt() { return $this->updatedAt; } }
An example use case for this implementation is to update the order status as follows:
// Fetch an order from the database $anOrder = $orderRepository->find(1); // Update order status $anOrder->setStatus(Order::STATUS_ACCEPTED); // Update updatedAt field $anOrder->setUpdatedAt(new DateTimeImmutable()); // Save the order to the database $orderRepository->save($anOrder);
From the point of view of code reuse, this code has a similar case to initializing user authentication.To address this issue, maintainers of this practice recommend using a service layer to make operations clear and reusable.Now you can encapsulate the previous implementation into separate classes:
class ChangeOrderStatusService { private $orderRepository; public function __construct(OrderRepository $orderRepository) { $this->orderRepository = $orderRepository; } public function execute($anOrderId, $anOrderStatus) { // Fetch an order from the database $anOrder = $this->orderRepository->find($anOrderId); // Update order status $anOrder->setStatus($anOrderStatus); // Update updatedAt field $anOrder->setUpdatedAt(new DateTimeImmutable()); // Save the order to the database $this->orderRepository->save($anOrder); } }
Or, when updating the number of orders, consider this:
class UpdateOrderAmountService { private $orderRepository; public function __construct(OrderRepository $orderRepository) { $this->orderRepository = $orderRepository; } public function execute($orderId, $amount) { $anOrder = $this->orderRepository->find(1); $anOrder->setAmount($amount); $anOrder->setUpdatedAt(new DateTimeImmutable()); $this->orderRepository->save($anOrder); } }
This will greatly reduce the client's code while providing concise and clear operations:
$updateOrderAmountService = new UpdateOrderAmountService( $orderRepository ); $updateOrderAmountService->execute(1, 20.5);
Implementing this approach can result in a large degree of code reuse.If someone wants to update the number of orders, they just need to find an instance of UpdateOrderAmountService and call the execute method with the appropriate parameters.
However, choosing this path will undermine the object-oriented principles discussed earlier and incur the cost of building a domain model without any advantages.
Anemia Domain Model Destruction Encapsulation
If we look back at the service code we defined with the service layer, we can see that as a client using an Order entity, we need to know every detail of its internal representation.This finding violates the basic object-oriented principle of combining data with behavior.
Anemia domain model gives code reuse illusion
Suppose there is an instance where a client bypasses the UpdateOrderAmountService and retrieves, updates, and persists directly using OrderRepository.Then, any additional business logic for the UpdateOrderAmountService service may not be executed.This may result in inconsistent state of order storage.Therefore, invariants should be properly protected, and the best way to deal with them is to use a real domain model.In this example, the Order entity is the best place to ensure this:
class Order { // ... public function changeAmount($amount) { $this->amount = $amount; $this->setUpdatedAt(new DateTimeImmutable()); } }
Note that by dropping this operation on an entity and naming it according to a common language, the system will achieve excellent code reuse.Now anyone who wants to change the number of orders must call the Order::changeAmount method directly.
This results in richer classes with the goal of code reuse.This is often referred to as the rich domain model.
How to Avoid Anemia Domain Model
The way to avoid falling into anemia domain models is to consider behavior first when starting a new project or function.Database, ORM, etc. are implementation details and we should postpone our decision to use these tools as late as possible during development.By doing so, we can focus on one attribute that really matters: behavior.
As with entities, domain services are mentioned in Chapter 6: Domain events.However, when events are triggered most of the time by domain services rather than entities, it again indicates that you may be creating an anemia model.
Summary
Above, services represent operations within our system, and we can distinguish them into three types:
- Application Services: Helps coordinate requests from outside into the domain.These services do not contain domain logic.Transactions are handled at the application level; wrapping services in a transaction decorator will make your code transactions unknown.
- Domain Services: Domain concepts only, that is, concepts expressed in common language.Remember to defer implementation details and prioritize behavior because abusing domain services results in anemia models and poor object-oriented design.
- Infrastructure services: Infrastructure operations, such as sending mail or log information.
Our most important recommendation is that all situations be taken into account when deciding to create domain services.First try putting your business logic into an entity or value object.Communicate with colleagues and check again.If, after experimenting with different approaches, the best option is to create a domain service, then use it.