写点什么

DDD 与应用架构

作者:胖子笑西风
  • 2022-11-18
    上海
  • 本文字数:9158 字

    阅读完需:约 30 分钟

DDD与应用架构

架构

架构这一词,英文单词为 Architecture。Architecture 在计算机体系中解释为架构,在建筑领域中理解为结构。因此,理解计算机的架构,我们可以从建筑学中的结构出发。



上图是一张房子的结构图。从图中我们可以看到,房子的结构主要是说明这个房子有什么东西,这些东西应该在什么位置,怎么样整合起来整体更好看,更耐用。同样,我认为架构跟结构是一个道理的。网上大家对架构是这么定义的:架构是“以组件、组件之间的关系、组件与环境之间的关系为内容的某一系统的基本组织结构,以及指导上述内容设计与演化的原则。说白了就是说明你所描述的东西有什么组成,以什么方式组成。

业务架构

我们通常所说的业务架构。就是描绘一个业务有哪些东西,这些东西怎么排列,以什么样的关系组成起来。


组织架构

组织架构,就是说明组织里面有哪些部门,部门的层级关系是什么样的。或者是组织间人与人之间的关系是什么样的。


应用架构

应用架构,其实就是说明一个应用里面有哪些部分,各个部分如何排列,如何依赖。比如我们最经典的三层架构


框架

框架,顾名思义,我觉得就是把一件事框在一定范围内,按照一定规范行事。框架是一种约束。大家都遵守一种规范,在一个约束下干活。这样才能事半功倍。框架,在是一种约束的情况下,也是一种工具。所谓工具,就是生产力。框架会提供一些方法、方式使你的生产力太幅度提高。从而使项目更快的完成。比如 spring 框架 spring 框架遵守了 mvc 的规范,让大家在这个规范约束下统一的行动。比如数据库接为 Controller,业务处理为 Service。大家统一行动,大大减少了沟通成本。同时,spring 的 AOP,IOC 大大提高了生产力,开发者只要会实现 AOP 或者 IOC,会编写简单的业务代码,就可以实现一套可运行的程序。

服务应用架构的演进

我们知道,当一个项目或者一件事都只有一个人在做的时候,再混乱都能整出一些条理出来。但多人开发一个项目或者是多人做一件事的时候,混乱就开始了,人越多,就越混乱,通常我们叫它熵增。而如之前所说,架构就是要确定一件事情里面有哪些东西,这些东西该如何整合来解决某个问题。说白了就是提出某种规范,大家都遵守这种规范,事情就会变得井然有序。但我们知道软件开发与建筑不一样的是,建筑不管前期设计过多少遍结构,只要定下来,建造完成之后基本上就不会动了。而软件开发在需求确定之后,随着业务变化,还是会不断的修改,不断的迭代。



我认为,应用架构的产生是为了推动软件开发的,那它最起码要解决两个问题:1.怎么确定规范使一团杂乱的事情变得有序。2.当修改产生的时候,怎么样才能花最小的代价应付改动。

J2EE

早的时候,我们开发一套 Java Web 程序,直接创建一个项目。一个 servlet 的 service 方法就把所有的逻辑处理了。service 方法好几千行,各种逻辑夹杂在其中。开发人员要想修改一个逻辑,往往要找半天。改完了还不知道修改的逻辑会不会影响到其它地方。测试也很麻烦,修改过一处地方,往往就要将所有功能重新测试一遍。


public class OmnipotentServlet extends HttpServlet {
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { /** * 此处省略几千行代码 */ }}
复制代码


是不是心很累!是的,所有软件开发人员都觉得很累。如我们之前所说,我们要定义一种架构,规范,使这几千行变得有序,且能最小化改动。那规范怎么建立呢。分层是一种很好的方法。Frank Buschmann 等人著的《面向模式的软件架构》第一卷《模式系统》第二章中提出了层的概念,该模式参考了 ISO 对 TCP/IP 协议的分层。书中是这样描述层的:


