浅谈业务系统模块化设计-分层

 

1. 前言

互联网产品迭代速度极快,人员变更频繁,对底层承载业务的系统带来的影响则是:一个系统可能会有成百人在改造它,一个系统糅合了 N 多人的编程思想,然而每个人的编程风格不同、设计理念不同,这些差异性会导致系统的可读性变差、可维护性降低,新增业务的成本变高。 耳闻通天塔的故事,上帝为了不让人民齐心协力、统一强大而改变了人类的语言,让各地人民使用不同的语言,增加其沟通的成本,最终通天塔半途而废。所以本文描述了一种普适的、通用的系统架构设计方案,还原系统原本的样子,让程序猿在系统设计的最基本问题上达成共识,更大程度的增强系统的活力,延缓系统的腐朽速度。

2. 模块化

在讲架构分层之前,我们先聊一聊模块化的概念,因为模块化是架构分层的理论基础和最终目标,架构分层是模块化的一种表现形式。

将一个程序分割到不同的组件中,在程序内部创造一些定义良好的、有文档描述的边界,这可以某种程度上减少它的复杂性,这是模块化设计的初衷。

模块化将程序划分为一些模块,每个模块可以单独编译、但又与其他模块有联系;模块内部的细节只对模块内部的代码可见,而其他代码只能看到模块明确公开的部分。模块化的总体目标是允许模块独立的设计和修改,从而减少软件的维护成本。我们发现模块化和面向对象提供的功能比较类似,但是它们从不同的粒度层次来实现这些功能,如图。

模块化的概念由来已久,各种语言、架构在不同粒度层次对模块化提供了技术支持。以 JAVA 为例,其自带的Package 机制提供了最小粒度的模块化,但是只做到了类级别可见性的封装;JAVA9 引入 module 的声明,实现了更高级别的模块化,但仍然局限于类可见性的封装;古老的 OSGI 技术通过 bundle 的方式实现了服务级别的隔离和通信,但是它是单进程的,JVM crash 会导致整个应用服务的终止。目前我们最常见的是应用级别的模块化,比如一个支付系统划分为支付核心、充值核心、结算平台、支付网关等子系统,每个子系统只关注特定领域的业务,借助分布式技术,对外开放自己的服务能力。如图:

我们不可能将所有不同的领域问题,都在应用级别做拆分实现,这会导致应用数量的极度膨胀,更倾向于按照系统的能力(平台级别or纯业务级别)、重要性(是否在核心链路)等维度做应用拆分。例如针对营销业务,营销的玩法很多,例如秒杀、砍价、团购、优惠券;我们并没有单独创建秒杀系统、砍价系统、团购系统,而是将秒杀、砍价、团购抽象成一个公共的活动,构建了一个营销核心系统,能够同时承载秒杀、砍价、团购业务;针对优惠券,由于其业务的复杂性和数据的敏感性,单独构建了优惠券应用。本文所介绍的分层方法,聚焦于单体应用系统的模块化设计,降低系统复杂度、提升系统的可维护性。

3. 架构分层

系统的演进是一个从简单到复杂的过程,我们都是从MVC初识网络系统设计,Controller、Service、DAO 组成了最基本的web系统框架,后来逐步引入了 RPC 接口、消息、分库分表、事务、NoSql 等;不仅技术革新日新月异,系统融合的业务量也同时在膨胀、业务复杂度逐渐提升。大家都在思考如何做模块化、如何保持系统的表达能力和生命力,这里介绍一种比较成熟的分层策略,期望为系统模块化的设计提供参考和建议。

3.1 服务层功能解释

3.1.1 外部服务层

外部服务层集合了最常见的三种系统对外能力输出的形式:WEB、PRC、MQ。

  • web 层封装了对外提供视图能力的 controller 逻辑、view 信息。

  • common-facade 是系统的服务门面,声明了所有对外暴露的 RPC 接口和数据模型;这里仅仅是声明,并没有具体业务实现。

  • message-callback 封装了消息有关的处理逻辑。

3.1.2 原子服务层

原子服务层封装了业务的最基础服务,每个原子服务只负责处理各自领域内的问题,服务之间低耦合。

  • 原子服务的粒度需要把控,既然是原子服务,其承载的业务就得足够单一,但对外输出的接口并不单一;接口太单一的原子服务实际上也没必要存在。

  • core-model 封装了系统的核心业务模型,不能向外部系统暴露其核心业务模型。如果说代码=算法+对象,那么系统=模型对象+服务,服务是系统中静态的沟壑和桥梁,模型对象是流通在服务中的水和货物,足见业务模型的重要性。

