一种简单的系统分层架构模式

本文描述一种简单、具备一定通用性、足够具体的软件系统分层架构,满足团队一般性的软件设计需求。换句话说,在日常需求开发的设计、编码阶段,开发人员可以套用这个架构模式,或者可将它作为参照。

该架构的灵感来源有三:**传统MVC分层理论,简洁架构模型(《架构简洁之道》),领域驱动设计理论。**之所以需要单独提出这么个架构,是因为没有现成的可直接复制;上面提到的几个架构理论,无疑都是很有价值的,但是过于抽象,难以直接落地。另一方面,一个合适的架构设计,需要考项目类型、用户规模、团队构成、现有系统状态…等诸多因素,这里所谓通用,也仅仅是对我们团队而言。

1、简洁架构模型

在这里插入图片描述

这是《架构简洁之道》提出的通用架构模式,这个模型最大的亮点有二:

  1. 明确了分层规则:离IO越近的模块,越靠近外层,反之约靠近内层;业务逻辑在内,技术机制在外;
  2. 阐述了内外层的关系:内层策略虽然依赖外层机制来实现,但内层代码不能依赖外层代码,需要某种适配机制(也就是DIP)。

个人非常赞同这个架构模式,本文要叙述的所谓“简单分层架构模式”,本质上就是对这个架构进行简化、具体化。

2、简单分层架构模式

在这里插入图片描述
上图,就是本文要提出的简单分层架构模式示意图,可以和上一节的图做个比较:该图的上层对应上图的内层,该图的下层对应上图的外层。另外,箭头代表不同层之间运行时的调用关系,箭头上的数字代表调用顺序。

这是一个技术框架极为丰富的年代,强大的技术框架帮我们做了很多事情,因此简单分层架构模式做了两处主要的简化:

  • 角色比较少,强大的技术框架身兼数职,我们的工作自然轻松很多;
  • 弱化了DIP原则,代码的依赖关系与运行时调用关系基本一致,原因有二:
    • 几乎所有技术框架都是按DIP原则设计的,所以只要你遵守规则,结果就不会太差;
    • 应用系统对模块可重用性的诉求相对较低。

接下来,我们逐层介绍这个模式,每个章节分别介绍一层,内容涵盖:职责,角色,命名方案。其中命名方案是我们团队所采用的,有一定的特殊性,仅供参考。

2.1 业务模型层

职责:实现业务逻辑

说得简单一点,就是实现需求文档上描述的那些业务概念和业务规则,以及必要的领域逻辑;对于这一层的准确把握需要一点DDD(领域驱动设计)的知识。

角色:业务实体和业务服务,

业务实体是实现某种业务概念的,一般需要某种持久化机制支撑,而业务服务则实现了业务规则。
这一层的设计实现容易犯两个错误:

  • 用ORM对象替代业务实体

由于业务实体需要持久化,自然就会用到某种ORM机制,而ORM机制肯定要求我们编写存储实体字段的ORM对象。但我们要明白,ORM对象的设计目的是为了读写数据库,一般只有setter&getter方法。而业务实体对象是用来做业务计算的,它的方法应该有丰富的业务语义。诚然,在业务简单的情况下,把ORM对象当做业务实体用用倒也无妨,关键要心中有数,当业务变得复杂时知道怎么调整。

  • 将本属于业务服务的方法放到了业务实体

从单一职责的原则出发,业务实体不应该包含复杂的业务规则,它应该专注于维护业务实体的状态,保持状态一致性、有效性。尤其有很多的业务规则会涉及不止一个业务实体,放到哪个业务实体都不合适。

打个比方,订单Order是一个业务实体,但是生成订单和订单状态变迁一般涉及很复杂的规则,放在业务服务内更合适。

业务模型层是非必需的

只有当业务规则复杂的情况下,才需要独立的业务模型层。一些仅限于CRUD的功能模块,它的逻辑仅限于字段校验,确实没有独立出来的必要。不过当业务有一定深度时,这一层是非常重要的,否则这些业务规则就会夹杂在下层代码中,非常难维护。

命名方案

由于下一层(功能服务层)也包含服务角色,为了以示区别,业务模型层的服务我们使用后缀Logic,比如订单业务服务叫做OrderLogic。而业务实体则基于业务概念来命名即可,但有个小麻烦:同一个业务概念,它有数据库记录形态,也有业务实体形态,此时我们将前者命名为xxxDO(Data Object),后者命名为xxxBO(Business Object)。

2.2 功能(UserCase)服务层

职责:实现功能用例

它定义了某个模块所具备的具体功能,这里的功能是与UserCase相对应的,比如给用户发邮件、推送一个广播等等。为了实现UserCase,功能服务需要依赖下层的技术框架,还有可能依赖其他服务,或远程调用。