层(layer)体系结构模式有助于构建这样的应用:它能被分解成子任务组,其中每个子任务组处于一个特定的抽象层次上。


三层架构

在计算机本身的架构中或者网络的架构中,可以看到:到处都有分层的例子。当用分层的观点来考虑系统时,可以将各个子系统想像成按照“多层蛋糕”的形式来组织,每一层都依托在其下层之上。在这种组织方式下,上层使用了下层定义的各种服务,而下层对上层一无所知。另外,每一层对自己的上层隐藏其下层的细节。比如 ISO/OSI 七层协议模型。



我们看 7 层协议,从上到下,越上层越靠近用户,越下层越靠近设备。我们知道一个网站功能大概可以概括成这样,用户浏览页面->点击页面上的某个功能->页面上给于反馈。对于开发来说,就是接收到一个请求->处理事件->存储信息->查询信息->返回请求。Martin Fowler 在企业应用架构模式一书中提出了应用开发架构基本上可以分为三层。


  • 处理请求与返回请求都是最靠近用户那一层的两个子任务组,它们两个可以放到一层。

  • 处理事件是一个子任务组,可以放到一层。

  • 存储信息与查询信息都是最接近于存储底层的,可以放到一层。



我认为,三层架构在水平层面对一个服务的功能进行了比较粗的颗粒度的区分。更初的两层架构往往是把处理请求与返回请求放到一层,认为与用户交互相关。处理事件与存储信息、查询信息放到一层,认为是机器处理。我们来看三层架构这样区分之后,带来的效果。

三层架构的效果

用户展示层

@Controllerpublic class FinanceInfoController {        private OrganizationService organizationService;        private FinanceService financeService;        @RequestMapping("/queryFinanceInfo.json")    public FinanceInfo queryFinanceInfo(@RequestParam String orgNo){        Organization org = organizationService.get(orgNo);        Finance finance = financeService.get(orgNo);        FinanceInfo financeInfo = new FinanceInfo(org, finance);        return financeInfo;    }    }
复制代码


我们可以看到,展示层专注于处理用户的请求。如果这时候页面发生了改变,我们可以将返回的 FinanceInfo 进行修改,而 Service 的业务处理可以不做修改。

业务逻辑层。

我们可以看业务按一定规则分层几个 Service,一个业务逻辑或以分散到各个 Service 中,如果要修改某个业务,我们可以只修改到对应的 Service.

数据访问层。

三层结构相对于两层结构,是把业务处理与数据访问分隔开来。个人认为早些时候,分开的价值不大。一是早些时候数据库比较固定,一个公司一旦使用了某个数据库,基本上就不会再改变了。而是原来的软件设计是从设计往往是先设计表结构,然后基于表结构来进行开发。数据库的结构变动往往伴随着业务逻辑的变动。之所以分开,我觉得更多是人员安排上的需求。你可以让一个不了解业务的但精通 SQL 的开发来专注于写 DAO 层,而让精通于业务的开通更聚焦于写业务逻辑。然而,从毕业到现在,全栈工程师见得多了,但专职于写 DAO 的工程师还真没见过。

三层架构的不足

三层架构已经流行了很长一段时间,虽然设计的初衷是美好的,所有的业务逻辑都有不同的 Service 来实现,Controller 只负责聚合。但是在大多数情况下,开发是不怎么懂业务的,他们每天做的事情,是由产品把 prd 出出来,然后将 prd 翻译成代码。而这就造成了很多 Service 的混乱。混乱的情况大概会有这么几种

一个 Service 打天下

我上面写到的 FinanceService 一样,看名字,就是跟财务相关的所有业务都在这个 Service 中实现。


@Servicepublic class FinanceService {
/** * 获取收入 * @return */ public Map<String,Object> getRevenue();
/** * 获取成本项 * @param bizNo * @return */ public List<Cost> queryCost(List<String> bizNo);
/** * 更新成本项 * @param bizNo * @param cost * @return */ public Cost updateCost(String bizNo, Cost cost);
/** * 省略其余几十个方法 */
}
复制代码

