Implementing Domain Driven Design (Part Three)

发布时间 2023-09-09 08:35:58作者: 天行健君子以自强

Example Use Cases

This section will demonstrate some example use cases and discuss alternative scenarios.

Entity Creation

Creating an object from an Entity/Aggregate Root class is the first step of the lifecycle of that entity. The Aggregate/Aggregate Root Rules & Best Practices section suggests to create a primary constructor for the Entity class that guarantees to create a valid entity. So, whenever we need to create an instance of that entity, we should always use that constructor.

See the Issue Aggregate Root class below:

  • This class guarantees to create a valid entity by its constructor.

  • If you need to change the Title later, you need to use the SetTitle method which continues to keep Title in a valid state.

  • If you want to assign this issue to a user, you need to use IssueManager (it implements some business rules before the assignment - see the Domain Services section above to remember).

  • The Text property has a public setter, because it also accepts null values and does not have any validation rules for this example. It is also optional in the constructor.

Let's see an Application Service method that is used to create an issue:

CreateAsync method:

  • Uses the Issue constructor to create a valid issue. It passes the Id using the IGuidGenerator service. It doesn't use auto object mapping here.

  • If the client wants to assign this issue to a user on object creation, it uses the IssueManager to do it by allowing the IssueManager to perform the necessary checks before this assignment.

  • Saves the entity to the database.

  • Finally uses the IObjectMapper to return an IssueDto that is automatically created by mapping from the new Issue entity.

Apply Domain Rules on Entity Creation

The example Issue entity has no business rule on entity creation, except some formal validations in the constructor. However, there maybe scenarios where entity creation should check some extra business rules.

For example, assume that you don't want to allow to create an issue if there is already an issue with exactly the same Title. Where to implement this rule? It is not proper to implement this rule in the ****, because it is a core business(domain) rule that should always be checked.

This rule should be implemented in a Domain Service, IssueManager in this case. So, we need to force the Application Layer always to use the IssueManager to create a new Issue.

First, we can make the Issue constructor internal, instead of public:

This prevents Application Services to directly use the constructor, so they will use the IssueManager. Then we can add a CreateAsync method to the IssueManager:

  • CreateAsync method checks if there is already an issue with the same tilte and throws a business exception in this case.

  • If there is no duplication, it creates and returns a new Issue.

The IssueAppService is changed as shown below in order to use the IssueManager's CreateAsync method:

Discussion: Why is the Issue not saved to the database in IssueManager

You may ask "Why didn't IssueManager save the Issue to the database?". We think it is the responsibility of the Application Service.

Because, the Applicaton Service may require additional changes/operations on the Issue object before saving it. If Domain Service saves it, then the Save operation is duplicated:

  • It causes performance lost because of double database round trip.

  • It requires explicit database transaction that covers both operations.

  • If additional actions cancel the entity creation because of a business rule, the transaction should be rolled back in the database.

When you check the IssueAppService, you will see the advantage of not saving Issue to the database in the IssueManager.CreateAsync. Otherwise, we would need to perform one Insert (in the IssueManager) and one Update (after the Assignment).

Discussion: Why is the duplicate Title check not implemented in the Application Service?

We could simply say "Because it is a core domain logic" and should be implemented in the Domain Layer". However, it brings a new question "How did you decide that it is a core domain logic, but not an application logic?" (We will discuss the difference later with more details).

For this example, a simple question can help us to make the decision: "If we have another way (use case) of creating an issue, should we still apply the same rule? Is that rule should always be implemented". You may think "Why do we have a second way of creating an issue?". However, in real life, you have:

  • End users of the application may create issues in your application's standard UI.

  • You may have a second back office application that is used by your own employees and you may want to provide a way of creating issues (probably with different authorization rules in this case).

  • You may have an HTTP API that is open to 3rd-part clients and they create issues.

  • You may have a background worker service that do something and creates issues if it detects some problems. In this way, it will create an issue without any user interaction (and probably without any standard authorization check).

  • You may have a button on the UI that converts something (for example, a discussion) to an issue.

We can give more examples. All of these are should be implemented by different Application Servcie methods (see the Multiple Application Layers section below), but they always follow the rule: Title of the new issue can not be same of any existing issue! That's why this logic is a core domain logic, should be located in the Domain Layer and should not be duplicated in all these application service methods.

Updating / Manipulating An Entity

Once an entity is created, it is updated/manipulated by the use cases until it is deleted from the system. There can be different types of the use cases directly or indirectly changes an entity.

In this section, we will discuss a typical update operation that changes multiple properties of an Issue.

This time, beginning from the Update DTO:

By comparing to IssueCreationDto, you see no RepositoryId. Because, our system doesn't allow to move issues across repositories (think as GitHub repositories). Only Title is required and the other properties are optional.

Let's see the Update implementation in the IssueAppService:

  • UpdateAsync method gets id as a separate parameter. It is not included in the UpdateIssueDto. This is a design decision that helps ABP to properly define HTTP routes when you auto expose this service as an HTTP API endpoint. So, that's not related to DDD.

  • It starts by getting the Issue entity from the database.

  • Uses IssueManager's ChangeTitleAsync instead of directly calling Issue.SetTitle(...). Because we need to implement the duplicate Title check as just done in the Entity Creation. This requires some changes in the Issue and IssueManager classes (will be explained below).

  • Uses IssueManager's AssignToAsync method if the assigned user is being changed with this request.

  • Directly sets the Issue.Text since there is no business rule for that. If we need later, we can always refactor.

  • Saves changes to the database. Again, saving changed entities is a responsibility of the Application Service method that coordinates the business objects and the transaction. If the IssueManager had saved internally in ChangeTitleAsync and AssignToAsync method, there would be double database operation (see the Discussion: Why is the Issue not saved to the database in IssueManager? above).

  • Finally uses the IObjectMapper to return an IssueDto that is automatically created by mapping from the updated Issue entity.