有独立的功能服务层,最大的好处是方面重用,比如“给用户发邮件”:用户通过前端界面可以发送邮件,系统其他模块在特定场景下也需要发送邮件。

功能服务的接口设计非常重要,两个核心要素:

  1. 从UserCase的角度来设计接口,方便上层和其他模块调用;
  2. 不要混入技术框架层的元素,避免拿HttpRequest当接口参数类型;

命名方案

一般基于模块命名,加Service后缀,比如OrderService,MailService。

几乎是必需的

如果一个模块,实现了任何有用的功能,这一层是必不可少的。即使比较简单,也不要将这一层的代码并入下一层(交互控制器)。

2.3 数据适配层

职责:负责业务数据读写

这一层的职责比较明确,就是负责业务数据的读写,屏蔽存储细节。一个数据适配对象内部可能使用一种或多种存储机制:ORM框架(Mybatis),Redis,文件读写,甚至远程数据访问接口,从分层角度看,这些都是技术细节而已。

命名方案

基于模块命名,加上Repository后缀,比如OrderRepository,MailRepository。

在我们的项目里,由于配置数据普遍,有一类特殊的数据适配层对象被命名为xxxConfig。

几乎是必需的

除非模块不需要持久化数据

2.4 交互控制器

职责:处理外部信号

这里的外部信号是泛指,包括了网络请求、异步消息、内部事件等。交互控制器对应MVC中的C,往往与某个具体的技术框架关联。

交互控制器一般干两件事:

1、将来自外部的信号转化为内部功能执行,典型的方式是:对外部参数进行解析之后,转发给Service层;
2、将执行结果打包成所需的协议格式,作为对外部信号的响应。

设计编写这一层的关键是:和功能服务层之间做好职责区分,一方面不要将外部请求参数的细节转嫁给后者,另一方面也不要越俎代庖,直接实现业务功能。

这层有点类似人体的感觉器官,命名为“控制器”还是受MVC理论的影响,团队成员容易接收。

命名方案

基于模块和技术框架命名,如果你用的框架Spring MVC,自然加上Controller后缀,如OrderController;如果是内部事件处理器,加上Handler后缀,如GameEventHandler。

是否必需

取决于这个模块是否为客户端、其他系统提供访问接口,是否需要处理内/外部事件。

2.5 展示器

职责:生成展示数据;

很多情况下,功能服务层的执行结果并不适合直接返回给客户端,因此需要先转换成展示器对象,再传输。

对服务端而言,如果前端是Web页面,那展示器生成ModelView;如果前端是APP,展示器则生成DTO对象(方便打包成json格式)。

命名方案

假设前端是APP,那么展示器对象使用后缀DTO(Data Transfer Object);DTO对象一般基于业务实体对象构造,将数据格式转换成客户端所需要的形式。

非必需

对于APP这样的富客户端来说,某些UserCase下,只需要服务端返回简单的执行结果,自然不需要什么展示器,此时在控制器里面设置一个独立的生成响应消息的方法即可。

注意:尽管富客户端具备强大的计算能力,但我们的设计理念是——客户端展示的字段尽量由服务端生成,避免前后端产生歧义。(依据真理的单点性原则,《Unix编程艺术》)

2.6 调用关系

最后捋一下这些层之间的调用关系,如本节示意图上的箭头和数字编号所示,一次典型的网络请求处理流程如下:

  1. 技术框架,从底层接受到网络请求,生成HttpRequest,回调到交互控制器——Controller的处理方法;

  2. Controller解析请求参数,然后调用功能服务层——Service的接口;

  3. Service,执行对应的功能,可能需要以下几个步骤

    1. 从数据适配层——Repository拉取所需的记录数据;
    2. 调用业务逻辑层,实施业务规则;
    3. 可能还需要调用其他Service甚至外部服务
    4. 业务实体可能被修改了,Service再调用Repository将数据写回;
    5. 从内部视角,业务功能就算执行完了,如果需要生成内部事件、触发其他Service调用,也一并执行;
  4. 将业务执行结果返回给Controller;

  5. Controller调用展示器(如果有的话)生成响应数据;

  6. Controller将打包后的响应数据通过技术框架再返回给客户端。

2.7 跨层调用

按照分层理论,层与层之间的依赖和调用关系应该被严格限制:

  • 上层代码不可以依赖下层代码(如果有调用方向上的依赖,必需通过DIP来解决)
  • 只有相邻的层之间允许互相调用,跨层的调用是不允许的

在实战中,我们对不同类型的对象,采用了不同的策略。对于数据结构对象(比如数据实体、数据库记录),我们并不限制它们的作用域;而对于非数据结构对象(service,repository,logic,controller)的使用,则要严格控制范围。

最需要警惕是技术框架所引入的对象类型,比如Controller,Request等,我们要尽量将它们锁定在特定的适配层中,不要污染系统其他部分。这样做的核心原因有两个:

  1. 降低技术框架替换或版本升级的难度;
  2. 技术框架缩所引入的对象类型很通用的,一旦离开具体的上下文,其含义就会很模糊,难以理解。