多个 Service 代表同一个意思

要么就是每次产品迭代的时候,根据当时的需求来新建一个 Service,比如 FinanceCostServiceFinanceCostProfitServiceFinanceCostAnaliticService 这三个 Service 分别是三个不同的开发写的,第一个接到的需求是要处理公司的成本。第二个接到的需求是需要给老板一个可以看到成本项与利润的报表,开发一看,成本数据库表知道在哪了,简单,建一个 Service,写一个 Mapper,返回,搞定。第三个开发接到的是运营的需求,需要对成本项按照一定规则进行分析,得到分析结果。开发一看,成本数据库表知道在哪了,简单,建一个 Service,写一个 Mapper,返回,搞定。

多个 Service 都在更新同一个对象

还是拿上面一个例子如有两个 Service:FinancePoService:财务采购单据 Service


@Servicepublic class FinancePoService {        @Resource    private PurchaseOrderMapper purchaseOrderMapper;        public void updatePurchaseOrderStatus(String poNo,int status){        purchaseOrderMapper.updateSatus(poNo, status);    }    }
复制代码


PoCustomerService:采购单据客服 Service


@Servicepublic class PoCustomerService {        @Resource    private PurchaseOrderMapper purchaseOrderMapper;        public void updatePurchaseOrderStatusByPoNo(String poNo,int status){        purchaseOrderMapper.updateSatus(poNo, status);    }    }
复制代码


两个 Service 都在更新一个采购单据状态。这时候,我们如果发现一个采购单据发生问题,我们就很难发现问题究竟出现在哪里。更有甚者,分别有几个不同的 controller 或者 service 分别调用这两个不同的 service.这就更混乱了。

三层架构往后发展

从上面几个例子我们可以看到,三层架构解决了一部分问题,但究其根本,Service 还是还过于庞大了,而且没有规范。你可以说是因为写代码的人水平不高,才造成的上面几个问题。但是在软件开发过程中,总是会有各式各样的开发人员。你不可能指望每个人的水平都很高,即使水平都很高,每个人的想法也会不一样。既然原因是 Service 过于庞大,那我们应该想一下怎么样把 Service 拆分。刚才说到,很多开发人员不懂业务,所以不知道怎么设计 Service 才是合理的。这就造成了 Service 中核心逻辑与其它业务逻辑混合在一块,纠缠在一起,如果我们可以把 Service 中的核心逻辑都摘出来,单独放在一个地方,整个架构如下图所示



可以看到,不管业务逻辑怎么改变,我们的核心逻辑是稳定的。只要不涉及到核心逻辑的改变,那么我们就可以大致的认为,当前业务是安全的。那么怎么把核心逻辑提取出来了,这就需要对业务有一定的了解了。我们接下的介绍的 DDD 就与此有关。

DDD(领域驱动)

领域驱动设计(Domain Driven Design, DDD)是由 Eric Evans 提出的软件系统设计的面象对象建模方式。它不是一种架构,也不是一个框架。我认为它是一种思维方式,一种思想。帮助开发人员怎么样才能更好的处理好复杂的软件项目开发。



前文我们说过,三层架构的主要问题在于,开发一般是不懂业务的,拿一个需求来做一个需求,多次迭代之后就容易造成混乱。在领域设计就是建立了一种思维方式,让我们了解怎么样才能更好的把核心逻辑提炼出来。这里不太过多的介绍 DDD,后面再详细说明。我们理解简单理解 DDD 的主题思想是需要开发团队与某个领域方面的专家进入业务上的沟通,并互相理解对方的语言,开发团队要了解领域内的术语,领域专家要大概懂开发团队是怎么画图的。互相沟通,进而不断迭代测试,实现核心逻辑。

领域驱动经典四层分层架构

Eric 在领域驱动设计一书中在提出了一个四层分层架构。



书中对这四层的职责是这样描述的



