系统架构:分层架构

Posted @一鸣惊人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了系统架构:分层架构相关的知识,希望对你有一定的参考价值。

引子

系统在从0到1阶段时,为了可让产品快速上线,此时系统分层一般不是软件开发需要重点考虑的范畴,但是随着业务逐渐复杂 ,大量代码纠缠耦合,此时会出现逻辑不清楚、模块相互依赖、扩展性差、改一处动全身的问题。

系统在从1到100…0时,系统会变的特别复杂,此时系统分层就会被提上日程,那么什么是分层?为何要分层?如何对软件进行分层?软件分层有什么指导原则呢?本文将带大家逐一寻找答案。

何为分层架构?

分层架构是一种分而治之的设计思想,是一种基于层次的架构范式。一般开发人员会把一个系统基于分而治之的理念将系统拆分成多个模块或子系统;然后对已拆分出的模块按业务层次分成相关依赖和层次关系的组;从而实现根据功能组实现功能和耦合的隔离。我们称这种范式为分层架构范式。

分层架构举例

图1 android系统架构图
图2 Android架构层间关系图

分层架构最典型的例子即是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)。三层构架如图3所示,表示层UI位于三层架构的最上层,实现系统与用户直接的交互,以及消息事件的处理;业务逻辑层BLL,实现数据处理和数据传递,将界面表示层和数据访问层连接起来,起到承上启下的作用;数据访问层DAL,实现数据的增加、删除、修改、查询等操作,并将操作结果反馈到业务逻辑层 BLL。

图3 三层构架

阿里四层架构

阿里四层架构,在原三层架构基础上增加了 Manager 层,将原经典三层架构细化成如图4所示的架构图

图4 阿里四层架构图

  • 开放接口层:
    1. 开放接口层可以直接依赖Service层也可依赖WEB层;
    2. 依赖Service层,可将 Service 封装成 RPC 对外暴露;
    3. 依赖WEB层,可以将业务封装成 HTTP对外暴露。
  • 终端显示层:
    1. 负责各个端的模板渲染并执行显示;
    2. 当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等;
  • Web层:
    1. 对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等;
  • Service层:
    1. 主要负责具体的业务逻辑服务实现;
  • Manager通用业务处理层,主要职责包括:
    1. 对第三方平台封装:对Service层通用基础组件的封装,如缓存、中间件通用处理;
    2. 与DAO层交互,对多个DAO的组合复用
  • DAO 层:
    1. 数据访问层,与 mysql、Oracle、Redis等进行数据交互。

DDD分层架构

DDD领域驱动分层架构,领域驱动设计在经典三层架构的基础上进一步改良,在顶层用户界面层与业务逻辑层之间引入了应用层。引入应用层后,根据领域驱动的概念各层的名称也做了调整。将业务逻辑层更名为领域层,而将数据访问层更名为基础设施层(Infrastructure Layer),图5为 Eric Evans 在其经典著作《领域驱动设计》中的分层架构。

图5 DDD领域驱动分层架构

总结

现在,我们对分层架构有了更清晰的认识。因此我们必须打破那种谈分层架构必为经典三层架构又或领域驱动设计推荐的四层架构这种固有思维,而是将分层视为关注点分离的水平抽象层次的体现。既然如此,架构的抽象层数就不是固定的,甚至每一层的名称也未必遵循固有(经典)的分层架构要求。设计系统的层需得结合系统具体业务场景而定。当然我们也要认识到层次多少的利弊:过多的层会引入太多的间接而增加不必要的开支,层太少又可能导致关注点不够分离,导致系统的结构不合理。

分层架构范式,是软件开发中最常用,也是容易滥用的架构范式。开发人员可以根据当前软件的开发约束和需求,制订出符合自身的分层,以方便软件开发和开发人员协助。基于分层范式,我们可以做到修改模块内的代码,而不影响其他模块,这是软件分层给我们带来的优势。也是高内聚低耦合的特性。

软件架构之分层架构理解

分层架构特定场景:分层架构是一种很常见的架构模式,它也叫N层架构。分层架构适用于一个集成不同功能的系统,当我们需要把很多不同的代码集起来的时候,这种模式提供了最合理的结构。能让我们的代码有足够的灵活性去应对需求改变。当系统本身不负责或者可预期的修改很少时,则不适合用分层架构,因为这样可以增加很多不必要的代码,陷入过度设计的泥坑。不过分层架构模式是一个稳定的通用模式,这使得它成为大部分应用程序的首选,特别是当你不确定使用哪个架构的时候。没有任何一种架构模式是万能的,所以每个模式都必须有“适应场景”。而模式本身的内容,就是通过“模式定义”和“模块关系”两个层面的规定来表现出来。

 

分层架构解决的问题:当软件需求变更之后,基本上是对软件代码的修改。而软件代码的修改却是程序员们最头疼的事情。因为一些大型系统,它的代码根本完全看不懂,即便是了解了部分细节后,着手修改的时候,也会碰到牵一发而动全身的问题。因为有些功能的修改,需要修改整个系统的很多部分,导致了无穷的BUG。另外就是在紧迫时间内,对于代码的修改往往只能依赖有限的一个或几个程序员,只有他们对系统最熟悉。但是,会面临工作量巨大的问题,几乎无法让更多的程序玩参与进来。假如,熟悉的程序员离职,就有可能代表了整个系统的无法维持。即便是系统能分割给几个人负责,在“集成”几个部分代码的时候,其调试和排错工作,又常常是很持久的,因为那些从来没有协作过的代码,隐藏着大量的误解和不兼容性的问题。这一切的根源只是一个最简单的事实,就是系统对于“代码耦合”的结构问题。糟糕的代码耦合让整个系统变得难以理解,难以修改,难以分工,难以集成。而分层架构就是来解决这个耦合问题的。在这个模式中,请求流只是简单的穿过层次,不留一点云彩,或者说只留下一阵青烟。比如说界面层响应了一个获得数据的请求。响应层把这个请求传递给了业务层,业务层也只是传递了这个请求到持久层,持久层对数据库做简单的SQL查询获得用户的数据。这个数据按照原理返回,不会有任何的二次处理,返回到界面上。每个分层架构或多或少都可能遇到这种场景。关键在于这样的请求有多少。80-20原则可以帮助你确定架构是否处于反污水模式。大概有百分之二十的请求仅仅是做简单的穿越,百分之八十的请求会做一些业务逻辑操作。然而,如果这个比例反过来,大部分的请求都是仅仅穿过层,不做逻辑操作。那么开放一些架构层会比较好。不过由于缺少了层次隔离,项目会变得难以控制。

 

分层架构的解决方案:分层架构里的组件被分成几个平行的层次,每一层都代表了应用的一个功能。一般情况下,结构大多分成四层:展示层,业务层,持久层,和数据层。有时候,业务层和持久层会合并成单独的一个业务层,尤其是持久层的逻辑绑定在业务层的组件当中。因此,有一些小的应用可能只有3层,一些有着更复杂的业务的大应用可能有5层或者更多的分层。例如:展示层负责处理所有的界面展示以及交互逻辑, 业务层负责处理请求对应的业务。架构里的层次是具体工作的高度抽象,它们都是为了实现某种特定的业务请求。例如:展示层并不需要关心怎样得到用户数据,它只需在屏幕上以特定的格式展示信息。 业务层并不关心要展示在屏幕上的用户数据格式,也不关心这些用户数据从哪里来。它只需要从持久层得到数据,执行与数据有关的相应业务逻辑,然后把这些信息传递给展示层。分层架构的一个突出特性是组件间关注点分离。

      分层架构中的每一层都着特定的角色和职能。它们中的每一层都有着特定的角色和职能。架构里的层次是具体工作的高度抽象,它们都是为了实现某种特定的业务请求。分层的模式的规定非常简单有效:①每个模块必须属于某个层次,提供给“第N+1层”(上层)服务;同时委派任务给“第N-1层”的模块。②任何一个模块。都不得逆层次调用:属于第N层模块的,不得调用,第N+1层或者以上层次的模块。③任何一个模块,都不得跨层调用:属于第N层的模块,不应该调用第N-2层或者更下层的模块。以业务逻辑特征建模,使用分层模式,往往需要我们在大脑里对问题领域进行层次抽象,这种抽象是可信赖的原则,就是按照业务逻辑去做。比如现实业务中有一个角色,我们就建立一个角色的模块,如果我们有一个场景,就以此为名建立一个这样的模块。以业务逻辑建立的模块,本身也会让系统更容易被理解,因为在代码里能找到和现实中一一对应的概念。多亏了组件分离,让我们更容易构造有效的角色和强力的模型。 这样应用变的更好开发,测试,管理和维护。

 

分层架构的实例:

实例1:比如,存在一个复杂功能的多人在线社区系统,它的服务器端是我们需要重点讨论的对象。这个产品的服务器端必须满足多样功能:如,玩家移动到不同的场景中,玩家移动到不同的场景中,玩家可以换上不同的服装,可以互相加好友并且可以互相聊天,同时还有广播频道的聊天功能,每个玩家还有终极的资料库和背包,另外还有各种运营活动。

       在最初的开发过程中,我们需要针对每一个需要开发的功能,建立一个模块。这些模块通过单独和客户端、数据库的操作,完成所需功能。如果要开发新的功能,就重写一个这样的模块。这种架构在一开始的时候就有效的,但是随着产品的功能被不断的开发出来,模块的数量也哎增多,但是就潜藏了一个问题。

       这个问题的爆发,就是随着任务系统的功能的增多而出现的。因为任务系统本质上是很多其他模块功能的功能支持。如需要玩家去某个场景,获得了某个东西,然后添加了一个好友,或者换了某个时装,发一句消息等等。这样的任务功能实现,被迫要修改很多个模块的代码,因为每个模块都只有最基本的“自由使用”功能的代码,编程接口都仅仅是面向客户端的,而数据结构都是直接SQL到数据库的。这种需要组合的功能需求,以及获得功能的结果状况,都是其接口上没有写的。这导致了非常复杂的,持续的代码修改。因为任务的内容是时常会变化的。所以这个时候,我们就需要重构整个架构。把架构从一字排开的设计,修改成可以多个层次互相调用的模块。这些模块之间的接口,有面向客户端的也有面向其他模块的,这样我们就能直接调用那些现成的功能,组合开发出更复杂强大的功能。不管任务系统如何变化,我们都可以不用重写那些已经实现的功能,这让整个系统成为可以应对这种需求变化的关键。这就是一个分层架构的实例。

 

实例2:想象一个场景,用户发出了一个请求要获得客户的信息。在这个例子中,用户信息由客户数据和订单数组组成(客户下的订单)。用户界面只管接受请求以及显示客户信息。它不管怎么得到数据的,或者说得到这些数据要用到哪些数据表。如果用户界面接到了一个查询客户信息的请求,它就会转发这个请求给用户委托(Customer Delegate)模块。这个模块能找到业务层里对应的模块处理对应数据(约束关系)。业务层里的customer object聚合了业务请求需要的所有信息(在这个例子里获取客户信息)。这个模块调用持久层中的 customer dao 来得到客户信息,调用order dao来得到订单信息。这些模块会执行SQL语句,然后返回相应的数据给业务层。当 customer object收到数据以后,它就会聚合这些数据然后传递给 customer delegate,然后传递这些数据到customer screen 展示在用户面前。

 

优点:

       1.结构简单,容易理解和开发

       2.不同技能的程序员可以分工,负责不同的层,适合大多数软件公司的组织架构

       3.每一层都可以独立测试,其他层的接口可以模拟解决。

缺点:

       1.一旦环境变化,需要代码调整或增加工能时,通常比较麻烦和费时

       2.部署比较麻烦,即时只修改一个小地方,往往需要整个软件重新部署,不容易做持续发布。

       3.软件升级时,可能需要整个服务停止。

       4.扩展性差,用户请求大量增加时,必须一次扩展每一层,由于每一层内部是耦合的,扩展会很困难。

 

以上是关于系统架构:分层架构的主要内容,如果未能解决你的问题,请参考以下文章

软件架构设计-软件架构风格分层架构

软件架构设计-软件架构风格分层架构

系统架构:分层架构

标准Web系统的架构分层[转]

标准Web系统的架构分层

软件架构之分层架构理解