在需求分析完成之后,正式开始编码之前,还有一件非常重要的事情要做,那就是架构的设计。
由于开始实习了,毕设剩余时间也不多了,加之个人对DDD理解有限无法快速上手,因此会先采用传统做法,留待日后填坑
传统做法
一般情况下,当拿到一个项目并且分析完需求的时候,我们最先做的,一定是思考的是要完成需求需要哪些数据以及如何存储,说白了其实就是如何设计数据库,如何设计数据库的实体,我们一般会采用ER图进行建模,然后努力使得数据库的设计符合第三范式(3NF),然后考虑各种约束,最后得到一个符合我们项目需求的数据库模式。再然后我们会创建一个项目,例如Spring项目、ASP.Net项目等,接着我们会创建DAO层、Service层、Controller层等等,然后我们将我们的数据库模式映射到实体类,接着在Service层写具体的业务,最后在Controller层调用Service,一个项目就完成了。
一切看起来非常完美,但是当业务需求开始变得复杂之后,你会发现修改旧功能会变得越来越复杂,一个最直观的感受就是,对某个实体的业务逻辑会分散在系统的各个角落,例如在购物车中修改订单也可以在订单管理中修改订单(当然是未付款的),好的设计会尽量保证修改订单功能最终落在同一个方法的头上,但是如果一个项目新人不知道这样的设计,那么就可能会产生冗余的、行为不太一致的订单修改功能。当然,由于我本人对这方面并非很有经验,这也只是我认为的一个潜在的风险。
以上这种做法,通常在使用Java的Spring框架编程的项目中比较常见,这也是Spring框架推荐的做法。有一个专门的名称来描述它,即“贫血模型”。贫血模型将数据和逻辑分开,数据即实体类,不承担任何的业务逻辑,业务逻辑则由service层中的各种方法对实体类进行加工处理来完成。我们一开始学习Java时,灌输最多的就是面向对象编程,一个类有自己的数据——属性,还有一些操作——方法,二者描述了类的特性与功能,然后我们学习如何抽象现实世界的事务,我们学习如何在抽象的基础上进一步抽象,我们还学习如何通过UML工具描述我们抽象出的类的关系……因此在我看来,贫血模型实质上是面向过程编程,毕竟,数据和操作已经完全分开了不是?这样,我们所学习到的面向对象知识就无用武之地了,我们只是在面向对象编写的框架之上在面向过程编码,给人的感觉非常奇怪。
领域驱动设计(DDD)
庆幸的是,传统做法并非得到了所有人的认可,领域驱动设计就是一种通过面向对象编程来解决前述风险的方法论。这种方法是2004年Eric Evans 在《Domain-Driven Design –Tackling Complexity in the Heart of Software》一书中提出的,领域驱动设计这个话题太过复杂,我本人也是刚刚接触到这一理念,因此我不打算介绍这种设计方法,如果感兴趣可以自行阅读学习。网络上也有一些非常好的实践指导,例如《后端开发实践系列——领域驱动设计(DDD)编码实践 – Thoughtworks洞见》、《领域驱动设计在互联网业务开发中的实践 – 美团技术团队》、【领域驱动设计】DDD入门三板斧之一:Domain Primitive_哔哩哔哩。
项目架构
基于以上原因和对领域驱动设计的学习,我打算采用领域驱动设计来组织项目的架构,需要说明的是,由于我本人刚刚接触这方面知识,可能无法完全正确应用领域驱动设计,如果你发现了我的实践有错误欢迎指出,另外,由于需要赶项目进度,因此我不会非常详细地、完全地应用领域驱动设计的最佳实践,也暂时不会详细认真地做很有难度的上下文分解等工作,如果未来时间充足,那么我会考虑重新设计并进行重构。
还需要注意的是,我并非DDD的拥趸,应用DDD到我的项目上是为了能够让我更好进行实践和学习,同时也希望可以在CRUD之外学到一些更加深刻的理论,提高面对复杂项目时的处理能力和水平。
首先,本项目将采用六边形架构设计,六边形架构的示意图如下:
![六边形架构图](https://hhao.wang/wp-content/uploads/2022/01/1227832-20201220180315345-1541638848-1-1024x884.png)
在本项目中,按照我的理解,我将其分为如下结构:
- Controller:控制器仍然负责请求的处理与响应,这一点和传统的控制器一致,但是它属于Adapter,在调用ApplicationService时,会将外部请求参数处理为信息传输对象(DTO)作为入参传递
- ApplicationService:应用服务起着调用Domain完成业务的作用,它只是很薄的一层,负责控制Domain中聚合根的初始化调用、功能调用、持久化调用,正如我们在上Java课程时的main函数一样——实例化我们编写完成的类、调用对象功能、打扫现场等
- Domain:这一层主要是具体的业务实现的地方,它会按照划分好的领域模型设计并实现各种类,在类中承载着具体的业务细节,主体是各种聚合根,辅以一些值对象(VO)、实体对象(Entity),此外还包括一些辅助类例如异常等
- Repository:也是一种Adapter,主要用于数据的持久化存储。这一部分负责对接所使用到的外部依赖,例如数据库、缓存等,常见包括有数据对象(PO)以用于ORM框架,当然也可以不使用框架之间编写SQL语句实现数据持久化等,当外部依赖变更时(例如从MySQL转为Oracle或是从MyBatis改为Hibernate)主要修改的地方就是这里
- Adapter:这一层实际上是一个较为宽泛的概念,包括了许多东西,我认为其实质作用是将内部核心(也就是那些聚合根或者说类)与外部隔离开,这样无论我们是采用什么技术框架或是什么工具,都与我们的业务核心无关。
在这种设计下,最极端最理想的状态是,你既可以通过适配外部内容将Domain中的内容通过控制台运行(就像我们写Hello World一样),又可以通过适配外部内容使其在Spring框架上运行,Domain中的内容完全不需要变更。那么假如再极端一些,你甚至可以将Domain从Java项目变更为C#项目(开个玩笑,实际上语言差异不太可能直接转换)。这听起来非常非常酷,我们也从“CRUD导向编程”重新回到了OOP,我们所学习到的OOP终于有了用武之地。
最后,在项目实践中,我计划将Controller单独集中放置,ApplicationService和Domain按领域划分集中放置。其实很多实践会将三者按照领域划分集中放置,我更希望能够让领域内部更加纯粹,作为外部的控制器尽量不掺和在内部。这样做也是有一些问题的,当我们要修改业务时,必须要同时修改不同层次,分散放置会导致很容易忘记修改外部或者内部的代码,不过好在这种情况一般都会在测试阶段发现,影响不是很大。