软件架构思想系列分层架构
Posted 禅与计算机程序设计艺术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件架构思想系列分层架构相关的知识,希望对你有一定的参考价值。
- 分层架构 -
今天谈下架构设计中的分层思想和分层模型。
架构思维概述
对于架构思维本身仍然是类似系统思维,结构化思维,编程思维等诸多思维模式的一个合集。
由于架构的核心作用是在业务现实世界和抽象的IT实现之间建立起一道桥梁,因此架构思维最核心的就是要:
理解到业务驱动技术,技术为最终的业务服务。
要真正通过架构设计来完成业务和技术,需求和实现,软件和硬件,静态和动态,成本和收益等多方面的平衡。
架构设计中有两个重点:一个是分解,一个是集成。
分解是最基础的,架构的重点就是:
对复杂问题进行分而治之,同时保证分解后的各个部分还能够高内聚,松耦合,最终又集成为一个完整的整体。
分解核心是定义问题,因此架构首先仍然需要理解清楚需求。
集成是配合分解完成的动作,最终分解完成的各个组件或子系统,通过合适的接口设计,最终还能够集成为一个完整的整体,分解仅仅是加速开发和降低问题复杂度,如果分解后的内容无法集成在一起,那么分解就没有任何意义。
“分解+集成” 可以理解为架构最核心的思考方式和方法。
在分解完成后,一个大的系统已经拆分为了诸多的小模块,或者一个小模块实现本身又分为了多个步骤阶段。那么零散的节点必须向上汇集和归纳,形成一个完整的架构。
而这个架构的形成要给关键就是要又分层思维。架构分层是谈架构绝对绕不开的一个点,通过架构分层可以更好地全面理解业务系统或功能实现。
分层架构的一个重要原则是每层只能与位于其下方的层发生耦合。分层架构可以简单分为两种,即严格分层架构和松散分层架构。在严格分层架构中,某层只能与位于其直接下方的层发生耦合,而在松散分层架构中,则允许某层与它的任意下方层发生耦合。
分层架构的好处是显而易见的。首先,由于层间松散的耦合关系,使得我们可以专注于本层的设计,而不必关心其他层的设计,也不必担心自己的设计会影响其它层,对提高软件质量大有裨益。其次,分层架构使得程序结构清晰,升级和维护都变得十分容易,更改某层的具体实现代码,只要本层的接口保持稳定,其他层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。
要保持程序分层架构的优点,就必须坚持层间的松散耦合关系。设计程序时,应先划分出可能的层次,以及此层次提供的接口和需要的接口。
设计某层时,应尽量保持层间的隔离,仅使用下层提供的接口。
关于分层架构的优点,Martin Fowler在《Patterns of Enterprise Application Architecture》一书中给出了答案:
1. 开发人员可以只关注整个结构中的某一层。
2. 可以很容易的用新的实现来替换原有层次的实现。
3. 可以降低层与层之间的依赖。
4. 有利于标准化。
5. 利于各层逻辑的复用。
“金无足赤,人无完人”,分层架构也不可避免具有一些缺陷:
1. 降低了系统的性能。这是显然的,因为增加了中间层,不过可以通过缓存机制来改善。
2. 可能会导致级联的修改。这种修改尤其体现在自上而下的方向,不过可以通过依赖倒置来改善。
在每个BC中为了凸显领域模型,DDD中提出了分层架构模式。
- 模式一:四层架构 -
Eric Evans在《领域驱动设计-软件核心复杂性应对之道》这本书中提出了传统的四层架构模式,如下图所示:
1. User Interface为用户界面层(或表示层),负责向用户显示信息和解释用户命令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
2. Application为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
3. Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型位于这一层。
4. Infrastructure层为基础实施层,向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。传统的四层架构都是 限定型松散分层架构 ,即Infrastructure层的任意上层都可以访问该层(“L”型),而其它层遵守 严格分层架构
笔者在四层架构模式的实践中,对于分层的本地化定义主要为:
1. User Interface层主要是Restful消息处理,配置文件解析,等等。
2. Application层主要是多进程管理及调度,多线程管理及调度,多协程调度和状态机管理,等等。
3. Domain层主要是领域模型的实现,包括领域对象的确立,这些对象的生命周期管理及关系,领域服务的定义,领域事件的发布,等等。
4. Infrastructure层主要是业务平台,编程框架,第三方库的封装,基础算法,等等。
说明:严格意义上来说,User Interface指的是用户界面,Restful消息和配置文件解析等处理应该放在Application层,User Interface层没有的话就空缺。但User Interface也可以理解为用户接口,所以将Restful消息和配置文件解析等处理放在User Interface层也行。
层之间的协作
在我们固有的认识中,分层架构的依赖都是自顶向下传递的,这也符合大多数人对分层的认知模型。从抽象层次看,层次越处于下端,就会变得越通用越公共,与具体的业务隔离得越远。出于重用的考虑,这些通用和公共的功能往往会被单独剥离出来形成平台或框架,在系统边界内的低层,除了面向高层提供足够的实现外,就都成了平台或框架的调用者。换言之,越是通用的层,越有可能与外部平台或框架形成强依赖。若依赖的传递方向仍然采用自顶向下,就会导致系统的业务对象也随之依赖于外部平台或框架。
依赖倒置原则(Dependency Inversion Principle,DIP)提出了对这种自顶向下依赖的挑战,它要求“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”这个原则正本清源,给了我们当头棒喝——谁规定在分层架构中,依赖就一定要沿着自顶向下的方向传递?我们常常理解依赖,是因为被依赖方需要为依赖方(调用方)提供功能支撑,这是从功能重用的角度来考虑的。但我们不能忽略变化对系统产生的影响!与建造房屋一样,我们自然希望分层的模块“构建”在稳定的模块之上。谁更稳定?抽象更稳定。因此,依赖倒置原则隐含的本质是:我们要依赖不变或稳定的元素(类、模块或层)。也就是该原则的第二句话:抽象不应该依赖于细节,细节应该依赖于抽象。
这一原则实际是“面向接口设计”原则的体现,即“针对接口编程,而不是针对实现编程”。高层模块对低层模块的实现是一无所知的,带来的好处是:
低层模块的细节实现可以独立变化,避免变化对高层模块产生污染
在编译时,高层模块可以独立于低层模块单独存在
对于高层模块而言,低层模块的实现是可替换的
倘若高层依赖于低层的抽象,必然会面对一个问题:如何将具体的实现传递给高层的类?由于在高层通过接口隔离了对具体实现的依赖,就意味着这个具体依赖被转移到了外部,究竟使用哪一种具体实现,由外部的调用者来决定。只有在运行调用者代码时,才将外面的依赖传递给高层的类。Martin Fowler 形象地将这种机制称为“依赖注入(dependency injection)”。
为了更好地解除高层对低层的依赖,我们往往需要将依赖倒置原则与依赖注入结合起来。
层之间的协作并不一定是自顶向下的传递通信,也有可能是自底向上通信,例如在 CIMS(计算机集成制造系统)中,往往会由低层的设备监测系统监测(侦听)设备状态的变化。当状态发生变化时,需要将变化的状态通知到上层的业务系统。如果说自顶向下的消息传递往往被描述为“请求(或调用)”,则自底向上的消息传递则往往被形象地称之为“通知”。倘若我们颠倒一下方向,自然也可以视为这是上层对下层的观察,故而可以运用观察者模式(Observer Pattern),在上层定义 Observer 接口,并提供 update() 方法供下层在感知状态发生变更时调用。或者,我们也可以认为这是一种回调机制。虽然本质上这并非回调,但设计原理是一样的。
如果采用了观察者模式,则与前面讲述的依赖倒置原则有差相仿佛之意,因为下层为了通知上层,需要调用上层提供的 Observer 接口。如此看来,无论是上层对下层的“请求(或调用)”,抑或下层对上层的“通知”,都颠覆了我们固有思维中那种高层依赖低层的理解。
现在,我们对分层架构有了更清醒的认识。我们必须要打破那种谈分层架构必为经典三层架构又或领域驱动设计推荐的四层架构这种固有思维,而是将分层视为关注点分离的水平抽象层次的体现。既然如此,架构的抽象层数就不是固定的,甚至每一层的名称也未必遵循固有(经典)的分层架构要求。设计系统的层需得结合该系统的具体业务场景而定。当然,我们也要认识到层次多少的利弊:过多的层会引入太多的间接而增加不必要的开支,层太少又可能导致关注点不够分离,导致系统的结构不合理。
我们还需要正视架构中各层之间的协作关系,打破高层依赖低层的固有思维,从解除耦合(或降低耦合)的角度探索层之间可能的协作关系。另外,我们还需要确定分层的架构原则(或约束),例如是否允许跨层调用,即每一层都可以使用比它低的所有层的服务,而不仅仅是相邻低层。这就是所谓的“松散分层系统(Relaxed Layered System)”。
- 模式二:五层架构 -
James O. Coplien和Trygve Reenskaug在2009年发表了一篇论文《DCI架构:面向对象编程的新构想》,标志着DCI架构模式的诞生。有趣的是James O.Coplien也是MVC架构模式的创造者,这个大叔一辈子就干了两件事,即年轻时创造了MVC和年老时创造了DCI,其他时间都在思考,让我辈望尘莫及。
面向对象编程的本意是将程序员与用户的视角统一于计算机代码之中:对提高可用性和降低程序的理解难度来说,都是一种恩赐。可是虽然对象很好地反映了结构,但在反映系统的动作方面却失败了,DCI的构想是期望反映出最终用户的认知模型中的角色以及角色之间的交互。
传统上,面向对象编程语言拿不出办法去捕捉对象之间的协作,反映不了协作中往来的算法。就像对象的实例反映出领域结构一样,对象的协作与交互同样是有结构的。协作与交互也是最终用户心智模型的组成部分,但你在代码中找不到一个内聚的表现形式去代表它们。在本质上,角色体现的是一般化的、抽象的算法。角色没有血肉,并不能做实际的事情,归根结底工作还是落在对象的头上,而对象本身还担负着体现领域模型的责任。
人们心目中对“对象”这个统一的整体却有两种不同的模型,即“系统是什么”和“系统做什么”,这就是DCI要解决的根本问题。用户认知一个个对象和它们所代表的领域,而每个对象还必须按照用户心目中的交互模型去实现一些行为,通过它在用例中所扮演的角色与其他对象联结在一起。
正因为最终用户能把两种视角合为一体,类的对象除了支持所属类的成员函数,还可以执行所扮演角色的成员函数,就好像那些函数属于对象本身一样。换句话说,我们希望把角色的逻辑注入到对象,让这些逻辑成为对象的一部分,而其地位却丝毫不弱于对象初始化时从类所得到的方法。我们在编译时就为对象安排好了扮演角色时可能需要的所有逻辑。如果我们再聪明一点,在运行时才知道了被分配的角色,然后注入刚好要用到的逻辑,也是可以做到的。
算法及角色-对象映射由Context拥有。Context“知道”在当前用例中应该找哪个对象去充当实际的演员,然后负责把对象“cast”成场景中的相应角色(cast这个词在戏剧界是选角的意思,此处的用词至少符合该词义,另一方面的用意是联想到cast在某些编程语言类型系统中的含义)。在典型的实现里,每个用例都有其对应的一个Context 对象,而用例涉及到的每个角色在对应的Context 里也都有一个标识符。Context 要做的只是将角色标识符与正确的对象绑定到一起。然后我们只要触发Context里的“开场”角色,代码就会运行下去。
于是我们有了完整的DCI架构(Data、Context和Interactive三层架构):
1. Data层描述系统有哪些领域概念及其之间的关系,该层专注于领域对象的确立和这些对象的生命周期管理及关系,让程序员站在对象的角度思考系统,从而让“系统是什么”更容易被理解。
2. Context层:是尽可能薄的一层。Context往往被实现得无状态,只是找到合适的role,让role交互起来完成业务逻辑即可。但是简单并不代表不重要,显示化context层正是为人去理解软件业务流程提供切入点和主线。
3. Interactive层主要体现在对role的建模,role是每个context中复杂的业务逻辑的真正执行者,体现“系统做什么”。role所做的是对行为进行建模,它联接了context和领域对象。由于系统的行为是复杂且多变的,role使得系统将稳定的领域模型层和多变的系统行为层进行了分离,由role专注于对系统行为进行建模。该层往往关注于系统的可扩展性,更加贴近于软件工程实践,在面向对象中更多的是以类的视角进行思考设计。
DCI目前广泛被看作是对DDD的一种发展和补充,用在基于面向对象的领域建模上。显式的对role进行建模,解决了面向对象建模中的充血模型和贫血模型之争。DCI通过显式的用role对行为进行建模,同时让role在context中可以和对应的领域对象进行绑定(cast),从而既解决了数据边界和行为边界不一致的问题,也解决了领域对象中数据和行为高内聚低耦合的问题。
面向对象建模面临的一个棘手问题是数据边界和行为边界往往不一致。遵循模块化的思想,我们通过类将行为和其紧密耦合的数据封装在一起。但是在复杂的业务场景下,行为往往跨越多个领域对象,这样的行为如果放在某一个对象中必然会导致别的对象需要向该对象暴漏其内部状态。所以面向对象发展的后来,领域建模出现两种派别之争,一种倾向于将跨越多个领域对象的行为建模在领域服务中。如果这种做法使用过度,则会导致领域对象变成只提供一堆get方法的哑对象,这种建模结果被称之为贫血模型。而另一派则坚定的认为方法应该属于领域对象,所以所有的业务行为仍然被放在领域对象中,这样导致领域对象随着支持的业务场景变多而变成上帝类,而且类内部方法的抽象层次很难一致。另外由于行为边界很难恰当,导致对象之间数据访问关系也比较复杂,这种建模结果被称之为充血模型。
关于多角色对象,举个生活中的例子:
人有多重角色,不同的角色履行的职责不同。
1. 作为父母:我们要给孩子讲故事,陪他们玩游戏,哄它们睡觉。
2. 作为子女:我们要孝敬父母,听取他们的人生建议。
3. 作为下属:我们要服从上司的工作安排,并高质量完成任务。
4. 作为上司:我们要安排下属的工作,并进行培养和激励。
5. ……
这里人(大对象)聚合了多个角色(小类),人在某种场景下,只能扮演特定的角色:
1. 在孩子面前,我们是父母。
2. 在父母面前,我们是子女。
3. 在上司面前,我们是下属。
4. 在下属面前,我们是上司。
5. ……
引入DCI后,DDD四层架构模式中的Domain层变薄了,以前Domain层对应DCI中的三层,而现在:
1. Domain层只保留了DCI中的Data层和Interaction层,我们在实践中通常将这两层使用目录隔离,即通过两个目录object和role来分离层Data和Interaction。
DCI中的Context层从Domain层上移变成Context层。
因此,DDD分层架构模式就变成了五层,如下图所示:
1. User Interface是用户接口层,主要用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给Application层的接口。
2. Application层是应用层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到用户接口层的请求后,委托Context层与本次业务相关的上下文进行处理。
3. Context是环境层,以上下文为单位,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。
4. Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模。
5. Infrastructure层是基础实施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等。
DDD五层架构模式讨论完了吗?故事还没有结束……
笔者参与的很多DDD落地实践,都是面向控制面或管理面且消息交互比较多的系统。这类系统的一次业务,包含一组同步消息或异步消息构成的序列,如果都放在Context层,会导致该层的代码比较复杂,于是我们考虑:
1. Context层在面向控制面或管理面且消息交互比较多的系统中又分裂成两层,即Context层和大Context层。
2. Context层处理单位为Action,对应一条同步消息或异步消息。
3. 大Context层对应一个事务处理,由一个Action序列组成,一般通过Transaction DSL实现,所以我们习惯把大Context层叫做Transaction DSL层。
4. Application层在面向控制面或管理面且消息交互比较多的系统中经常会做一些调度相关的工作,所以我们习惯把Application层叫做Scheduler层。
因此,在面向控制面或管理面且消息交互比较多的系统中,DDD分层架构模式就变成了六层,如下图所示:
笔者在实践中,将这六层的本地化定义为:
1. User Interface是用户接口层,主要用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给Scheduler层的接口。
2. Scheduler是调度层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到用户接口层的请求后,委托Transaction层与本次操作相关的事务进行处理。
3. Transaction是事务层,对应一个业务流程,比如UE Attach,将多个同步消息或异步消息的处理序列组合成一个事务,而且在大多场景下,都有选择结构。万一事务执行失败,则立即进行回滚。当事务层收到调度层的请求后,委托Context层的Action进行处理,常常还伴随使用Context层的Specification(谓词)进行Action的选择。
4. Context是环境层,以Action为单位,处理一条同步消息或异步消息,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。环境层通常也包括Specification的实现,即通过Domain层的知识去完成一个条件判断。
5. Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模。
6. Infrastructure层是基础实施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等。
事务层的核心是事务模型,事务模型的框架代码一般放在基础设施层。关于事务模型,笔者以前分享过一篇文章— 《Golang事务模型》 ,感兴趣的同学可以看看。
综上所述,DDD六层架构可以看做是DDD五层架构在特定领域的变体,我们统称为DDD五层架构,而DDD五层架构与传统的四层架构类似,都是限定型松散分层架构 。
- 模式三:六边形架构 -
有一种方法可以改进分层架构,即依赖倒置原则(Dependency Inversion Principle,DIP),它通过改变不同层之间的依赖关系达到改进目的。
依赖倒置原则由Robert C. Martin提出,正式定义为:
高层模块不应该依赖于底层模块,两者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
根据该定义,DDD分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构好像被推平了。如果我们把分层架构推平,再向其中加入一些对称性,就会出现一种具有对称性特征的架构风格,即六边形架构。六边形架构是Alistair。
Cockburn在2005年提出的,在这种架构中,不同的客户通过“平等”的方式与系统交互。需要新的客户吗?不是问题。只需要添加一个新的适配器将客户输入转化成能被系统API所理解的参数就行。同时,对于每种特定的输出,都有一个新建的适配器负责完成相应的转化功能。
六边形架构也称为端口与适配器,如下图所示:
六边形每条不同的边代表了不同类型的端口,端口要么处理输入,要么处理输出。对于每种外界类型,都有一个适配器与之对应,外界通过应用层API与内部进行交互。上图中有3个客户请求均抵达相同的输入端口(适配器A、B和C),另一个客户请求使用了适配器D。假设前3个请求使用了HTTP协议(浏览器、REST和SOAP等),而后一个请求使用了AMQP协议(比如RabbitMQ)。
端口并没有明确的定义,它是一个非常灵活的概念。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后端口将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。应用程序通过公共API接收客户请求,使用领域模型来处理请求。
我们可以将DDD战术设计的建模元素Repository的实现看作是持久化适配器,该适配器用于访问先前存储的聚合实例或者保存新的聚合实例。正如图中的适配器E、F和G所展示的,我们可以通过不同的方式实现资源库,比如关系型数据库、基于文档的存储、分布式缓存或内存存储等。如果应用程序向外界发送领域事件消息,我们将使用适配器H进行处理。该适配器处理消息输出,而上面提到的处理AMQP消息的适配器则是处理消息输入的,因此应该使用不同的端口。
我们在实际的项目开发中,不同层的组件可以同时开发。当一个组件的功能明确后,就可以立即启动开发。由于该组件的用户有多个,并且这些用户的侧重点不同,所以需要提供多个不同的接口。同时,这些用户的认识也是不断深入的,可能会多次重构相关的接口。于是,组件的多个用户经常会找组件的开发者讨论这些问题,无形中降低了组件的开发效率。
我们换一种方式,组件的开发者在明确了组件的功能后就专注于功能的开发,确保功能稳定和高效。组件的用户自己定义组件的接口(端口),然后基于接口写测试,并不断演进接口。在跨层集成测试时,由组件开发者或用户再开发一个适配器就可以了。
- 六边形架构模式的演变 -
尽管六边形架构模式已经很好,但是没有最好只有更好,演变没有尽头。在六边形架构模式提出后的这些年,又依次衍生出三种六边形架构模式的变体,感兴趣的读者可以点击链接自行学习:
1. Jeffrey Palermo在2008年提出了 洋葱架构 ,六边形架构是洋葱架构的一个超集。
2. Robert C. Martin在2012年提出了干净架构(Clean Architecture),这是六边形架构的一个变体。
3. Russ Miles在2013年提出了 Life Preserver 设计,这是一种基于六边形架构的设计。
DDD、DSL 和 DCI
DDD 概念最早提出于 2004 年,作为一种软件开发的指导思想,DDD 对软件开发带来了诸多可能与方向,张晓龙认为 DDD 为软件开发带来的好处主要有以下几点:
首先,最大好处就是所有参与者围绕一个统一一致的领域模型工作,传统的分析模型和设计模型不再割裂,不管是做设计、做分析还是写代码、写文档,脑海中所构建的画面都是一致的。
第二,DDD 是一个软件开发过程,它显式地把领域和设计放到了软件开发的核心,软件人员和业务人员被受到同样的重视,他们合作来构建领域模型,使得软件的交付质量更高且维护成本更低;
第三,DDD 提出的分层架构,有效分离了业务复杂度和技术复杂度,凸显了领域模型,使得领域层的代码和领域模型保持高度一致;
第四,统一语言非常重要,每个概念在各自的上下文中是清晰的无歧义的,同时要控制领域模型的复杂度,于是 DDD 在战略上提出了分离子域(问题域空间)和拆分 BC(解决方案空间)的模式,BC 间通过 Context Mapping 来集成;
第五,DDD 在战术层面提出了很多模式(聚合,实体,值对象,服务,工厂,仓储),对领域模型中的元素进行了分类,并给出了每类元素在领域模型中的职责和特征,降低了领域模型的构建成本。
DDD 和 DSL、DCI 之间存在极强的关联性。
DDD 和 DSL 的融合有三点:
面向领域;
模型的组装方式;
分层架构演进。
DSL 可以看作是在领域模型之上的一层外壳,可以显著增强领域模型的能力。它的价值主要有两个,一是提升了开发人员的生产力,二是增进了开发人员与领域专家的沟通。举个例子:想让 BA 负责流程契约的设计,该流程契约是一个活文档,可以跑测试,而 BA 不熟悉宿主语言。于是,我们设计了一种外部 DSL 来专门描述流程契约,对 BA 非常友好,学习成本也很低(不超过 5 分钟就可以学会),最后发现 BA 很快就广泛使用了起来。外部 DSL 并不一定要定义新文法,我们直接复用了 plantUML 文法,安装该插件可以自动生成序列图,非常棒!对于外部 DSL,需要自己实现一个解析器将 DSL 文法解析成语法树,再根据语法树生成语义模型。语义模型可以看作领域模型(严格的讲语义模型是领域模型的子集),外部 DSL 就是对领域模型的一种组装方式。
DCI 的作用主要体现在两方面:
首先,DCI 助力 DDD 战术设计:
显式的对 ROLE 建模,解决了贫血模型与充血模型之争;
一个聚合可以支持哪些 ROLE,一个 ROLE 可以由哪些聚合扮演,一个场景下哪些聚合要扮演哪些角色;
当 Aggregate 内部实体行为比较多时可以嵌套使用 DCI 来拆分和组合;
其次,DCI 助力 DDD 代码落地:
对象就是 Data,Client 为 Context,对象在 Client 中的行为就是 ROLE。
根据正交设计原则得到小类(素材库),根据多重继承(only C++)或依赖注入来组合素材,不管是行为类还是数据类,都按 Role 的方式来组合,对像仅仅组合 Role 并注入依赖;
小类大对象:类作为一种模块化手段,遵循高内聚,低耦合,让软件易于应对变化;对象作为一种领域对象的的直接映射,解决了过多的类带来的可理解性问题,让领域可以指导设计,设计真正反映领域;领域对象需要真正意义上的生命周期管理。
DDD 思想贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构、设计,和最终的代码实现,甚至对代码的测试与重构。代码是业务的核心资产,不管是否特性团队,开发团队肯定是代码的编写者和守护者。
对于开发团队而言,需要关注以下几点:
首先是统一语言,让团队成员可以做到无障碍的沟通,不管是什么角色都能基于同样的画面进行讨论;
其次是团队中各个角色都围绕领域模型开展工作;
第三是代码物理设计容易标准化,比如说在分层设计时,基础设施层怎么设计,应用层怎么设计,DTO 应该放在哪儿,领域层中各个建模元素如何组织?
更进一步,在分层架构里,应用层更加关注横切面的东西,比如说要上报一个告警,要给用户发送一个 Email,这些最好都集中放到应用层里面。但触发是在领域层发生的,应用层怎么知道?通过领域事件来实现依赖反转,即应用层订阅领域事件,领域层发布领域事件。
应用架构分层
回到单个应用的架构分层,谈得最多的就是常说的三层架构模式。在软件架构中,经典三层架构自顶向下由用户界面层(User Interface Layer)、业务逻辑层(Business Logic Layer)与数据访问层(Data Access Layer)组成。
在整个实现过程中,可能还会增加独立的Facade层,或独立的API接口服务提供层,统一的DTO数据传输对象层等,但是这些都不影响整体的三层逻辑结构。
三层架构本身也和一个业务功能实现的完整对应,在数据访问层处理数据获取和持久化操作,在业务逻辑层对业务规则进行处理,在界面展现层进行相应的前端展现和用户交互。
而谈到领域建模的时候,又引入了领域模型中的分层架构,如下:
领域驱动设计在经典三层架构的基础上做了进一步改良,在用户界面层与业务逻辑层之间引入了新的一层,即应用层(Application Layer)。同时,一些层次的命名也发生了变化。将业务逻辑层更名为领域层自然是题中应有之义,而将数据访问层更名为基础设施层(Infrastructure Layer),则突破了之前数据库管理系统的限制,扩大了这个负责封装技术复杂度的基础层次的内涵。
当然,也有融合了领域模型和传统三架构思路后的技术架构如下:
领域层和业务逻辑层
在领域建模的一个核心是领域模型,领域模型不再是一个个独立的数据库表或数据对象,而是一个业务对象或领域对象。因此领域层是面向领域对象而设计实现,而业务规则能力本身也是属于领域对象对外提供的能力接口。即业务规则本身也是领域对象暴露的能力。
传统业务逻辑层实现往往是一个数据对象对应一个DAO,一个Service和一个Interface。而领域模型下DAO可以是分开的,但是Service逻辑层往往则更多应该按领域模型思路对DAO层的能力进行组装和聚合。
独立应用层拆分
在我原来理解里面,领域层提供领域模型和领域服务能力接口,而应用层更多的是对领域层多个领域对象模型提供的服务能力进一步进行组装和编排,然后再暴露给前端应用。
谈到应用层的概念,实际上可以理解为前端应用中存在的共性能力的进一步下沉。即应用本身只是用户业务功能实现的承载,但是这个功能的实现可以通过多种前端展现形式,比如传统的CS桌面应用,BS应用,或手机端APP。
在电商里面,一个商品订购就是一个独立的应用,用户可以在APP完成,也可以在BS端完成,但是不论在哪里完成最终应用层提供的能力都应该一样。比如完成一个商品订购需要同时和底层的订单,库存,支付多个服务进行交付和协同。那么这个逻辑显然不适合同时在BS端应用和APP端应用中进行重复编写和开发。那么这个内容就应该在应用层实现。
如果回到微服务和中台架构下,这个应用层拆分更加必要,即通过应用层来下沉共性的服务组合和组装逻辑,这个逻辑和协同不应该属于任何一个前端应用。
界面层还是接口层
在开发一个聚合能力的中台微服务模块的时候,可以看到这个微服务模块本身并没有界面展现层,那么该微服务的最上层仅仅是提供API接口的接口服务层。
该API接口服务能力既可以提供给APP前端,也可以提供给BS端使用。
软件技术架构分层
软件技术架构构图,分层仍然可以沿用软件三层分层模型,重点是说明清楚各层用到的关键技术组件或技术服务能力。比如软件开发三层模型的技术架构分层如下:
如果本身就是一个技术平台,类似大数据平台,那么我们在整体构图的时候仍然需要考虑先进行分层,再详细说明每层里面的技术内容。
比如对应一个大数据平台,包括了大数据采集,大数据存储,大数据处理,大数据分析和应用,那么这个就是关键的分层,可以基于这个分层再来考虑各层采用的关键技术。
对于技术栈构图基本也可以参考技术架构构图模式进行。
技术架构重点需要回答的就是你在进行软件架构设计过程中,究竟会用到哪些关键技术,哪些开源产品或工具等。可以细化到具体的技术产品,也可以仅细化到产品类型。
比如消息中间件,你可以细化到采用RabbitMQ,也可以在技术架构中只体现采用消息中间件。
技术架构和软件功能分层架构唯一相同的就是分层,技术架构在各个分层里面都没有具体的业务功能点和实现内容,仅仅是关键技术点说明。
单个应用功能架构
注意应用功能架构完全是重点描述应用系统具备哪些功能,一个功能究竟是采用什么三层技术架构实现并不用关心。因此功能架构不应该体现数据层,逻辑层,技术点这些内容。
那么对于一个应用系统的功能如何分层?
我们可以参考业务分层分类,将业务分为基础支撑层,执行层,决策管理层。这样基本的分层模式就出来了,基于该方式可以完成一个功能架构构图。
对于单个应用来说一般不会自身有云平台,PaaS平台这类概念。但是单个应用构建一定存在共性技术支撑平台能力,比如有自己的流程管理,各自共性技术功能组件等。因此单应用构建还可以采用基础技术支撑层+应用层+门户层的方式进行构图。
在应用层再按具体的业务域或业务阶段进行进一步细分。
架构图的分层构图逻辑
在前面基本给出了不同类型的架构图的核心分层逻辑,可以看到在画架构图的时候尽量不要混合使用不同场景下的构图方式,否则就导致整体架构图混乱。
在画整体架构的时候一般需要重点参考云三层架构,SOA三层架构的构图模式进行构图。而在细化到某一个应用系统的时候,仍然还需要分清是构建技术架构图还是功能架构图,两者本身的分层逻辑也存在很大的差别而不能混用。
架构图的构图逻辑
要完成一个完整的架构图构图,可以先拆分为两边+中间。两边一般是放具体的标准,规范等,比如安全管理,质量管理,技术标准规范,开发运维规范等。
中间即是重点需要考虑进行分层构建的地方。
在前面也谈到了中间部分重点参考云计算和SOA的架构分层逻辑。一般来说核心的还是资源层,平台层,应用层,门户层。而对于应用层本身又可以考虑业务域进一步拆分,或者根据价值链或业务生命周期拆分为多个阶段域再展开描述。
在云和SOA下,更加强调平台+应用构建模式。
而两者之间一般是服务层,通过SOA平台或API能力开放平台来统一接入和发布服务,以形成一个完整的资源+服务+应用的松耦合架构。
同时一个完整的架构本身就是多视角的,如下:
功能架构往往可以给具体用户和业务人员看,而对于技术架构往往更多是内部团队开发人员研讨使用。而设计到资源和平台的架构图往往又是运维工程人员进行部署架构搭建的重要参考。因此不同维度的架构分层属性本身不能随意融合使用,而导致架构图混乱。
大数据架构分层
数据分层的一些概念说明
01 什么是数据分层?
数据分层是一套行之有效的数据组织和管理方法,使得数据体系更有序。
02 数据分层的好处
(1)清晰数据结构
每一个数据分层都有它的作用域和职责,在使用表的时候能更方便的定位和理解。
(2)减少重复开发
规范数据分层,开发一些通用的中间层数据,能够减少极大的重复计算。
(3)统一数据口径
通过数据分层,提供统一的数据出口,统一对外输出的数据口径。
(4)复杂问题简单化
将一个复杂的任务分解成多个步骤完成,每一层解决特定的问题。
一种通用的数据分层设计
ODS:存放原始数据
DW:存放数仓中间层数据
APP:面向业务定制的应用数据
03 举例
以下是一个电商网站的数据体系设计,只关注用户访问日志这部分数据。
04 各层会用到的计算引擎和存储系统
05 分层实现
在确定建模思路和模型类型之后,下一步的工作是数据分层。数据分层可以使得数据构建体系更加清晰,便于数据使用者快速对数据进行定位;同时数据分层也可以简化数据加工处理流程,降低计算复杂度。
我们常用的数据仓库的数据分层通常分为集市层、中间层、基础数据层上下三层结构。由传统的多层结构减少到上下三层结构的目的是为了压缩整体数据处理流程的长度,同时扁平化的数据处理流程有助于数据质量控制和数据运维。
在上下三层的结构的右侧,我们增加了流式数据,将其添加成数据体系的一部分。这是因为当前的数据应用方向会越来越关注数据的时效性,越实时的数据价值度越高。
但是,由于流式数据集的采集、加工和管理的成本较高,一般都会按照需求驱动的方式建设;此外,考虑到成本因素,流式数据体系的结构更加扁平化,通常不会设计中间层。
大数据数据仓库是基于HIVE构建的数据仓库,分布文件系统为HDFS,资源管理为Yarn,计算引擎主要包括MapReduce/Tez/Spark等,分层架构说明如下:
数据来源层:日志或者关系型数据库,并通过Flume、Sqoop、Kettle等etl工具导入到HDFS,并映射到HIVE的数据仓库表中。
事实表是数据仓库结构中的中央表,它包含联系事实与维度表的数字度量值和键。事实数据表包含描述业务(例如产品销售)内特定事件的数据。
维度表是维度属性的集合。是分析问题的一个窗口。是人们观察数据的特定角度,是考虑问题时的一类属性,属性的集合构成一个维。数据库结构中的星型结构,该结构在位于结构中心的单个事实数据表中维护数据,其它维度数据存储在维度表中。每个维度表与事实数据表直接相关,且通常通过一个键联接到事实数据表中。星型架构是数据仓库比较流向的一种架构。
星型模式的基本思想就是保持立方体的多维功能,同时也增加了小规模数据存储的灵活性。
说明:
事实表就是你要关注的内容;
维度表就是你观察该事务的角度,是从哪个角度去观察这个内容的。
例如,某地区商品的销量,是从地区这个角度观察商品销量的。事实表就是销量表,维度表就是地区表。
4、主题表:主题(Subject)是在较高层次上将企业信息系统中的数据进行综合、归类和分析利用的一个抽象概念,每一个主题基本对应一个宏观的分析领域。在逻辑意义上,它是对应企业中某一宏观分析领域所涉及的分析对象。例如“销售分析”就是一个分析领域,因此这个数据仓库应用的主题就是“销售分析”。
面向主题的数据组织方式,就是在较高层次上对分析对象数据的一个完整并且一致的描述,能刻画各个分析对象所涉及的企业各项数据,以及数据之间的联系。所谓较高层次是相对面向应用的数据组织方式而言的,是指按照主题进行数据组织的方式具有更高的数据抽象级别。
与传统数据库面向应用进行数据组织的特点相对应,数据仓库中的数据是面向主题进行组织的。例如,一个生产企业的数据仓库所组织的主题可能有产品订货分析和货物发运分析等。而按应用来组织则可能为财务子系统、销售子系统、供应子系统、人力资源子系统和生产调度子系统。
5、汇总数据层:聚合原子粒度事实表及维度表,为满足固定分析需求,以提高查询性能为目的,形成的高粒度表,如周报、月报、季报、年报等。
6、应用层:应用层,这层数据是完全为了满足具体的分析需求而构建的数据,也是星形结构的数据。应用层为前端应用的展现提现数据,可以为关系型数据库组成。
数据仓库一般分为三层,自上而下分别为数据贴源层(ODS,Operation Data Store)、数据公共层(CDM,Common Data Model)和数据应用层(ADS,Application Data Service)。
1
ODS层
贴源层,与业务库保持一致,不做任何处理
2
CDM层
数据公共层CDM(Common Data Model,又称通用数据模型层),包括DIM维度表、DWD,DW和DWS,由ODS层数据加工而成。主要完成数据加工与整合,建立一致性的维度,构建可复用的面向分析和统计的明细事实表,以及汇总公共粒度的指标
公共维度层(DIM):基于维度建模理念思想,建立企业一致性维度。降低数据计算口径和算法不统一风险。
公共维度层的表通常也被称为逻辑维度表,维度和维度逻 辑表通常一一对应。
明细粒度事实层(DWD):对数据进行规范化编码转换,清洗,统一格式,脱敏等,不做横向整合
主题宽表层(DW) 对dwd各种信息进行整合,输出主题宽表(面 向业务过 程,不同业务过程的信息不冗余建设,采用外键形式)
公共汇总粒度事实层(DWS):以分析的主题对象作为建模驱动,基于上层的应用和产品的指标需求,构建公共粒度的汇总指标事实表,以宽表化手段物理化模型。构建命名规范、口径一致的统计指标,为上层提供公共指标,建立汇总宽表、明细事实表。
公共汇总粒度事实层的表通常也被称为汇总逻辑表,用于存放派生指标数据。
3
ADS层
数据应用层ADS(Application Data Service):面向业务需求定制开发,存放数据产品个性化的统计指标数据。
2
逻辑分层架构
3
分层的好处
清晰数据结构:每一个数据分层都有它的作用域,这样我们在使用表的时候能更方便地定位和理解。
数据血缘追踪:简单来讲可以这样理解,我们最终给业务呈现的是一张能直接使用的张业务表,但是它的来源有很多,如果有一张来源表出问题了,我们希望能够快速准确地定位到问题,并清楚它的危害范围。
减少重复开发:规范数据分层,开发一些通用的中间层数据,能够减少极大的重复计算。
把复杂问题简单化:将一个复杂的任务分解成多个步骤来完成,每一层只处理单一的步骤,比较简单和容易理解。而且便于维护数据的准确性,当数据出现问题之后,可以不用修复所有的数据,只需要从有问题的步骤开始修复。
【更多阅读】
Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析&Redis分布式锁的正确使用姿势!
B-树(B-Tree)与二叉搜索树(BST):讲讲数据库和文件系统背后的原理(读写比较大块数据的存储系统数据结构与算法原理)
《人月神话》8 胸有成竹(Chaptor 8.Calling the Shot -The Mythical Man-Month)
《人月神话》(The Mythical Man-Month)5画蛇添足(The Second-System Effect)
《人月神话》(The Mythical Man-Month)4概念一致性:专制、民主和系统设计(System Design)
Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析&Redis分布式锁的正确使用姿势!
十年技术进阶路:让我明白了三件要事。关于如何做好技术 Team Leader?如何提升管理业务技术水平?(10000字长文)
……
以上是关于软件架构思想系列分层架构的主要内容,如果未能解决你的问题,请参考以下文章
软件架构设计学习总结(22):软件架构——分层架构事件驱动架构微内核架构微服务架构基于空间的架构