引子
系统在从0到1阶段时,为了可让产品快速上线,此时系统分层一般不是软件开发需要重点考虑的范畴,但是随着业务逐渐复杂 ,大量代码纠缠耦合,此时会出现逻辑不清楚、模块相互依赖、扩展性差、改一处动全身的问题。
系统在从1到100…0时,系统会变的特别复杂,此时系统分层就会被提上日程,那么什么是分层?为何要分层?如何对软件进行分层?软件分层有什么指导原则呢?本文将带大家逐一寻找答案。
何为分层架构?
分层架构是一种分而治之的设计思想,是一种基于层次的架构范式。一般开发人员会把一个系统基于分而治之的理念将系统拆分成多个模块或子系统;然后对已拆分出的模块按业务层次分成相关依赖和层次关系的组;从而实现根据功能组实现功能和耦合的隔离。我们称这种范式为分层架构范式。
分层架构举例
| |
分层架构最典型的例子即是Android操作系统,其是基于Linux的移动开源操作,Android系统采用自上而下的分层范式,图1是google官方提供Android系统架构图,图2是Android系统的五层层间关系图。
Android操作系统架构的五个层分别为:
- 应用层 applications
- 应用框架层 frameworks
- 系统运行库层 native c/c++ libraries/android runtime
- 硬件抽象层 hardware abstraction layer
- Linux内核层 linux kernel
为何分层?
在软件开发从0到1阶段,开发人员一般通过模块隔离实现业务隔离,此时模块隔离是软件隔离的“专注点”;而在系统从1到100…0时,由于系统处于超大规模状态,按照模块隔离已不太现实。此时几乎每个软件都通过层隔离“关注点”,以此应对不同需求的变化,使得这种变化可以独立进行,从而给开发者带来如下的好处:
- 高内聚:分层设计可以简化系统设计,让不同的层专注自身业务
- 低耦合:层与层之间通过接口或API交互,依赖方不用知道被依赖方的细节
- 复用:分层之后可以做到很高的复用
- 扩展性:分层架构可以让我们更容易做横向扩展
除此之外,分层架构范式还是隔离业务复杂度与技术复杂度的利器,《领域驱动设计模式、原理与实践》有这样一段论述:
为了避免将代码库变成大泥球并因此减弱领域模型的完整性且最终减弱可用性,系统架构要支持技术复杂性与领域复杂性的分离。引起技术实现发生变化的原因与引起领域逻辑发生变化的原因显然不同,这就导致基础设施和领域逻辑问题会以不同速率发生变化。
这里的“以不同速率发生变化”,其实就是引起变化的原因各有不同,这正好是单一职责原则(Single-Responsibility Principle,SRP)的体现,这也是为什么要将业务与基础设施分开的原因,因为引起它们变化的原因不同。
综上所述,分层范式本质就是将复杂问题简单化,基于单一职责原则让每层代码各司其职,基于“高内聚,低耦合”的设计思想实现相关层对象之间的交互。从而提升代码的可维护性和可扩展性。在小规模软件中,职责隔离的关注点是模块或类,在大规模软件设计中,职责隔离的关注点是层次,我们通过层次隔离变化,这才是软件分层的终极目标。
如何分层?
软件的分层存在诸多原则,此处笔者首先分析介绍软件分层的四个原则,然后为大家介绍《面向模式的软件架构:卷1(模式系统)》提出的一种逐步细化的分层架构方法。
指导原则
之所以需要对软件进行分层,这其实是我们下意识的认知规则:机器为本,用户至上。机器是软件运行的基础,而我们打造的系统是为用户服务的。分层架构层次越高,其抽象层次就越面向业务,越面向用户;分层架构层次越往下,其抽象层次就越变的统一,越面向设备。经典的三层架构就是源于这个认知规则:上层关注用户的体验和交互;中层关注应用和业务逻辑;下层关注外部资源和设备。因此分层的第一个原则就是基于关注点为不同的业务划分层次。
分层的第二个原则是隔离变化,分层时针对不同的变化原因确定层次的边界,至少保证将变化对各层的影响限制到最小,但是这只是最小目标,最大目标是严禁层间相互干扰。例如,数据库的修改应该只影响基础设施层的数据模型和领域层的领域模型,但是如果只修改基础设施层的数据库访问逻辑,就不应该影响到领域层的领域模型。
分层的第三个原则是层与层之间应该是正交的,所谓正交,并非指两层之间没有关系,而是两者应该是垂直相交的两条直线。两次之间唯一的依赖点就是两条直线的交点,即两层之间的协作点。此协作点就是层间的抽象接口。正交的两条直线,无论对那条直线进行延展,都不会对另外一条直线产生任何影响(直线的投影)。如果非正交,一条直线延伸时,它总会投影到另外一条直线,这就意味着另外一条直线受到了本条直线延展的影响。
分层还有一个原则是保证同一层的组件处于同一个抽象层次。此原则借鉴了 Kent Beck 在《Smalltalk Best Practice Patterns》 一书提出的“组合方法”模式。该模式要求一个方法中的所有操作处于相同的抽象层,这就是所谓的“单一抽象层次原则(SLAP)”。
分层指导
了解了诸多分层原则,下面为大家介绍《面向模式的软件架构:卷1(模式系统)》提出的一种逐步细化的分层架构方法。
第一步,定义将任务划分到不同层的抽象准则。在真正的软件开发中,通常根据距离硬件的距离划分较低的层,按照概念的复杂度划分比较高的层。
第二步,根据抽象准则确定抽象层的级数。
第三步,给每层命名并分派任务。
第四步,规范服务,确保任何组件或模块不会跨层。同时将更多的组件放置到较高的层次,让较低的层次保持苗条。从而形成倒金字塔。
第五步,完善层次划分,反复执行第一到四步。
第六步,规范每一层的抽象接口,确保对J+1层而言,J层应该是一个“黑盒”。设计统一的接口,该接口可以提供第J层的所有服务。
第七步,确定各层的结构。确定层次得结构不仅要确保层间关系要合理,同时要求层内组件要有适当的颗粒度。从而避免层间关系完美无暇,但是层内关系混乱的局面。
第八步,规范相邻层的通信机制,分层架构中,常用的通信机制就是推模型,第J层调用第J-1层时随服务调用一起传递所以所需的信息。
第九步,将相邻层解耦,一般情况下,分层架构中上层知道下层,但是下层不知道上层的身份,从而形成单向耦合。
层间协作
在大家的固有认知中,分层架构都是自顶向下传递的,从抽象层次而言,抽象程度越高,越通用,越公共,此层次与具体业务隔离的越远。此层次就是我们平常所说的平台层或框架的调用者。
自顶向下要求上层依赖下一层,这是和依赖倒置原则(Dependency Inversion Principle,DIP)存在冲突的。依赖倒置原则要求:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。这一原则给了我一个明确的提醒,谁规定的在自顶向下的架构中,依赖就要沿着自顶向下的方向传递,这是一个错误的理解。依赖倒置原则隐含的本质是:我们要依赖不变或稳定的元素(类、模块或层)。也就是该原则的第二句话:抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则是“面向接口设计”原则的体现,即“针对接口编程,而不是针对实现编程”。高层模块对低层模块的实现是一无所知的,带来的好处是:
- 低层模块的细节实现可以独立变化,避免变化对高层模块造成影响
- 高层和底层模块都可以独立于对方单独编译,从而实现独立开发编译
- 对于高层模块而言,低层模块的实现是可替换的
如果高层和低层都依赖抽象,那现在会存在这样一个问题,底层的具体实现怎么传递给高层呢?由于高层通过两者的“正交点”即抽象接口隔离的对具体实现的依赖,那么具体实现的依赖就转移到外部了,具体实现将由外部调用者决定。调用者在调用代码时才会把底层的具体实现传递给高层。软件开发大师Martin Fowler形象地将这种机制称为“依赖注入(dependency injection)。
因此,为了很好的解除高层对底层的依赖,我们需要将依赖倒置和依赖注入两种结合,从而更好的理解他们。
除此之外,层间的信息传递不一定都是自顶向下的传递,还有可能是自底向上的传递。例如Android系统中的通知机制。当Android系统收到消息时,Android系统将消息通知上层业务模块。如果说自顶向下的消息传递被描述为“请求“(或调用)”,那么自底而上的消息可被描述为“通知”。换个角度思考“通知”,这其实就是上层对下层的观察,下层的状态发生变化,通过观察机制将下层的变化传递给上层。而上层消费对下层传递的消息。此模式就是大家常说的观察者模式。
无论是通过依赖注入和依赖倒置原则实现上层对下层的“请求(或调用)”,亦或是通过观察者模式实现下层对上层的“通知”。这些都颠覆了我们固有思维中那种高层依赖低层的理解。
现在我们对层间协助有了更清晰的认知,所以我们在开发中要正视架构中各层之间的协作关系,打破高层依赖低层的固有思维,从解除耦合(或降低耦合)的角度探索层之间可能的协作关系。
另外,我们还需要确定分层的架构原则(或约束),例如是否允许跨层调用,即每一层都可以使用比它低的所有层的服务,而不仅仅是相邻低层。这就是所谓的“松散分层系统(Relaxed Layered System)”。
经典分层范式
经典的分层范式主要包括经典三层架构,阿里四层架构,DDD领域驱动分层架构三种架构方式。
经典三层架构
经典三层架构按照功能将系统功能模块划分为表示层(UI)、业务逻辑层(BLL)和数据访问层(DAL)。表示层UI位于三层架构的最上层,实现系统与用户直接的交互,以及消息事件的处理;业务逻辑层BLL,实现数据处理和数据传递,将界面表示层和数据访问层连接起来,起到承上启下的作用;数据访问层DAL,实现数据的增加、删除、修改、查询等操作,并将操作结果反馈到业务逻辑层 BLL。
阿里四层架构
阿里四层架构,在原三层架构基础上增加了 Manager 层,将原经典三层架构细化成如图3所示的架构图
- 开放接口层:
- 开放接口层可以直接依赖Service层也可依赖WEB层;
- 依赖Service层,可将 Service 封装成 RPC 对外暴露;
- 依赖WEB层,可以将业务封装成 HTTP对外暴露。
- 终端显示层:
- 负责各个端的模板渲染并执行显示;
- 当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等;
- Web层:
- 对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等;
- Service层:
- 主要负责具体的业务逻辑服务实现;
- Manager通用业务处理层,主要职责包括:
- 对第三方平台封装:对Service层通用基础组件的封装,如缓存、中间件通用处理;
- 与DAO层交互,对多个DAO的组合复用
- DAO 层:
- 数据访问层,与 MySQL、Oracle、Redis等进行数据交互。
DDD分层架构
DDD领域驱动分层架构,领域驱动设计在经典三层架构的基础上进一步改良,在顶层用户界面层与业务逻辑层之间引入了应用层。引入应用层后,根据领域驱动的概念各层的名称也做了调整。将业务逻辑层更名为领域层,而将数据访问层更名为基础设施层(Infrastructure Layer),图4为 Eric Evans 在其经典著作《领域驱动设计》中的分层架构。
总结
现在,我们对分层架构有了更清晰的认识。因此我们必须打破那种谈分层架构必为经典三层架构又或领域驱动设计推荐的四层架构这种固有思维,而是将分层视为关注点分离的水平抽象层次的体现。既然如此,架构的抽象层数就不是固定的,甚至每一层的名称也未必遵循固有(经典)的分层架构要求。设计系统的层需得结合系统具体业务场景而定。当然我们也要认识到层次多少的利弊:过多的层会引入太多的间接而增加不必要的开支,层太少又可能导致关注点不够分离,导致系统的结构不合理。
分层架构范式,是软件开发中最常用,也是容易滥用的架构范式。开发人员可以根据当前软件的开发约束和需求,制订出符合自身的分层,以方便软件开发和开发人员协助。基于分层范式,我们可以做到修改模块内的代码,而不影响其他模块,这是软件分层给我们带来的优势。也是高内聚低耦合的特性。