As said, we need some changes in the Issue and IssueManager classes.

First, made SetTitle internal in the Issue class:

Then added a new method to the IssueManager to change the Title:

Domain Logic & Application Logic

As mentioned before, Business Logic in the Domain Driven Design is split into two parts (layers): "Domain Logic" and "Application Logic":

Domain Logic consists of one Core of the system while Application Logic implements application specific Use Cases.

While the definition is clear, the implementation may not be easy. You may be undecided which code should stand in the Application Layer, which code should be in the Domain Layer. This section tries to explain the differences.

Multiple Application Layers

DDD helps to deal with complexity when your system is large. Especially, if there are multiple applications are being developed in a single domain, then the Domain Logic vs Application Logic separation becomes much more important.

Assume that you are building a system that has multiple applications:

  • A Public Web Site Application, built with ASP.NET Core MVC, to show your products to users. Such a web site doesn't require authentication to see the products. The users login to the web site, only if they are performing some actions (like adding a product to the basket).

  • A Back office Application, built with Angular UI (that uses REST APIs). This application used by office workers of the company to manage the system (like editing product descriptions)

  • A Mobile Application that has much simpler UI compared to the Public Web Site. It may communicate to the server via REST APIs or another technology (like TCP sockets).

Every application will have different requirements, different use cases (Application Service methods), different DTOs, different validation and authorization rules...etc.

Mixing all these logics into a single application layer makes your services contain too many if conditions with complicated business logic makes your code harder to develop, maintain and test and leads to potential bugs.

If you've multiple applications with a single domain;

  • Create separate application layers for each application/client type and implement application specific busniess logic in these separate layers.

  • Use a single domain layer to share the core domain logic.

Such a design makes it even more important to distinguish between Domain Logic and Application Logic.

To be more clear about the implementation, you can create different projects (.csproj) for each application types. For example:

  • IssueTracker.Admin.Application & IssueTracker.Admin.Application.Contracts projects for the Back Office (admin) Application.

  • IssueTracker.Public.Application & IssueTracker.Public.Application.Contracts projects for the Public Web Application.

  • IssueTracker.Mobile.Application & IssueTracker.Mobile.Application.Contracts projects for the Mobile Application

Examples

This section contains some Application Service and Domain Service examples to discuss how to decide to place business logic inside these services.

Example: Creating a new Organization in a Domain Service

Let's see the CreateAsync method step by step to discuss if the code part should be in the Domain Service, or not;

  • Correct: It first checks for duplicate organization name and throws exception in this case. This is something related to core domain rule and we never allow duplicated names.

  • WRONG: Domain Services should not perform authorization. Authorization should be done in the Application Layer.

  • WRONG: It logs a message with including the Current User's UserName. Domain service should not be depend on the Current User. Domain Services should be usable even if there is no user in the system. Current User (Session) should be a Presentation/Application Layer related concept.

  • WRONG: It sends an email about this new organization creation. We think this is also a use case specific business logic. You may want to create different type of emails in different use cases or don't need to send emails in some cases.

Example: Creating a new Organization in an Application Service

Let's see the CreateAsync method step by step to discuss if the code part should be in the Application Service, or not;

  • CORRECT: Application Service methods should be unit of work (transactional). ABP's Unit Of Work system makes this automatic (even without need to add [UnitOfWork] attribute for the Application Services).

  • CORRECT: Authorization should be done in the application layer. Here, it is done by using the [Authorized] attribute.

  • CORRECT: Payment (an infrastruct service) is called to charge money for this operation (Creating an Organization is a paid service in our business).

  • CORRECT: Application Service method is responsible to save changes to the database.

  • CORRECT: We can send email as a notification to the system admin.

  • WRONG: Do not return entities from the Application Services. Return a DTO instead.

Discussion: Why don't we move the payment logic into the domain service?

You may wonder why the payment code is not inside the OrganizationManager. It is an important thing and we never want to miss the payment.

However, being important is not sufficient to consider a code as a Core Business Logic. We may have other use cases where we don't charge money to create a new Organization. Examples:

  • An admin user can use a Back Office Application to create a new organization without any payment.

  • A background-working data import/integration/ system may also need to create organizations without any payment operation.

As you see, payment is not a necessary operation to create a valid organiztion. It is a use-case specific application logic.

Examples: CRUD Operations

This Application Service does nothing itself and delegates all the work to the Domain Service. It even passes the DTOs to the IssueManager.

  • Do not create Domain Service methods just for simple CRUD operations without any domain logic.

  • Never pass DTOs to or return DTOs from the Domain Services.

Application Services can directly work with repositories to query, create, update or delete data unless there are some domain logics should be performed during these operations. In such cases, create Domain Service methods, but only for those really necessary.

Do not create such CRUD domain service methods just by thinking that they may be needed in the future (YAGNI)! Do it when you need and refactor the existing code. Since the Application Layer gracefully abstracts the Domain Layer, the refactoring process doesn't effect the UI Layer and other clients.

Reference Books

If you are more interested in the Domain Driven Desgin and building large-scale enterprise system, the following books are recommended as reference books;

  • "Domain Driven Design" by Eric Evans

  • "Implementing Domain Driven Design" by Vaugh Vernon

  • "Clean Architecture" by Robert C.Martin