3、技术框架

现在的软件开发都离不开技术框架,技术框架有几个重要作用:

  1. 抽象了某个领域的常用功能,避免重复造轮子;
  2. 屏蔽了底层技术细节和平台差异性;
  3. 承担了Main函数的职责,驱动系统运行,我们的业务代码更像是框架的插件。

后端常用的技术框架不知凡几,常见的有Spring,Mybatis,Netty,Dubbo等,关于技术框架我遵循了若干使用原则。

框架不是架构

随着框架的版本更迭,它的功能只会越来越多,并试图说服你围绕它来设计系统。但作为使用者,我们不要上它的贼船,按照我们自身的需要,使用框架最本质、最成熟、最稳定的部分即可;尽量避免与框架深入绑定。

框架是外层技术细节

Web容器和数据库虽然差别很大,但本质上都是实现了某种IO能力的底层技术,从系统架构角度来看,它们位于类似的位置。

当我们说一个东西是“技术细节”,并不是说它不重要,更不是贬低它,而是指它在架构中的逻辑位置。

降低框架的侵入性

由于框架是技术细节,按照DIP(依赖反转)原则,我们极力避免将底层技术框架的对象引入上层业务逻辑,因此数据适配层屏蔽了ORM框架,交互控制器屏蔽了Http框架。

将框架依赖限制在局部,让我们将来更容易替换它。虽然某些核心框架(如Spring)在软件系统生命周期内被替换的可能性很低,但版本升级的可能性可不小,升级本质上也是一种替换。

4、简单示例

以常见的“用户下单”功能为例,展示各个层次的典型设计和写法。

业务模型层

三个业务实体(订单,用户,商品),一个值对象(地址),一个业务服务(订单逻辑)。

//实体对象Order,代表:订单
class Order {
	long userId;
	String productId; //商品id
	Time orderTime; 
	Address address
	...
}

//实体对象User,代表:用户
class User {
	long userId;
	String phone;
	Address address;
   ...
}

//值对象Address,代表:地址
class Address {
	String country;
	String province;
	String street;
	String mailCode;
	...
}

//实体对象Product,代表:商品
class Product {
	String productId;
	long price;
	long supply;
	...
}

//业务服务OrderLogic,暂时只有一个下单操作
class OrderLogic {
	Order makeOrder(User user, Product product, ....) {
		[check order rule]
		[create order object]
		return order
	}
}

数据适配层

包含三个对象,分别管理三种实体:用户、商品、订单的持久化。

class UserRepository {
	User getUser(long userId);
	...
}

class ProductRepository {
	Product queryProduct(String productId);
	void updateProduct(Product product);
}

class OrderRepository {
	Order queryOrder(long orderId);
	void insertOrder(Order order);
}

功能服务层

一个对象——OrderSerivce,实现下单case:它调用userRepository和productRepository查询所需数据,调用orderLogic生成订单,通过orderRepository插入订单;并通过productRepository修改商品库存。

class OrderSerivce {

	Order makeOrder(long userId, String productId) {
		user = userRepository.getUser(userId)
		product = productRepository. queryProduct(productId)
		order = orderLogic.makeOrder(user, product)
		if order success:
			orderRepository.insertOrder(order)
			product.minusSupply
			productRepository.updateProduct(product)
		return order
	}
	
}

实际场景中,如果涉及到并发安全性问题,一般也在这一层解决。

交互控制器

交互控制器对象一般基于技术框架实现,request和response参数都是框架注入的:

class OrderController {

	void makeOrder(Request request, Response response) {
		long userId = request.getParamUserId
		String productId = request.getParamProductId
		order = orderSerivce.makdOrder(userId,productId)
		
		dto = new OrderDTO(order)
		response.append(dto)
	}
	
}

展示器

展示器定义了OrderDTO,以前端所需的形式展示下单结果:

class OrderDTO {
	long userId
	String productId;
	String productInfo
	String addressInfo
	...
	
	OrderDTO(Order order) {
		...
	}
}

5、变化与扩展

随着业务场景的变化,我们可以将”简单分层架构“作为一种参考,灵活地运用。

对于及其简单的业务需求,可能一个Controller(控制器)+Respository(数据源)就够了,根本不需要独立的Service,更不需要建什么业务模型。此时,Controller这个类把所有的活都干了,但我们仍可以在方法级别按分层理论来拆分,比如:一个方法做协议数据打包,一个方法做业务逻辑,一个方法做数据读写。

反过来,是否存在极为复杂的业务场景,导致我们需要更多的层次?我个人的答案是:几乎不需要。如果这些层次还解决不了你的问题,更多的层次只会把问题复杂化;此时需要的可能是横向拆分:将一个模块拆分成多个模块。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值