docs: make Chinese README the default
This commit is contained in:
@@ -1,124 +1,119 @@
|
||||
<!-- WEHUB_ZH_README -->
|
||||
> [!NOTE]
|
||||
> 本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。
|
||||
> [English](./README.en.md) · [原始项目](https://github.com/ddd-by-examples/library) · [上游 README](https://github.com/ddd-by-examples/library/blob/HEAD/README.md)
|
||||
> 原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。
|
||||
|
||||
[](https://circleci.com/gh/ddd-by-examples/library)
|
||||
[](https://codecov.io/gh/ddd-by-examples/library)
|
||||
|
||||
# Table of contents
|
||||
# 目录
|
||||
|
||||
1. [About](#about)
|
||||
2. [Domain description](#domain-description)
|
||||
3. [General assumptions](#general-assumptions)
|
||||
3.1 [Process discovery](#process-discovery)
|
||||
3.2 [Project structure and architecture](#project-structure-and-architecture)
|
||||
3.3 [Aggregates](#aggregates)
|
||||
3.4 [Events](#events)
|
||||
3.4.1 [Events in Repositories](#events-in-repositories)
|
||||
1. [关于](#about)
|
||||
2. [领域描述](#domain-description)
|
||||
3. [总体假设](#general-assumptions)
|
||||
3.1 [流程发现](#process-discovery)
|
||||
3.2 [项目结构与架构](#project-structure-and-architecture)
|
||||
3.3 [聚合(Aggregates)](#aggregates)
|
||||
3.4 [事件(Events)](#events)
|
||||
3.4.1 [仓储中的事件](#events-in-repositories)
|
||||
3.5 [ArchUnit](#archunit)
|
||||
3.6 [Functional thinking](#functional-thinking)
|
||||
3.7 [No ORM](#no-orm)
|
||||
3.8 [Architecture-code gap](#architecture-code-gap)
|
||||
3.9 [Model-code gap](#model-code-gap)
|
||||
3.6 [函数式思维](#functional-thinking)
|
||||
3.7 [不使用 ORM](#no-orm)
|
||||
3.8 [架构与代码的差距](#architecture-code-gap)
|
||||
3.9 [模型与代码的差距](#model-code-gap)
|
||||
3.10 [Spring](#spring)
|
||||
3.11 [Tests](#tests)
|
||||
4. [How to contribute](#how-to-contribute)
|
||||
5. [References](#references)
|
||||
3.11 [测试](#tests)
|
||||
4. [如何贡献](#how-to-contribute)
|
||||
5. [参考资料](#references)
|
||||
|
||||
## About
|
||||
|
||||
This is a project of a library, driven by real [business requirements](#domain-description).
|
||||
We use techniques strongly connected with Domain Driven Design, Behavior-Driven Development,
|
||||
Event Storming, User Story Mapping.
|
||||
这是一个图书馆(library)项目,由真实的[业务需求](#domain-description)驱动。
|
||||
我们采用与领域驱动设计(Domain Driven Design)、行为驱动开发(Behavior-Driven Development)、
|
||||
事件风暴(Event Storming)、用户故事地图(User Story Mapping)紧密相关的技术。
|
||||
|
||||
## Domain description
|
||||
|
||||
A public library allows patrons to place books on hold at its various library branches.
|
||||
Available books can be placed on hold only by one patron at any given point in time.
|
||||
Books are either circulating or restricted, and can have retrieval or usage fees.
|
||||
A restricted book can only be held by a researcher patron. A regular patron is limited
|
||||
to five holds at any given moment, while a researcher patron is allowed an unlimited number
|
||||
of holds. An open-ended book hold is active until the patron checks out the book, at which time it
|
||||
is completed. A closed-ended book hold that is not completed within a fixed number of
|
||||
days after it was requested will expire. This check is done at the beginning of a day by
|
||||
taking a look at daily sheet with expiring holds. Only a researcher patron can request
|
||||
an open-ended hold duration. Any patron with more than two overdue checkouts at a library
|
||||
branch will get a rejection if trying a hold at that same library branch. A book can be
|
||||
checked out for up to 60 days. Check for overdue checkouts is done by taking a look at
|
||||
daily sheet with overdue checkouts. Patron interacts with his/her current holds, checkouts, etc.
|
||||
by taking a look at patron profile. Patron profile looks like a daily sheet, but the
|
||||
information there is limited to one patron and is not necessarily daily. Currently a
|
||||
patron can see current holds (not canceled nor expired) and current checkouts (including overdue).
|
||||
Also, he/she is able to hold a book and cancel a hold.
|
||||
公共图书馆允许读者在其各分馆为图书办理预约(hold)。
|
||||
在任意时刻,可预约的图书只能被一位读者预约。
|
||||
图书分为流通型(circulating)或限制型(restricted),并可能收取取书费或使用费。
|
||||
限制型图书只能由研究员读者(researcher patron)预约。普通读者在任意时刻最多只能有五笔预约,
|
||||
而研究员读者可拥有不限数量的预约。开放式预约(open-ended book hold)在读者借出图书之前一直有效,
|
||||
借出时即告完成。若在申请后固定天数内未完成的封闭式预约(closed-ended book hold)将过期。
|
||||
该检查在每天开始时进行,方法是查看包含即将过期预约的日报(daily sheet)。
|
||||
只有研究员读者可以申请开放式预约期限。若某读者在某分馆有超过两笔逾期借出(overdue checkouts),
|
||||
则在该分馆尝试预约时会被拒绝。图书最多可借出 60 天。逾期借出的检查通过查看
|
||||
包含逾期借出的日报来完成。读者通过查看读者档案(patron profile)来了解自己的当前预约、借出等情况。
|
||||
读者档案类似日报,但信息仅限于一位读者,且不一定是按日汇总的。目前读者可以查看当前预约
|
||||
(未取消且未过期)和当前借出(含逾期)。此外,他/她还可以预约图书和取消预约。
|
||||
|
||||
How actually a patron knows which books are there to lend? Library has its catalogue of
|
||||
books where books are added together with their specific instances. A specific book
|
||||
instance of a book can be added only if there is book with matching ISBN already in
|
||||
the catalogue. Book must have non-empty title and price. At the time of adding an instance
|
||||
we decide whether it will be Circulating or Restricted. This enables
|
||||
us to have book with same ISBN as circulated and restricted at the same time (for instance,
|
||||
there is a book signed by the author that we want to keep as Restricted)
|
||||
读者实际上如何知道有哪些书可借?图书馆拥有图书目录(catalogue),
|
||||
图书及其具体实例会一并加入其中。只有当目录中已存在 ISBN 匹配的图书时,
|
||||
才能添加该图书的具体实例。图书必须具有非空标题和价格。在添加实例时,
|
||||
我们决定它是流通型(Circulating)还是限制型(Restricted)。这使我们可以
|
||||
让同一 ISBN 的图书同时存在流通型和限制型(例如,有一本作者签名的书,我们希望将其保留为限制型)。
|
||||
|
||||
## General assumptions
|
||||
|
||||
### Process discovery
|
||||
|
||||
The first thing we started with was domain exploration with the help of Big Picture EventStorming.
|
||||
The description you found in the previous chapter, landed on our virtual wall:
|
||||
我们首先借助 Big Picture EventStorming 进行领域探索。
|
||||
你在上一章看到的描述,被贴在了我们的虚拟墙上:
|
||||

|
||||
The EventStorming session led us to numerous discoveries, modeled with the sticky notes:
|
||||
EventStorming 会议引导我们做出了诸多发现,并用便利贴进行建模:
|
||||

|
||||
During the session we discovered following definitions:
|
||||
在会议期间,我们发现了以下定义:
|
||||

|
||||
|
||||
This made us think of real life scenarios that might happen. We discovered them described with the help of
|
||||
the **Example mapping**:
|
||||
这促使我们思考现实生活中可能发生的情景。我们借助
|
||||
**Example mapping** 对它们进行了描述:
|
||||

|
||||
|
||||
This in turn became the base for our *Design Level* sessions, where we analyzed each example:
|
||||
这进而成为我们 *Design Level* 会议的基础,我们在其中分析了每个示例:
|
||||

|
||||
|
||||
Please follow the links below to get more details on each of the mentioned steps:
|
||||
请通过以下链接了解上述各步骤的更多细节:
|
||||
- [Big Picture EventStorming](./docs/big-picture.md)
|
||||
- [Example Mapping](docs/example-mapping.md)
|
||||
- [Design Level EventStorming](docs/design-level.md)
|
||||
|
||||
### Project structure and architecture
|
||||
At the very beginning, not to overcomplicate the project, we decided to assign each bounded context
|
||||
to a separate package, which means that the system is a modular monolith. There are no obstacles, though,
|
||||
to put contexts into maven modules or finally into microservices.
|
||||
在项目伊始,为避免过度复杂化,我们决定将每个限界上下文(bounded context)
|
||||
分配到独立的包中,这意味着系统是一个模块化单体(modular monolith)。不过,
|
||||
将上下文拆分为 maven 模块乃至最终拆分为微服务,并无障碍。
|
||||
|
||||
Bounded contexts should (amongst others) introduce autonomy in the sense of architecture. Thus, each module
|
||||
encapsulating the context has its own local architecture aligned to problem complexity.
|
||||
In the case of a context, where we identified true business logic (**lending**) we introduced a domain model
|
||||
that is a simplified (for the purpose of the project) abstraction of the reality and utilized
|
||||
hexagonal architecture. In the case of a context, that during Event Storming turned out to lack any complex
|
||||
domain logic, we applied CRUD-like local architecture.
|
||||
限界上下文应当(除其他外)在架构意义上引入自治。因此,封装各上下文的模块
|
||||
拥有与其问题复杂度相匹配的本地架构。对于识别出真正业务逻辑(**lending**)的上下文,
|
||||
我们引入了领域模型——这是对现实的一种简化(为项目目的而简化)抽象,并采用了
|
||||
六边形架构(hexagonal architecture)。对于在 Event Storming 中证明缺乏复杂
|
||||
领域逻辑的上下文,我们采用了类 CRUD 的本地架构。
|
||||
|
||||

|
||||
|
||||
If we are talking about hexagonal architecture, it lets us separate domain and application logic from
|
||||
frameworks (and infrastructure). What do we gain with this approach? Firstly, we can unit test most important
|
||||
part of the application - **business logic** - usually without the need to stub any dependency.
|
||||
Secondly, we create ourselves an opportunity to adjust infrastructure layer without the worry of
|
||||
breaking the core functionality. In the infrastructure layer we intensively use Spring Framework
|
||||
as probably the most mature and powerful application framework with an incredible test support.
|
||||
More information about how we use Spring you will find [here](#spring).
|
||||
若谈及六边形架构,它使我们能够将领域逻辑和应用逻辑与
|
||||
框架(及基础设施)分离。采用这种方式我们能获得什么?首先,我们可以对应用程序最重要的部分——**业务逻辑**——进行单元测试,
|
||||
通常无需 stub 任何依赖。其次,我们为自己创造了调整基础设施层的机会,而无需担心
|
||||
破坏核心功能。在基础设施层中,我们大量使用 Spring Framework,
|
||||
它可能是目前最成熟、最强大且具备出色测试支持的应用框架。
|
||||
关于我们如何使用 Spring 的更多信息,请见[此处](#spring)。
|
||||
|
||||
As we already mentioned, the architecture was driven by Event Storming sessions. Apart from identifying
|
||||
contexts and their complexity, we could also make a decision that we separate read and write models (CQRS).
|
||||
As an example you can have a look at **Patron Profiles** and *Daily Sheets*.
|
||||
如前所述,架构由 Event Storming 会议驱动。除识别上下文及其复杂度外,
|
||||
我们还可以决定分离读写模型(CQRS)。例如,可以查看 **Patron Profiles** 和 *Daily Sheets*。
|
||||
|
||||
### Aggregates
|
||||
Aggregates discovered during Event Storming sessions communicate with each other with events. There is
|
||||
a contention, though, should they be consistent immediately or eventually? As aggregates in general
|
||||
determine business boundaries, eventual consistency sounds like a better choice, but choices in software
|
||||
are never costless. Providing eventual consistency requires some infrastructural tools, like message broker
|
||||
or event store. That's why we could (and did) start with immediate consistency.
|
||||
在 Event Storming 会议中发现的聚合(Aggregates)通过事件相互通信。不过存在争议:
|
||||
它们应当立即保持一致,还是最终保持一致?由于聚合总体上界定了业务边界,
|
||||
最终一致性(eventual consistency)听起来是更好的选择,但软件中的选择从无免费午餐。
|
||||
提供最终一致性需要一些基础设施工具,例如消息代理(message broker)
|
||||
或事件存储(event store)。因此我们可以(也确实)从立即一致性开始。
|
||||
|
||||
> Good architecture is the one which postpones all important decisions
|
||||
> 好的架构会推迟所有重要决策
|
||||
|
||||
... that's why we made it easy to change the consistency model, providing tests for each option, including
|
||||
basic implementations based on **DomainEvents** interface, which can be adjusted to our needs and
|
||||
toolset in future. Let's have a look at following examples:
|
||||
……正因如此,我们使一致性模型易于变更,并为每种选项提供测试,包括基于 **DomainEvents** 接口的基础实现,
|
||||
未来可根据我们的需求和工具集进行调整。请看以下示例:
|
||||
|
||||
* Immediate consistency
|
||||
* 立即一致性(Immediate consistency)
|
||||
```groovy
|
||||
def 'should synchronize Patron, Book and DailySheet with events'() {
|
||||
given:
|
||||
@@ -146,9 +141,9 @@ toolset in future. Let's have a look at following examples:
|
||||
.get("COUNT(*)") == 1
|
||||
}
|
||||
```
|
||||
_Please note that here we are just reading from database right after events are being published_
|
||||
|
||||
Simple implementation of the event bus is based on Spring application events:
|
||||
_请注意,此处我们是在事件发布后立即从数据库读取_
|
||||
|
||||
事件总线的简单实现基于 Spring 应用事件(Spring application events):
|
||||
```java
|
||||
@AllArgsConstructor
|
||||
public class JustForwardDomainEventPublisher implements DomainEvents {
|
||||
@@ -162,7 +157,7 @@ toolset in future. Let's have a look at following examples:
|
||||
}
|
||||
```
|
||||
|
||||
* Eventual consistency
|
||||
* 最终一致性(Eventual consistency)
|
||||
```groovy
|
||||
def 'should synchronize Patron, Book and DailySheet with events'() {
|
||||
given:
|
||||
@@ -191,10 +186,10 @@ toolset in future. Let's have a look at following examples:
|
||||
}
|
||||
}
|
||||
```
|
||||
_Please note that the test looks exactly the same as previous one, but now we utilized Groovy's
|
||||
**PollingConditions** to perform asynchronous functionality tests_
|
||||
_请注意,该测试与之前的测试看起来完全相同,但现在我们利用了 Groovy 的
|
||||
**PollingConditions** 来执行异步功能测试_
|
||||
|
||||
Sample implementation of event bus is following:
|
||||
事件总线的示例实现如下:
|
||||
|
||||
```java
|
||||
@AllArgsConstructor
|
||||
@@ -218,26 +213,15 @@ toolset in future. Let's have a look at following examples:
|
||||
}
|
||||
```
|
||||
|
||||
To clarify, we should always aim for aggregates that can handle a business operation atomically
|
||||
(transactionally if you like), so each aggregate should be as independent and decoupled from other
|
||||
aggregates as possible. Thus, eventual consistency is promoted. As we already mentioned, it comes
|
||||
with some tradeoffs, so from the pragmatic point of view immediate consistency is also a choice.
|
||||
You might ask yourself a question now: _What if I don't have any events yet?_. Well, a pragmatic
|
||||
approach would be to encapsulate the communication between aggregates in a _Service-like_ class,
|
||||
where you could call proper aggregates line by line explicitly.
|
||||
需要澄清的是,我们应始终追求能够原子性地(若你愿意,也可以说是事务性地)处理业务操作的聚合,因此每个聚合应尽可能独立,并与其他聚合解耦。由此,最终一致性(eventual consistency)受到推崇。如前所述,这会带来一些权衡;从务实角度看,即时一致性(immediate consistency)同样是一种选择。
|
||||
你现在可能会问自己一个问题:_如果我还没有任何事件呢?_ 那么,一种务实的方法是将聚合之间的通信封装在一个类似 _Service_ 的类中,在其中可以按行显式调用相应的聚合。
|
||||
|
||||
### Events
|
||||
Talking about inter-aggregate communication, we must remember that events reduce coupling, but don't remove
|
||||
it completely. Thus, it is very vital to share(publish) only those events, that are necessary for other
|
||||
aggregates to exist and function. Otherwise there is a threat that the level of coupling will increase
|
||||
introducing **feature envy**, because other aggregates might start using those events to perform actions
|
||||
they are not supposed to perform. A solution to this problem could be the distinction of domain events
|
||||
and integration events, which will be described here soon.
|
||||
谈到聚合间通信,我们必须记住:事件能降低耦合,但并不能完全消除耦合。因此,只共享(发布)其他聚合存在和运行所必需的事件,这一点至关重要。否则,耦合程度可能会上升,并引入 **feature envy(特性依恋)**,因为其他聚合可能开始利用这些事件去执行本不该由它们执行的操作。解决该问题的一种方案是区分领域事件(domain events)与集成事件(integration events),相关内容将很快在此说明。
|
||||
|
||||
### Events in Repositories
|
||||
Repositories are one of the most popular design pattern. They abstract our domain model from data layer.
|
||||
In other words, they deal with state. That said, a common use-case is when we pass a new state to our repository,
|
||||
so that it gets persisted. It may look like so:
|
||||
仓储(Repository)是最常见的设计模式之一。它将领域模型与数据层解耦。
|
||||
换言之,它处理的是状态。也就是说,常见用例是我们将新状态传给仓储,以便持久化。可能看起来像这样:
|
||||
|
||||
```java
|
||||
public class BusinessService {
|
||||
@@ -252,12 +236,12 @@ public class BusinessService {
|
||||
}
|
||||
```
|
||||
|
||||
Conceptually, between 1st and 3rd line of that business method we change state of our Patron from A to B.
|
||||
This change might be calculated by dirty checking or we might just override entire Patron state in the database.
|
||||
Third option is _Let's make implicit explicit_ and actually call this state change A->B an **event**.
|
||||
After all, event-driven architecture is all about promoting state changes as domain events.
|
||||
从概念上讲,在该业务方法第 1 行与第 3 行之间,我们将 Patron 的状态从 A 变更为 B。
|
||||
这一变更可能通过脏检查(dirty checking)计算得出,也可能只是在数据库中整体覆盖 Patron 状态。
|
||||
第三种选择是 _让隐式变为显式_,并将这次 A->B 的状态变更真正称为一个 **event(事件)**。
|
||||
毕竟,事件驱动架构的核心,就是推动将状态变更提升为领域事件(domain events)。
|
||||
|
||||
Thanks to this our domain model may become immutable and just return events as results of invoking a command like so:
|
||||
得益于此,我们的领域模型可以变为不可变的,并在调用命令后仅返回事件,例如:
|
||||
|
||||
```java
|
||||
public BookPlacedOnHold placeOnHold(AvailableBook book) {
|
||||
@@ -265,7 +249,7 @@ public BookPlacedOnHold placeOnHold(AvailableBook book) {
|
||||
}
|
||||
```
|
||||
|
||||
And our repository might operate directly on events like so:
|
||||
而我们的仓储可以直接基于事件进行操作,例如:
|
||||
|
||||
```java
|
||||
public interface PatronRepository {
|
||||
@@ -274,16 +258,9 @@ public interface PatronRepository {
|
||||
```
|
||||
|
||||
### ArchUnit
|
||||
成功项目的主要组成部分之一,是能够引导团队朝正确方向前进的技术领导力。尽管如此,仍有一些工具可以帮助团队保持代码整洁、守护架构,使项目不会沦为 Big Ball of Mud(大泥球),从而令开发与维护都更加愉快。我们提出的第一种选择是 [ArchUnit](https://www.archunit.org/))——一个 Java 架构测试工具。ArchUnit 允许你为架构编写单元测试,从而使其始终与最初愿景保持一致。Maven 模块也可以作为替代方案,但此处我们聚焦前者。
|
||||
|
||||
One of the main components of a successful project is technical leadership that lets the team go in the right
|
||||
direction. Nevertheless, there are tools that can support teams in keeping the code clean and protect the
|
||||
architecture, so that the project won't become a Big Ball of Mud, and thus will be pleasant to develop and
|
||||
to maintain. The first option, the one we proposed, is [ArchUnit](https://www.archunit.org/) - a Java architecture
|
||||
test tool. ArchUnit lets you write unit tests of your architecture, so that it is always consistent with initial
|
||||
vision. Maven modules could be an alternative as well, but let's focus on the former.
|
||||
|
||||
In terms of hexagonal architecture, it is essential to ensure, that we do not mix different levels of
|
||||
abstraction (hexagon levels):
|
||||
就六边形架构(hexagonal architecture)而言,确保我们不混用不同抽象层级(hexagon levels)至关重要:
|
||||
```java
|
||||
@ArchTest
|
||||
public static final ArchRule model_should_not_depend_on_infrastructure =
|
||||
@@ -294,7 +271,7 @@ public static final ArchRule model_should_not_depend_on_infrastructure =
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("..infrastructure..");
|
||||
```
|
||||
and that frameworks do not affect the domain model
|
||||
并确保框架不会影响领域模型
|
||||
```java
|
||||
@ArchTest
|
||||
public static final ArchRule model_should_not_depend_on_spring =
|
||||
@@ -307,24 +284,18 @@ public static final ArchRule model_should_not_depend_on_spring =
|
||||
```
|
||||
|
||||
### Functional thinking
|
||||
When you look at the code you might find a scent of functional programming. Although we do not follow
|
||||
a _clean_ FP, we try to think of business processes as pipelines or workflows, utilizing functional style through
|
||||
following concepts.
|
||||
查看代码时,你可能会嗅到一丝函数式编程(functional programming)的气息。尽管我们并未遵循 _纯粹的_ FP,但我们尝试将业务流程视为流水线或工作流,并通过以下概念以函数式风格加以运用。
|
||||
|
||||
_Please note that this is not a reference project for FP._
|
||||
_请注意,本项目并非 FP 的参考项目。_
|
||||
|
||||
#### Immutable objects
|
||||
Each class that represents a business concept is immutable, thanks to which we:
|
||||
* provide full encapsulation and objects' states protection,
|
||||
* secure objects for multithreaded access,
|
||||
* control all side effects much clearer.
|
||||
每个表示业务概念的类都是不可变的,由此我们可以:
|
||||
* 提供完整封装并保护对象状态,
|
||||
* 保障对象在多线程访问下的安全性,
|
||||
* 更清晰地控制所有副作用。
|
||||
|
||||
#### Pure functions
|
||||
We model domain operations, discovered in Design Level Event Storming, as pure functions, and declare them in
|
||||
both domain and application layers in the form of Java's functional interfaces. Their implementations are placed
|
||||
in infrastructure layer as ordinary methods with side effects. Thanks to this approach we can follow the abstraction
|
||||
of ubiquitous language explicitly, and keep this abstraction implementation-agnostic. As an example, you could have
|
||||
a look at `FindAvailableBook` interface and its implementation:
|
||||
我们将设计层事件风暴(Design Level Event Storming)中发现的领域操作建模为纯函数(pure functions),并在领域层与应用层中以 Java 函数式接口的形式声明它们。其实现则作为带有副作用的普通方法放在基础设施层。借助这一做法,我们可以显式遵循通用语言(ubiquitous language)的抽象,并保持该抽象与具体实现无关。例如,你可以查看 `FindAvailableBook` 接口及其实现:
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
@@ -361,14 +332,10 @@ class BookDatabaseRepository implements FindAvailableBook {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Type system
|
||||
_Type system - like_ modelling - we modelled each domain object's state discovered during EventStorming as separate
|
||||
classes: `AvailableBook`, `BookOnHold`, `CheckedOutBook`. With this approach we provide much clearer abstraction than
|
||||
having a single `Book` class with an enum-based state management. Moving the logic to these specific classes brings
|
||||
Single Responsibility Principle to a different level. Moreover, instead of checking invariants in every business method
|
||||
we leave the role to the compiler. As an example, please consider following scenario: _you can place on hold only a book
|
||||
that is currently available_. We could have done it in a following way:
|
||||
|
||||
#### 类型系统
|
||||
_类型系统(type system)——与建模类似——_ 我们将 EventStorming 过程中发现的每个领域对象状态建模为独立的
|
||||
类:`AvailableBook`、`BookOnHold`、`CheckedOutBook`。采用这种方式,我们比使用单一 `Book` 类配合基于枚举的状态管理提供了更清晰的抽象。将逻辑迁移到这些特定类中,把单一职责原则(Single Responsibility Principle)提升到了新的层次。此外,我们不再在每个业务方法中检查不变量,而是将这一职责交给编译器。例如,请考虑以下场景:_只有当前可借阅的图书才能被预约(place on hold)_。我们可以这样实现:
|
||||
```java
|
||||
public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(Book book) {
|
||||
if (book.status == AVAILABLE) {
|
||||
@@ -376,38 +343,31 @@ public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(Book book) {
|
||||
}
|
||||
}
|
||||
```
|
||||
but we use the _type system_ and declare method of following signature
|
||||
但我们使用_类型系统_,并声明具有如下签名的方法
|
||||
```java
|
||||
public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
|
||||
...
|
||||
}
|
||||
```
|
||||
The more errors we discover at compile time the better.
|
||||
在编译期发现的错误越多越好。
|
||||
|
||||
Yet another advantage of applying such type system is that we can represent business flows and state transitions
|
||||
with functions much easier. As an example, following functions:
|
||||
应用此类类型系统的另一项优势是,我们能够用函数更轻松地表达业务流程与状态转换。例如,以下函数:
|
||||
```
|
||||
placeOnHold: AvailableBook -> BookHoldFailed | BookPlacedOnHold
|
||||
cancelHold: BookOnHold -> BookHoldCancelingFailed | BookHoldCanceled
|
||||
```
|
||||
are much more concise and descriptive than these:
|
||||
比下面这些更加简洁且富有表达力:
|
||||
```
|
||||
placeOnHold: Book -> BookHoldFailed | BookPlacedOnHold
|
||||
cancelHold: Book -> BookHoldCancelingFailed | BookHoldCanceled
|
||||
```
|
||||
as here we have a lot of constraints hidden within function implementations.
|
||||
因为在后者中,许多约束都隐藏在函数实现内部。
|
||||
|
||||
Moreover if you think of your domain as a set of operations (functions) that are being executed on business objects
|
||||
(aggregates) you don't think of any execution model (like async processing). It is fine, because you don't have to.
|
||||
Domain functions are free from I/O operations, async, and other side-effects-prone things, which are put into the
|
||||
infrastructure layer. Thanks to this, we can easily test them without mocking mentioned parts.
|
||||
此外,若将领域视为在一组业务对象(聚合,aggregates)上执行的一组操作(函数),你就不必考虑任何执行模型(例如异步处理)。这完全合理,因为你无需考虑。领域函数不受 I/O 操作、异步以及其他易产生副作用的操作的影响,这些都被放在基础设施层。得益于此,我们可以在不进行 mock 的情况下轻松测试它们。
|
||||
|
||||
#### Monads
|
||||
Business methods might have different results. One might return a value or a `null`, throw an exception when something
|
||||
unexpected happens or just return different objects under different circumstances. All those situations are typical
|
||||
to object-oriented languages like Java, but do not fit into functional style. We are dealing with this issues
|
||||
with monads (monadic containers provided by [Vavr](https://www.vavr.io)):
|
||||
* When a method returns optional value, we use the `Option` monad:
|
||||
业务方法可能产生不同的结果。一种可能返回值或 `null`,在发生意外时抛出异常,或在不同情况下返回不同对象。这些情况在 Java 等面向对象语言中很常见,却不符合函数式风格。我们通过 monad(由 [Vavr](https://www.vavr.io)): 提供的单子容器)来解决这些问题
|
||||
* 当方法返回可选值时,我们使用 `Option` monad:
|
||||
|
||||
```java
|
||||
Option<Book> findBy(BookId bookId) {
|
||||
@@ -415,7 +375,7 @@ with monads (monadic containers provided by [Vavr](https://www.vavr.io)):
|
||||
}
|
||||
```
|
||||
|
||||
* When a method might return one of two possible values, we use the `Either` monad:
|
||||
* 当方法可能返回两种可能值之一时,我们使用 `Either` monad:
|
||||
|
||||
```java
|
||||
Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
|
||||
@@ -423,7 +383,7 @@ with monads (monadic containers provided by [Vavr](https://www.vavr.io)):
|
||||
}
|
||||
```
|
||||
|
||||
* When an exception might occur, we use `Try` monad:
|
||||
* 当可能发生异常时,我们使用 `Try` monad:
|
||||
|
||||
```java
|
||||
Try<Result> placeOnHold(@NonNull PlaceOnHoldCommand command) {
|
||||
@@ -431,14 +391,10 @@ with monads (monadic containers provided by [Vavr](https://www.vavr.io)):
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to this, we can follow the functional programming style, but we also enrich our domain language and
|
||||
make our code much more readable for the clients.
|
||||
得益于此,我们可以遵循函数式编程风格,同时丰富领域语言,使代码对调用方而言更具可读性。
|
||||
|
||||
#### Pattern Matching
|
||||
Depending on a type of a given book object we often need to perform different actions. Series of if/else or switch/case statements
|
||||
could be a choice, but it is the pattern matching that provides the most conciseness and flexibility. With the code
|
||||
like below we can check numerous patterns against objects and access their constituents, so our code has a minimal dose
|
||||
of language-construct noise:
|
||||
#### 模式匹配(Pattern Matching)
|
||||
根据给定图书对象的类型,我们经常需要执行不同的操作。一连串 if/else 或 switch/case 语句固然可行,但模式匹配能提供最佳的简洁性与灵活性。借助如下代码,我们可以针对对象检查多种模式并访问其组成部分,从而使代码中的语言构造噪声降到最低:
|
||||
```java
|
||||
private Book handleBookPlacedOnHold(Book book, BookPlacedOnHold bookPlacedOnHold) {
|
||||
return API.Match(book).of(
|
||||
@@ -449,18 +405,17 @@ private Book handleBookPlacedOnHold(Book book, BookPlacedOnHold bookPlacedOnHold
|
||||
}
|
||||
```
|
||||
|
||||
### (No) ORM
|
||||
If you run `mvn dependency:tree` you won't find any JPA implementation. Although we think that ORM solutions (like Hibernate)
|
||||
are very powerful and useful, we decided not to use them, as we wouldn't utilize their features. What features are
|
||||
talking about? Lazy loading, caching, dirty checking. Why don't we need them? We want to have more control
|
||||
over SQL queries and minimize the object-relational impedance mismatch ourselves. Moreover, thanks to relatively
|
||||
small aggregates, containing as little data as it is required to protect the invariants, we don't need the
|
||||
lazy loading mechanism either.
|
||||
With Hexagonal Architecture we have the ability to separate domain and persistence models and test them
|
||||
independently. Moreover, we can also introduce different persistence strategies for different aggregates.
|
||||
In this project, we utilize both plain SQL queries and `JdbcTemplate` and use new and very promising
|
||||
project called Spring Data JDBC, that is free from the JPA-related overhead mentioned before.
|
||||
Please find below an example of a repository:
|
||||
### (无)ORM
|
||||
如果运行 `mvn dependency:tree`,你不会找到任何 JPA 实现。尽管我们认为 ORM 解决方案(如 Hibernate)
|
||||
非常强大且实用,但我们决定不使用它们,因为我们无法充分利用其特性。我们指的是哪些特性?懒加载(lazy loading)、缓存(caching)、脏检查(dirty checking)。为什么不需要?我们希望对 SQL 查询拥有更多控制权,
|
||||
并自行尽量减小对象-关系阻抗不匹配(object-relational impedance mismatch)。此外,得益于相对较小的聚合——
|
||||
其中仅包含保护不变量所需的最少数据——我们也不需要
|
||||
懒加载机制。
|
||||
借助六边形架构(Hexagonal Architecture),我们能够分离领域模型与持久化模型,并
|
||||
独立地对它们进行测试。此外,我们还可以为不同聚合引入不同的持久化策略。
|
||||
在本项目中,我们同时使用原生 SQL 查询与 `JdbcTemplate`,并采用了崭新且极具前景的
|
||||
项目 Spring Data JDBC,它摆脱了前述与 JPA 相关的开销。
|
||||
下面是仓储(repository)的一个示例:
|
||||
|
||||
```java
|
||||
interface PatronEntityRepository extends CrudRepository<PatronDatabaseEntity, Long> {
|
||||
@@ -471,7 +426,7 @@ interface PatronEntityRepository extends CrudRepository<PatronDatabaseEntity, Lo
|
||||
}
|
||||
```
|
||||
|
||||
At the same time we propose other way of persisting aggregates, with plain SQL queries and `JdbcTemplate`:
|
||||
同时,我们还提出了另一种持久化聚合的方式,即结合原生 SQL 查询与 `JdbcTemplate`:
|
||||
|
||||
```java
|
||||
@AllArgsConstructor
|
||||
@@ -495,17 +450,16 @@ class BookDatabaseRepository implements BookRepository, FindAvailableBook, FindB
|
||||
...
|
||||
}
|
||||
```
|
||||
_Please note that despite having the ability to choose different persistence implementations for aggregates
|
||||
it is recommended to stick to one option within the app/team_
|
||||
_请注意,尽管可以为聚合选择不同的持久化实现,
|
||||
仍建议在应用/团队内坚持使用同一种方案。_
|
||||
|
||||
### Architecture-code gap
|
||||
We put a lot of attention to keep the consistency between the overall architecture (including diagrams)
|
||||
and the code structure. Having identified bounded contexts we could organize them in modules (packages, to
|
||||
be more specific). Thanks to this we gain the famous microservices' autonomy, while having a monolithic
|
||||
application. Each package has well defined public API, encapsulating all implementation details by using
|
||||
package-protected or private scopes.
|
||||
### 架构与代码的鸿沟
|
||||
我们十分注重保持整体架构(包括各类图表)
|
||||
与代码结构之间的一致性。在识别出有界上下文(bounded contexts)后,我们可以将它们组织为模块(更准确地说,是包,package)。
|
||||
借此,我们在单体应用中也能获得众所周知的微服务自治性。每个包都有定义清晰的公共 API,并通过
|
||||
包级保护或私有作用域封装所有实现细节。
|
||||
|
||||
Just by looking at the package structure:
|
||||
仅看包结构:
|
||||
|
||||
```
|
||||
└── library
|
||||
@@ -534,28 +488,25 @@ Just by looking at the package structure:
|
||||
├── model
|
||||
└── web
|
||||
```
|
||||
you can see that the architecture is screaming that it has two bounded contexts: **catalogue**
|
||||
and **lending**. Moreover, the **lending context** is built around five business objects: **book**,
|
||||
**dailysheet**, **librarybranch**, **patron**, and **patronprofile**, while **catalogue** has no subpackages,
|
||||
which suggests that it might be a CRUD with no complex logic inside. Please find the architecture diagram
|
||||
below.
|
||||
你就能看出架构在大声宣告它包含两个有界上下文:**catalogue**(目录)
|
||||
与 **lending**(借阅)。此外,**lending context** 围绕五个业务对象构建:**book**(图书)、
|
||||
**dailysheet**、**librarybranch**、**patron** 和 **patronprofile**,而 **catalogue** 没有子包,
|
||||
这表明它可能只是一个内部没有复杂逻辑的 CRUD。架构图见下。
|
||||
|
||||

|
||||
|
||||
Yet another advantage of this approach comparing to packaging by layer for example is that in order to
|
||||
deliver a functionality you would usually need to do it in one package only, which is the aforementioned
|
||||
autonomy. This autonomy, then, could be transferred to the level of application as soon as we split our
|
||||
_context-packages_ into separate microservices. Following this considerations, autonomy can be given away
|
||||
to a product team that can take care of the whole business area end-to-end.
|
||||
与按层分包等方式相比,该方案的又一优势在于:为了
|
||||
交付某项功能,你通常只需在一个包内完成,这正是前述的
|
||||
自治性。一旦我们将 _context-packages_(上下文包)拆分为独立的微服务,这种自治性便可延伸至应用层面。基于上述考量,自治性可以下放给
|
||||
能够端到端负责整个业务领域的产品团队。
|
||||
|
||||
### Model-code gap
|
||||
In our project we do our best to reduce _model-code gap_ to bare minimum. It means we try to put equal attention
|
||||
to both the model and the code and keep them consistent. Below you will find some examples.
|
||||
### 模型与代码的差距(model-code gap)
|
||||
在本项目中,我们尽力将 _model-code gap_ 降至最低。这意味着我们同等重视模型与代码,并努力保持二者一致。下方是一些示例。
|
||||
|
||||
#### Placing on hold
|
||||
#### 预约(Placing on hold)
|
||||

|
||||
|
||||
Starting with the easiest part, below you will find the model classes corresponding to depicted command and events:
|
||||
先从最简单的部分说起,下方是与图中命令和事件相对应的模型类:
|
||||
|
||||
```java
|
||||
@Value
|
||||
@@ -582,9 +533,9 @@ class BookHoldFailed implements PatronEvent {
|
||||
}
|
||||
```
|
||||
|
||||
We know it might not look impressive now, but if you have a look at the implementation of an aggregate,
|
||||
you will see that the code reflects not only the aggregate name, but also the whole scenario of `PlaceOnHold`
|
||||
command handling. Let us uncover the details:
|
||||
我们知道它现在可能看起来并不起眼,但如果你查看聚合(aggregate)的实现,
|
||||
你会发现代码不仅体现了聚合名称,还完整反映了 `PlaceOnHold`
|
||||
命令处理的整个场景。下面来揭示其中的细节:
|
||||
|
||||
```java
|
||||
public class Patron {
|
||||
@@ -597,10 +548,10 @@ public class Patron {
|
||||
}
|
||||
```
|
||||
|
||||
The signature of `placeOnHold` method screams, that it is possible to place a book on hold only when it
|
||||
is available (more information about protecting invariants by compiler you will find in [Type system section](#type-system)).
|
||||
Moreover, if you try to place available book on hold it can **either** fail (`BookHoldFailed`) or produce some events -
|
||||
what events?
|
||||
`placeOnHold` 方法的签名一目了然:只有在图书可借时才能预约
|
||||
(关于如何通过编译器保护不变量(invariants)的更多信息,请参阅[类型系统章节](#type-system))。
|
||||
此外,若你尝试预约一本可借的图书,它**要么**失败(`BookHoldFailed`),**要么**产生某些事件——
|
||||
会产生哪些事件?
|
||||
|
||||
```java
|
||||
@Value
|
||||
@@ -629,13 +580,12 @@ class BookPlacedOnHoldEvents implements PatronEvent {
|
||||
}
|
||||
```
|
||||
|
||||
`BookPlacedOnHoldEvents` is a container for `BookPlacedOnHold` event, and - if patron has 5 book placed on hold already -
|
||||
`MaximumNumberOfHoldsReached` (please mind the `Option` monad). You can see now how perfectly the code reflects
|
||||
the model.
|
||||
`BookPlacedOnHoldEvents` 是 `BookPlacedOnHold` 事件的容器,并且——若读者已预约了 5 本书——
|
||||
还会包含 `MaximumNumberOfHoldsReached`(请注意 `Option` 单子(monad))。你现在可以看到代码与模型是多么完美地对应。
|
||||
|
||||
It is not everything, though. In the picture above you can also see a big rectangular yellow card with rules (policies)
|
||||
that define the conditions that need to be fulfilled in order to get the given result. All those rules are implemented
|
||||
as functions **either** allowing or rejecting the hold:
|
||||
但这还不是全部。在上图中,你还能看到一张大的矩形黄色卡片,上面有规则(policies),
|
||||
这些规则定义了要获得给定结果所需满足的条件。所有这些规则都实现为
|
||||
**要么**允许、**要么**拒绝预约的函数:
|
||||
|
||||

|
||||
```java
|
||||
@@ -681,14 +631,13 @@ PlacingOnHoldPolicy onlyResearcherPatronsCanPlaceOpenEndedHolds = (AvailableBook
|
||||
```
|
||||
|
||||
#### Spring
|
||||
Spring Framework seems to be the most popular Java framework ever used. Unfortunately it is also quite common
|
||||
to overuse its features in the business code. What you find in this project is that the domain packages
|
||||
are fully focused on modelling business problems, and are free from any DI, which makes it easy to
|
||||
unit-test it which is invaluable in terms of code reliability and maintainability. It does not mean,
|
||||
though, that we do not use Spring Framework - we do. Below you will find some details:
|
||||
- Each bounded context has its own independent application context. It means that we removed the runtime
|
||||
coupling, which is a step towards extracting modules (and microservices). How did we do that? Let's have
|
||||
a look:
|
||||
Spring Framework 似乎是有史以来最流行的 Java 框架。遗憾的是,在业务代码中过度使用其功能的情况也相当普遍。
|
||||
在本项目中你会发现,领域包完全专注于业务问题的建模,且没有任何 DI(依赖注入),
|
||||
这使得单元测试变得容易,对代码可靠性和可维护性而言弥足珍贵。不过,这并不意味着
|
||||
我们不使用 Spring Framework——我们确实在使用。以下是一些细节:
|
||||
- 每个限界上下文(bounded context)都有自己独立的 application context。这意味着我们消除了运行时
|
||||
耦合,这是向提取模块(以及微服务)迈出的一步。我们是如何做到的?让我们
|
||||
看一看:
|
||||
```java
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@@ -703,17 +652,16 @@ a look:
|
||||
}
|
||||
}
|
||||
```
|
||||
- As you could see above, we also try not to use component scan wherever possible. Instead we utilize
|
||||
`@Configuration` classes where we define module specific beans in the infrastructure layer. Those
|
||||
configuration classes are explicitly declared in the main application class.
|
||||
- 如上所示,我们也尽可能避免使用 component scan。取而代之的是,我们使用
|
||||
`@Configuration` 类,在基础设施层定义模块特定的 bean。这些
|
||||
配置类在主编应用类中显式声明。
|
||||
|
||||
### Tests
|
||||
Tests are written in a BDD manner, expressing stories defined with Example Mapping.
|
||||
It means we utilize both TDD and Domain Language discovered with Event Storming.
|
||||
### 测试
|
||||
测试以 BDD 方式编写,表达通过 Example Mapping 定义的用户故事。
|
||||
这意味着我们同时运用了 TDD 以及通过 Event Storming 发现的领域语言(Domain Language)。
|
||||
|
||||
We also made an effort to show how to create a DSL, that enables to write
|
||||
tests as if they were sentences taken from the domain descriptions. Please
|
||||
find an example below:
|
||||
我们还努力展示如何创建 DSL,使测试读起来就像取自领域描述的句子。请
|
||||
在下方查看一个示例:
|
||||
|
||||
```groovy
|
||||
def 'should make book available when hold canceled'() {
|
||||
@@ -730,25 +678,24 @@ def 'should make book available when hold canceled'() {
|
||||
availableBook.version == bookOnHold.version
|
||||
}
|
||||
```
|
||||
_Please also note the **when** block, where we manifest the fact that books react to
|
||||
cancellation event_
|
||||
_另请注意 **when** 代码块,其中我们体现了图书会对取消事件做出响应这一事实_
|
||||
|
||||
## How to contribute
|
||||
## 如何参与贡献
|
||||
|
||||
The project is still under construction, so if you like it enough to collaborate, just let us
|
||||
know or simply create a Pull Request.
|
||||
项目仍在建设中,因此如果你足够喜欢并愿意合作,请告知我们
|
||||
或直接创建 Pull Request。
|
||||
|
||||
|
||||
## How to Build
|
||||
## 如何构建
|
||||
|
||||
### Requirements
|
||||
### 要求
|
||||
|
||||
* Java 11
|
||||
* Maven
|
||||
|
||||
### Quickstart
|
||||
### 快速开始
|
||||
|
||||
You can run the library app by simply typing the following:
|
||||
只需输入以下命令即可运行图书馆应用:
|
||||
|
||||
```console
|
||||
$ mvn spring-boot:run
|
||||
@@ -760,9 +707,9 @@ $ mvn spring-boot:run
|
||||
|
||||
```
|
||||
|
||||
### Build a Jar package
|
||||
### 构建 Jar 包
|
||||
|
||||
You can build a jar with maven like so:
|
||||
你可以使用 Maven 这样构建 jar:
|
||||
|
||||
```console
|
||||
$ mvn clean package
|
||||
@@ -774,44 +721,44 @@ $ mvn clean package
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
### Build with Docker
|
||||
### 使用 Docker 构建
|
||||
|
||||
If you've already built the jar file you can run:
|
||||
如果你已经构建好了 jar 文件,可以运行:
|
||||
|
||||
```console
|
||||
docker build -t spring/library .
|
||||
```
|
||||
|
||||
Otherwise you can build the jar file using the multistage dockerfile:
|
||||
否则,你可以使用多阶段 Dockerfile 来构建 jar 文件:
|
||||
|
||||
```console
|
||||
docker build -t spring/library -f Dockerfile.build .
|
||||
```
|
||||
|
||||
Either way once built you can run it like so:
|
||||
无论哪种方式,构建完成后你都可以这样运行:
|
||||
|
||||
```console
|
||||
$ docker run -ti --rm --name spring-library -p 8080:8080 spring/library
|
||||
```
|
||||
|
||||
### Production ready metrics and visualization
|
||||
To run the application as well as Prometheus and Grafana dashboard for visualizing metrics you can run all services:
|
||||
### 生产就绪的指标与可视化
|
||||
|
||||
要运行应用程序以及用于可视化指标的 Prometheus 和 Grafana 仪表板,你可以启动所有服务:
|
||||
|
||||
```console
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
If everything goes well, you can access the following services at given location:
|
||||
* http://localhost:8080/actuator/prometheus - published Micrometer metrics
|
||||
* http://localhost:9090 - Prometheus dashboard
|
||||
* http://localhost:3000 - Grafana dashboard
|
||||
如果一切顺利,你可以在以下地址访问相关服务:
|
||||
* http://localhost:8080/actuator/prometheus - 已发布的 Micrometer 指标
|
||||
* http://localhost:9090 - Prometheus 仪表板
|
||||
* http://localhost:3000 - Grafana 仪表板
|
||||
|
||||
In order to see some metrics, you must create a dashboard. Go to `Create` -> `Import` and select attached `jvm-micrometer_rev8.json`. File has been pulled from
|
||||
`https://grafana.com/grafana/dashboards/4701`.
|
||||
要查看指标,你必须创建一个仪表板。前往 `Create` -> `Import`,并选择附带的 `jvm-micrometer_rev8.json`。该文件摘自 `https://grafana.com/grafana/dashboards/4701`。
|
||||
|
||||
Please note application will be run with `local` Spring profile to setup some initial data.
|
||||
请注意,应用程序将使用 `local` Spring profile 运行,以设置一些初始数据。
|
||||
|
||||
## References
|
||||
## 参考资料
|
||||
|
||||
1. [Introducing EventStorming](https://leanpub.com/introducing_eventstorming) by Alberto Brandolini
|
||||
2. [Domain Modelling Made Functional](https://pragprog.com/book/swdddf/domain-modeling-made-functional) by Scott Wlaschin
|
||||
|
||||
Reference in New Issue
Block a user