个人认为,Eric 提出的四层架构,本质上来说就是利用 DDD 思想将三层架构中的核心逻辑抽来,形成 domain Layer。其它的与三层架构的驱别不大。

六边形架构-Hexagonal Archetecture

这里把六边形架构放在 DDD 目录下,并不是因为六边形架构跟 DDD 绑定起来,而是 DDD 结合六边形架构,可以达到更好的效果。六边形架构是由 Alistair Cockburn 在 2005 年提出的,这里我们还是借助于三层架构与四层架构来说明一下。三层架构与四层架构理论上是可以把表示层与业务层分开的,但也会出现一些开发是会把一些业务代码直接写在 controller 层,跳过业务层或者领域层。或者说开发在原本应该在领域层或者业务层实现代码写到数据源层。这就又造成了一种混乱,业务逻辑与表现层或者数据源层紧紧耦合在一起,与架构设计的初衷相违背了。大多数的第一反应还是说这是开发的水平问题。但我觉得这是一种开发方式的问题。还是那句话,开发人员水平有好有差,架构就是要说明出一个应用中存在哪些东西,怎么组合,同时,也要规范不同的做事方式来达到架构设计的初衷。我们先来看一下六边形架构。



假设我们现在把开发方式改成不需要管外部输出什么了,也不用管要把数据存成什么格式。只专注于业务逻辑的开发。我们开发一个应用,需要什么的数据进来,就规范出一个对象参数。我们需要什么数据出去,就规范出一个返回对象。这样,我们不受表现层与数据源层的影响了,所有实现在应用中的都是业务逻辑。最后,我们把应用与表示层、数据源层对接。有几个客户端,我们用几个适配器将客户端的数据转换成应用的数据格式。有几中数据源层,我们用几个适配器将应用的数据格式转换数据源所需要的格式。我理解这就是六边形架构的思想。没有表示层与数据源层开发听起来是不是与 DDD 有点像,DDD 的核心也是与领域专家交流,形成一个的核心的业务逻辑。



我们再回到六边形架构,六边形架构将三层架构中的表示层与数据源层看成是同一种。六边形架构不是跟有几条边没有关系,你愿意的话,也可以画一个八边形叫八边形架构。六边形讲的是一个对称的概念。分层有高低之分,左右代表在同一层,没有高低之分。这里的对称说的是不管是输入还是输出,在六边形架构里是同一个概念。三层架构与六边形架构主要的区别,还在于说表示层与数据源层是不是对称、平等的问题。Martin Fowler 在企业应用架构模式一书的 2010 版中对该区别作了说明:


“然而,我认为这种非对称性是有益的。因为,为别人提供服务的接口与使用别人服务的接口存在较大的差别,需要明确区分。这就是表现层和数据源层相对于核心的本质差别。表现层是系统对外提供服务的外部接口,不管外面是复杂的人类还是一个简单的远端程序。数据源层是系统使用外部服务的接口。这样区分的好处是:客户的不同将改变你对服务的看法。”


Martin 所说的客户的不同将改变你对服务的看法,我的理解是,我们一般会在数据源层进行一些事务处理、连接池处理,在对外提供服务的外部接口一般不存这些问题。所以分开更合适。我们再来看六边形架构,个人认为只在应用程序产生的结果存储起来(这里的存储是在应用层),而通过不同的适配器再传导出去,这样的应用一般不需要数据强一致性,那么六边形架构更合适,因为这时的输入输出并没有什么更多的区别。如果数据需要实时影响到结果,如调用接口进行数据存储并确认,那么,三层架构更为合适。

洋葱架构-Onion Architecture


洋葱架构是 Jeffrey Palermo 在 08 年提出来的,与六边形架构有着大致相同的思路。他们的区别是,一个是圆形,一个是六边形,都很对称,哈哈。。。,我们看到,不管是圆还是六边形,封闭的架构图一般都意味着外层把里层包裹起来,只能是外层依赖于里层,里层不能依赖于外层。六边形架构提出的时候并没有考虑到领域驱动,洋葱相比于六边形架构,实现了更多 Eric 领域驱动的内容,即最内层不只是一个 Application 了,而是分的更细。所以个人认为洋葱架构是六边形架构与领域四层架构的结合体。这里的 Domain Service 与 Domain Model 这里就先不过多说明了,后面我们讲 DDD 的时候再展开。

