领域驱动设计是一种软件开发方法,其基础是使软件深刻反映真实世界的系统或过程。
领域驱动设计中的“领域”是指“应用程序逻辑所围绕的知识和活动领域”。换句话说,“领域”是在软件领域中通常被称为“业务逻辑”的东西。
在域驱动设计中,业务逻辑被视为软件的心脏。
你会在本文中找到对领域驱动设计的介绍,该介绍主要按照Eric Evans的书中的解释进行。还存在其他实现和词汇,以及相似体系结构共享相同原理,例如清洁架构和六边形架构。
领域驱动设计的目标是把领域代码从技术细节中释放,从而拥有更多空间来处理其复杂性。它非常适合处理高度复杂的领域,并且项目开始陷入遗留问题的处理。
实施领域驱动方法也意味着一开始花费更多的成本。开发人员将首先面临陡峭的学习曲线和掌握架构,这将使构建软件所需的时间更长。由于这些原因,不建议在简单的项目和无经验的团队中应用领域驱动设计。
在Inato,域驱动设计非常合适,因为我们开始发现我们的代码难以测试,从功能的角度很难阅读,并且在出现新的用例时很难扩展。产品的复杂性正在迅速上升,我们需要扩展代码的基础架构来处理它并更快地进行交付。
在深入探讨概念之前,这是实施领域驱动开发的代码的一个小示例。本示例实现对购物车的更新。我鼓励你在阅读完本文的其余部分后再回头看看这个例子。
从中,你可以看出:
我们将要讨论领域驱动设计里面的三个核心概念。
领域驱动设计专注于领域建模,并将模型(或业务逻辑)与实现细节(例如我们使用哪个数据库)分开。
确实,如果将与领域相关的代码与其他代码混合在一起,则很快就很难进行推理了。
推荐的体系结构由4层组成:展现层、应用层、领域层和基础设施层。
负责向用户显示信息并接收用户的命令。外部参与者有时可能是另一个计算机系统,而不是人类用户。
定义软件应该执行的工作(用例)并协调域对象以解决问题。
该层保持简单。它不包含业务规则或知识,而仅协调任务并将工作委托给下一层的域对象协作。
它没有反映业务情况的状态,但是可以具有反映用户或程序的任务进度的状态。
负责代表业务概念,有关业务状况的信息和业务规则。
在这层中反应业务状况的状态将会被使用,而状态存储的技术细节委托给基础结构。
该层是业务软件的核心。
向更高层的提供通用技术功能:如应用程序的消息发送,领域的持久性,UI的绘图小部件等等。基础架构层还可以通过架构框架支持四层之间的交互模式。
领域建模是一种使用概念和结构描述领域知识的活动,这些概念和结构将有助于推理和实现领域。
其基本约束是该模型必须既有助于功能的实现,又必须代表现实生活中的知识。
为了在代码中强制执行领域的表示,领域驱动设计还鼓励使用“统一建模语言”,该语言在开发人员和业务人员之间共享。
有3种工具可以在领域驱动设计中表达模型,可以在模块中进行分组:
值对象是传达含义和功能的简单对象。这些对象描述了事物,但没有特殊的标识。
值对象通常作为参数在对象之间的消息中传递。它们通常是为操作临时创建的,然后丢弃。
最佳做法:
```javascriot
// An example of a Value Object
class Price {
amountInEuroCents: number
inEuros(): number {
// implementation
}
// other logic
}
```
实体是主要由其身份而不是特定属性定义的对象。实体的身份贯穿时间,可能还有不同的表示形式。实体也称为“参考对象”。
```
// An example of an Entity
class ShoppingCart {
id: ShoppingCartId;
items: Array<ShoppingCartItem>
addItem(item: ShoppingCartItem) {
// implementation
}
}
```
在某些情况下,一个服务简单来说就是包括了若干操作,从概念上服务不从属于任何对象。我们可以根据问题的实际情况,在模型中明确包含服务,而不是死搬硬套。
// For e-commerce, an example of a domain service would be the checkout step function checkout(shoppingCart: ShoppingCart, ...) { // do service stuff }
领域分层中的模块应作为模型的有意义的一部分出现,以更大的规模来描述领域。
无论是代码方面还是概念方面,模块之间的耦合度应低,模块之间应具有较高的凝聚力。
最佳做法
目的是防止模型因管理生命周期的复杂性而陷入困境。为此,我们将生命周期的管理(即持久性对象)与业务逻辑分开。
最重要的概念是聚合和存储库。注意:聚合始终与一个且仅一个存储库关联。
聚集是一组实体和值对象,它们在域上有意义,并且被检索并持久保存在一起。
聚合为模型所包含的对象设置边界并提供清晰的所有权,使模型结构化。
最佳做法
```
// Our ShoppingCart example of an Entity is actually also an aggregate.
class ShoppingCart {
id: ShoppingCartId;
items: Array<ShoppingCartItem>
addItem(item: ShoppingCartItem) {
// implementation
}
}
```
尽管聚合通过定义所有权和边界来帮助管理生命周期,但它们对基础结构的细节一无所知,并且属于领域层。
存储库提供了一个接口来检索和保持聚合。他们从域中隐藏数据库详细信息。存储库管理生命周期的中期和中期。存储库接口在域层中声明,但是存储库本身在基础结构层中实现。这使得在存储库的不同实现之间轻松切换而不会影响任何业务代码(例如,从SQL到No-SQL存储,或编写内存实现以进行更快的测试)。
```
// Repository interface example
interface ShoppingCartRepository {
findById(shoppingCartId: ShoppingCartId): Promise<ShoppingCart>;
store(shoppingCart: ShoppingCart): Promise<void>;
}
```
```
class InMemoryShoppingCartRepository implements ShoppingCartRepository {
findById(shoppingCartId: ShoppingCartId) {
// implementation
}
store(ShoppingCart: ShoppingCart): Promise<void> {
// implementation
}
}
```
不要忘记从一开始就回顾一下代码示例,以了解所有代码示例如何结合到实际代码中。
实际上还不止如此,所以我真的建议你在正式实施前读一本书或两本书。到目前为止,这是我们迄今为止在Inato中使用过的最佳资源,可帮助我们进行域驱动设计: