分层单体架构风格是分层思想在单体架构中的应用,从技术角度着重于职责的分层。同时,基于不同层次的不同变化率,可以在一定程度上控制变化在系统中的传播,有助于提高系统的稳定性。但是,这种从技术角度而不是业务角度的关注点分离导致了问题域和工程实现之间的差距,这种分离会导致系统认知复杂度的增加。1.经典单体分层架构1.1四层单体架构风格经典的四层单体分层架构如下图所示。应用在逻辑上分为表现层、业务层、持久层和数据存储层。各层职责如下:表现层:负责向终端用户展示信息,接受用户输入触发系统的业务逻辑。用户可以是使用系统或其他软件系统的人。业务层:专注于系统业务逻辑的实现持久层:负责数据的访问数据存储层:底层的数据存储设施这种分层的单一架构可能是大多数开发者最早也是最熟悉的应用架构风格。特点是:层与层之间的依赖从上到下直接依赖于每一层,每一层都是封闭的,请求的数据流必须严格从上到下穿过每一层,不能被穿透传递。关注点分离:系统的关注点通过分层垂直分布。每一层只关注自己层边界内的职责,层与层之间的职责相互独立,不存在重叠。例如,业务层负责处理系统的核心业务逻辑,而持久层则专注于数据的访问。除了关注点的维度隔离,分层还隔离在“变化”的维度上。每一层的变化率不同,从低层到上层递增,表示层的变化率最快,数据存储层的变化率最低。通过严格的层依赖约束,最小化下层变化对上层的影响。此功能的上下文是层依赖于抽象而不是具体性。当实现改变但接口契约不变时,改变的范围仅限于当前层。但是,如果改变了接口契约,可能会直接影响到上游依赖层。这种分层架构风格优势明显:分层模型相对简单,理解和实现成本低,开放人员接受度和熟悉度高,认知和学习成本低是:层间数据效率问题:由于层间调用关系的依赖约束,层间数据传递需要额外的成本服务是从可重用性的角度来考虑的。在如下所示的五层架构中,通过引入中间层解决了可重用性问题。将业务层的共享服务存放到通用服务层,以提高可重用性。其特点是:引入通用服务层,提供通用服务,提高复用性。通用服务层是允许调用链路穿透的开放层。业务层可以根据需要直接访问下层持久层。与四层架构相比,五层架构的主要优点是中间层的引入在一定程度上解决了系统的复用性问题。但从逆向来看,正是中间层的引入导致了以下问题:中间层的引入降低了数据传输的效率,增加了开发和实施的成本,增加了系统混乱的风险:由于通用服务层的开放性,业务层可以通过调用。但这种需要渗透的场景并不能形成统一的判断原则,往往要靠实施者的个人经验来取舍。同样的业务场景,不同的开发者实现,可能会有不同的判断结果(在四层架构下,放开层间调用约束也会存在这个问题)。随着业务需求的迭代,系统依赖会日益增加,最终形成复杂的调用关系,这也会导致系统复杂度的增加,增加团队成员的认知成本。2.单层架构常见问题的讨论当然,也正是因为它的接受度高,人们对分层产生了误解,认为分层是必然的“默认选项”,从而忽视了分层的层性。分层解决什么问题?层级本质上是一种处理复杂性的方式:将复杂性抽象到不同层次,通过层级划分职责,从而降低认知成本。同时,通过分层形成的“屏障”,控制系统间变化的传播,提高系统稳定性。无论是四层架构还是五层架构,都是单一应用架构风格下分层思维的实践。这种分层模型的内在问题主要体现在以下几个方面:分层对系统复杂度和效率的影响影响变化真的可以完全隔离吗?问题域和解决方案的隔离2.1分层对系统复杂性和效率的影响如上所述,分层架构中的每一层都以不同的速度变化。越高,变化越快,稳定性越低;变化越低,变化越慢,稳定性越高。例如,表现层的用户展示逻辑可能会经常变化,不同场景展示的数据和形式可能不同。如果划分的层次越多,层间依赖越严格,系统的调用环节和依赖关系就会更加清晰。但是,请求和响应之间的链接越长,层间数据转换的额外成本就越高。即使引入了各种数据转换工具,比如MapStruct,实现起来还是会感觉非常繁琐和重复。如果层数较多,层与层之间的依赖松散,允许跨层调用(如下图从表现层调用持久层只是一个提示),频繁的数据转换成本可以降低到一定程度上。但是:第一:如何判断是否跨层调用,很难形成统一严格的判断标准,只能做粗粒度的划分。因此在执行过程中会出现不同的判断结果,系统的调用关系会随着代码量的增加而变得越来越复杂。当然,团队可以加强codereview的粒度,每次review都根据是否穿透call进行讨论、判断并达成共识。但实践经验是,由于人为因素,严格的codereview并不能保证决策的一致性。第二:如果允许跨层调用,就意味着“模型”的穿透,下层的模型会直接暴露给上层,这与组件的内聚和我们对模型的封装是冲突的追求。注意:层间依赖约束是一种架构决策,可以通过自动化的单元测试机制来保证。具体可以参考:《基于ArchUnit守护系统架构》《轻量级的架构决策记录机制 - ADR》2.2变更的隔离我们对分层有一个笼统的、“先入为主”的理解,它可以隔离变更。第一个想到的例子,比如底层数据库发生变化,或者ORM框架发生变化,那么我们只需要修改DAO层的实现,而不需要改变上层业务层的代码。你真的要更换数据库吗?你真的会取代ORM框架吗?有可能,但概率很低,大多数系统都不会发生这种情况。如果发生替代真的能隔离吗?如果你的层不依赖于抽象,而是依赖于具体的,那么隔离是不可能的。即使层依赖于抽象,变化真的是孤立的吗?实现变更的直接结果是依赖方需要引用新的实现,这个变更也会影响到上层。只是这种变化可能会交给IOC容器,但是这就是变化隔离吗?如果需要将新字段添加到表示层,但不在当前数据库模型中怎么办?如果需要在数据库中增加一个新的字段,但是表现层和业务逻辑层不关心?如果是...那么,系统变更的原因有很多,不同的场景,不同的业务需求,对变更的隔离程度不同:分层可以控制变更在系统中的传播,由于变更场景的多样化,分层不能完全隔离变化。2.3问题域与解决方案的分离重新思考上面提到的分层单体架构的一个特点:焦点隔离,表现层、业务层、数据访问层、存储层等各层各司其职。这个焦点的本质是什么?从技术角度隔离!!!每一层都从技术角度而不是业务领域角度隔离技术问题。技术视角对研发友好。作为开发者,自然可以理解并接受这种技术维度的统一语言:DAO层只负责处理数据相关的逻辑,Controller层的服务与RestfulAPI相关,RPC层只处理外部系统。跨进程调用等等。对于非常核心的业务概念,比如以订单为例,在单体层次架构下,需要回答这样一个问题:“订单组件”在哪里?在经典的分层单体架构风格中,典型的实现如下图所示:OrderConroller:Spring技术栈下系统访问的Rest接口OrderService/OrderServiceImpl:订单的核心业务逻辑实现服务,比如下单和取消订单等价逻辑OrderDAO/OrdeDAOImpl:订单数据的访问订单组件不作为一个单一的、内聚的东西存在。它的组成元素OrderService和它依赖的OrderDAO分散在不同的层次。因此,该模式下的订单组件只是逻辑的、概念的存在。订单组件作为业务领域的核心抽象,在代码实现上并没有真实、直观、内聚的体现。我们在项目代码库中寻找“顺序组件”:首先,我们在项目的顶部首先看到的是从技术角度来看的Module(MavenModule):web、service、dao然后,我们需要通过导航每一层都可以一窥全貌。有了IDE的支持,这种导航就不会很复杂了。但问题的根源在于:认知成本的增加。我们在了解系统的时候,自然是从业务领域而不是技术领域出发。单体分层正是来自技术领域,而不是业务领域。这种差异导致了业务域和实现的分离,增加了系统的识别度。知道成本。实现需要反映抽象。组件思维本质上是一种模块化思维。通过内聚和封装,将问题空间拆分成子空间,分而治之。通过接口对外提供组件能力,屏蔽内部复杂性。需要权衡接口契约的大小和粒度。粒度越小,能力提供越集中,理解和获取成本越低,但通用性越低。接口契约的粒度越大,通用性越强,但理解和访问的复杂度也越高。将组件思想应用于单层架构导致模块化单架构风格。应用程序架构按问题域模块化组织,而不是基于技术问题进行拆分。组件遵循内聚原则,内聚原则包含实现组件功能所需的所有元素和交互关系。组件通过统一的接口契约以适当的粒度进行交互,不直接依赖于组件的内部能力或模型。同时,一个组织良好的模块化单体应用架构也是微服务拆分的重要保障。如果不能在单体架构中进行优雅的模块化组织,何谈合理的微服务拆分?3.结束语单体分层架构风格是分层思想在单体架构中的应用,着重从技术角度对职责进行分层。同时,基于不同层次的不同变化率,可以在一定程度上控制变化在系统中的传播,有助于提高系统的稳定性。但是,这种从技术角度而不是业务角度的关注点分离导致了问题域和工程实现之间的差距,这种分离会导致系统认知复杂度的增加。将组件化思维应用于单体的层次化架构,将模块化单体技术视角的分层拉回业务领域视角的模块化,一定程度上降低了业务与工程实现之间的隔离。良好的模块化是单体走向微服务的重要基石。如果一个模块化设计不好的系统,不仅会增加微服务拆分的成本,更重要的是会增加形成分布式单体的概率和风险。