DCI 架构

小的时候看灌篮高手,我是一个天才是樱木的口头禅。当他在与晴子练球时说出我是一个天才时,晴子的反应是是的,樱木真是一个天才。而他在球场上比赛时说出我是一个天才时,流川往往会跳出来说:白痴。 在现实世界中,不同的场景,同一件事被理解的角度不一样。我们可以理解为,世界是由不同的场景构造出来的,一个模型只有在一个场景下才会有意义。之前我们说过,我们在三层架构开的时候,常常会因为 Service 中的方法太多,造成了程序的混乱。那么,创建好一个 DDD 模型之后,就万无一矢么。举一个例子。我们抽象出来一个人的模型。那么人的固定行为就表现为一个个方法。


一个人所能做的事情是很多的,当你要把人的所有现实行为都反映到一个对象中去后,你会发现,不知不觉中,你又创建了一个上帝类。Jon Kern 认为,“不要试着把对象在现实世界中可以想象到的行为都实现到设计中去。相反,只需要让对象能够合适于应用系统即可。对象能做的,所知的最好是一点不多一点不少。”对此,我们在 DDD 中通常的做法是在 DomainService 中描绘出对象在现实数据中可以提供的服务。


public class PeopleService {
private PeopleFactory peopleFactory;
public void teach(){ People people = peopleFactory.create(9527); people.talk(); people.write(); }
public void date(){ People bohu = peopleFactory.create(9527); People qiuxiang = peopleFactory.create(1); bohu.talk(); qiuxiang.talk(); }
/** * 此处省略十几个方法 */
}
复制代码


这样,PeopleService 将一个人的具体社交行为体现了出来,代表当 ApplicationService 中接收到一个需求时,我们通过 PeopleService 来反映,通过组合 People 的本能行为方法来实现。看起来很完美!但还是那个问题,这里的 PeopleService 没有一个具体的约束。比如苏炳添,他是一个老师,他教学生跑步的理论课,用讲跟画图就行了。同时,他也是一个教练,他要给学生示范跑步,就得真跑起来。那么我们在 PeopleService 里写法就会变成


public class PeopleService {
private PeopleFactory peopleFactory;
public void teachRunTheory(){ People people = peopleFactory.create(666); people.talk(); people.draw(); }
public void teachRunAction(){ People people = peopleFactory.create(666); people.run(); people.talk(); }
public void teach(){ People people = peopleFactory.create(9527); people.talk(); people.write(); }
public void date(){ People bohu = peopleFactory.create(9527); People qiuxiang = peopleFactory.create(1); bohu.talk(); qiuxiang.talk(); }
/** * 此处省略几十个方法 */
}
复制代码


想像一下,当有几十几百个苏炳添这样的对象出现之后,PeopleService。。。又乱了。那我们怎么整理这个乱象。软件设计终究是由人做出的决策,在提出一种设计方法时,若能从人的思维模式着手,就容易找到现实世界与模型世界的结合点。结合上述所说的,我们知道 PeopleService 缺少一种规范,我们试着来规范一下。像柏拉图的人生终极问题,"我是谁,从哪来,到哪去"。我们描述一个小场景可以用,"做什么,怎么做,具体的步骤"。



回到刚刚的 PeopleService,我们发现,PeopleService 里的行为可以抽象出来一个个角色。比如体育课老师是一种角色,教练员是一种角色。代码变成这样


public interface PhysicalTeacher {
public void teach();}
复制代码


public interface Coach {
public void teachRun();
}
复制代码


public class People implements PhysicalTeacher,Coach{    @Override    public void teach() {
}
@Override public void teachRun() {
}}
复制代码