3.1.3 依赖服务层

依赖服务层封装了所有对外部系统的引用。

  • common-dao 特指对关系型数据库的服务依赖。常用的持久化技术很多:mysql、hbase、redis 等等,但是只有关系型数据的操作最为复杂,尤其涉及事务、分库分表技术。-common-dao 包括数据的各种操作接口和数据的 class 定义(常说的数据库DO),hbase、redis 等依赖放到 common-integration 层处理。

  • common-integration 封装了对外部 RPC 依赖的处理操作,包括基本的异常处理、模型转换、日志打印等逻辑。模型转换包括核心模型组装 RPC 请求对象、RPC 返回结果组装核心业务模型,但非强制性的转换。有些 RPC 结果对象转换成核心业务模型的成本太高,而且使用频率较低,那可以直接在 core-service 中使用,但是要严格控制对象的生存范围,因为这类对象会极大的污染系统的健康度。

3.1.4 公共服务层

公共服务层比较简单,只是一个概念,可以以各种形式存在,比如 common-util 模块、比如 biz 层和 core 层自定义的 common-util 包。

  • common-util 是公共的业务处理单元,很多模块都可以直接引用它;主要封装一些业务无关的工具类,比如日期工具类、金额工具类等。

  • common-util 模块不应该出现 service 服务,一般以静态方法的形式输出功能。

3.1.5 业务服务层

最后再来说下业务服务层,因为该层的设计可选择性较多,并不是必须的。

  • 业务服务处封装了 RPC 接口的实现和通用的业务处理逻辑。举个栗子来解释通用的业务处理逻辑,如下图所示,这里有一个比较核心而且复杂的服务被多个上游接口调用,通过组装多个原子服务来实现其功能。

理论上只有 biz 层才真正的实现业务,RPC、WEB、MQ 只是业务的对外输出方式,原子服务关注的业务领域粒度较小。但是现实当中,只有一些核心系统才会有功能聚合性这么高的业务场景,而核心系统毕竟是少数,一个业务领域估计就有一个,比如交易核心系统、支付核心系统,绝大部分都是边缘的业务系统;边缘业务系统的biz 层设计时就退化为了纯 RPC 服务实现,WEB、MQ 处理器可以直接组装原子服务实现业务功能。

3.2 模块化粒度问题

我们以 Dubbo 为例,看下中间件系统的模块化策略(Dubbo 部分项目结构如图)。与业务系统相比,中间件系统的模块化粒度更细,几乎单独打包了每个功能,即使一个功能由多种实现,每种实现也被分离成了单独的模块,比如dubbo对接的各种 rpc 协议:http、dubbo、redis、memcached 等,每个协议客户端实现都是单独的模块。

业务系统的分层粒度会更粗糙一些,但也要根据实际情况做取舍。以下图一个业务系统的项目结构为例:common-integration、common-dao 没有被编译成独立的模块、而是以 package 的形式存在;该业务系统有个比较复杂而完整的规则引擎 rule,被提取成单独的业务模块;非核心系统淡化了 biz 层的设计,也就没必要单独设计一个 biz 的 module,也是以 package 的形式存在;core-service 中承载的服务能力在增强,不再只涵盖原子服务,发展成为了核心服务层。但是,无论系统最终的模块化形态是怎样的,上述所描绘各服务层的概念一定会存在,区别只在于你是显式 or 隐式的实现。

|- common-facade
|- message-callback
|- biz
|- core-service
|---- common-dao
|---- common-integration
|- core-model
|- common-util
|- rule

4. 总结

本文首先对模块化做了简要介绍,然后描述了一种架构分层方法,期望能给大家在设计系统中提供灵感和思路。分层的目标是提高系统的可维护能力,提高可维护能力的方法还有很多,比如代码重构、架构替换、模型设计、语言选择等;分层思想分隔了业务范围、理清了业务框架,各种业务在不同的领域模块中飞速成长,但又遵循一定的规则约束,使得系统有序发展。 这就像在一块蛮荒之地做规划,我们根据实际地理情况划定了区域范围和各自区域适合承载的业务,比如区域 A 土壤肥沃适合建造农田、区域 B 靠近河流适合建造工厂、区域 C 地块平坦适合建造人群定居区等。有了这些规划之后,才能避免在盐碱地搞种植、在山坳里建工厂等需要高维护成本的事情发生。

I n