我们看到,由 Role 代替掉了 DomainService,职责更清晰。这样,People 的方法更多了,但是具体相应职责的角色,来进行整体的调度,隐藏掉了 People 直接出去的混乱感,不在 Role 中需要的有 People 中不必呈现。整体上来看,Role 的引入,把 DomainService 这一层的混乱给整理了。我们之前说,事件的发生都是在特定的场景下的,比如苏炳添是在训练场里教导学生,在教室里面讲课。同一时间,苏炳添不会既在教室里讲课也在训练场里。这里,我们再引入一个场景。


public class TrainContext {        private Coach coach;        private Student student;        private PeopleFactory peopleFactory;
public void trainTeach(){ coach = peopleFactory.create(666); student = peopleFactory.create(333); coach.teachRun(); student.run(); }
}
复制代码


以上就是 DCI 架构要表达的内容。DCI 模式认为,在现实世界到对象世界的映射中,构成元素只有三个:数据(Data)、上下文(Context)和交互(Interaction)。



关于 DCI 的理论与场景驱动我们另外开一个篇幅来说明,这里就不具全的讨论了。

整洁架构(Clean Architecture)


整洁架构是由 Robert C. Martin 在 2012 年的提出的,把前面所说的六边形架构、DCI 架构、洋葱架构做了一个总结,Martin 认为这些架构都具有以下几个特点。他们都有同样的目标,隔离关注点。他们都通过将软件分层来达到隔离。每个都至少有一层业务规则,另一层作为接口。每个这些架构产出的系统都是:


  1. 独立的框架。架构不依赖一些存在类库的特性。这样你可以像工具一样使用这种框架,而不需要让你的系统受到它的约束条件。

  2. 可测试。业务规则可以脱离 UI,数据库,web 服务器或其他外部元素进行测试。

  3. 独立的 UI。UI 可以很容易的更换,系统的其他部分不需要变更。例如,Web UI 可以被换成控制台 UI,不需要变更业务规则。

  4. 独立的数据库。你可以交换 Oracle 或 SQL Server,用于 Mongo,BigTable,CouchDB 或其他的东西。你的业务规则不与数据库绑定。

  5. 独立的外部代理。实际你的业务规则并不知道关于外部世界的任何事情。


这里主要是想说明两个点,Martin 这里画的 4 个圈,不是代表整洁架构就分为四层,这里的圈只是代表外层依赖于内层。比如你可以在 Use Case 层与 Entities 层中间再加一层,DomanService 层。你要是觉得还可以再拆分。

总结

应用架构的存在,是了使一个应用的代码从混乱变得有序。尤其在多人参与开发的情况下,人数越多,熵越大。整洁有序的架构可以缓解熵增,但阻止不了,绝大部分的软件开发经过一段时间之后,都很难保持整洁。以上介绍了几个架构,提供了一些拆分应用的思路,但应用架构不是固定的,要根据自己业务,选择适合自己的最好。不要太拘泥于多一层少一层,其实并没有啥关系,只要你能通过某样形式组织好你的业务代码,使其可以最小化修改,测试,我觉得都是一个好的架构。另外,国内有很多人在这些架构之后根据自己做的业务情况发展出了很多架构,我们看这些架构的时候,重要的是看他们应用于什么场景,能不能帮助很好的管理好你的应用。不要把一些高大上的图或者名字迷惑,本质上,应用架构没那么复杂。你要愿意的话,也可以画一个圆形两层架构,西瓜架构,外一层,里一层。。。


原文:

胖子笑西风-DDD与应用架构

微信公共号:

胖子笑西风-DDD与应用架构


发布于: 2022-11-18阅读数: 51
用户头像

还未添加个人签名 2021-03-31 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
不是讲DDD架构吗?为什么我没听明白,感觉讲了好多其他内容(无恶意,纯想了解DDD)
2022-11-18 23:52 · 江西
回复
没有更多了
DDD与应用架构_架构_胖子笑西风_InfoQ写作社区