今日内容
上一篇文章的末尾我们留了一个问题:
“在传统分层中,处于越下方的层次,就应该越稳定,而传统分层中最下层是数据层。数据层随着业务的发展,是非常有可能升级、换框架乃至换底层存储的。如果哪天数据层要更新,难道要整个系统重构吗?”
这其实只是系统分层中的一个问题。我们今天以这个问题为引子,来聊聊系统分层的原则。
原则1:低层次修改对高层次透明空说没有体感,我们从一个反例开始
反例时间
假设我们有一个用户查询服务,通过用户id查询用户信息
我们的设计如下所示(按照我们上一篇文章说的典型分层方式):
下图是具体在代码中的目录结构:
接下来,我们看下具体的代码:接口层:UserController
服务层:UserService
数据层:UserDAO
从例子中大家可以看到,数据库的访问使用的是直接撸jdbc的方式来做的,显然这种方式已经过时了。
某一天架构师大喊一声:“我们要使用mybatis来替换掉这种jdbc的数据库访问方式”。
于是,我们可能会这样修改代码:
首先先添加一个mybatis的数据层:
mybatis数据层的实现:
UserService的修改:
到这里你有没有发现一个问题。那就是数据层DAO的替换居然影响了业务Service层!
所以,这里也就引出了我们的第一个原则:【低层次修改应该对高层次透明】
那要怎么达到这个效果呢?就是遵循一个非常重要的设计原则:【依赖抽象而非具体】
我们可以把最初的设计进行如下修改:
UserDAO代码修改如下:
UserService代码修改如下:
需要替换Mybatis框架时,只需要
【1】使用mybatis实现数据层逻辑。
【2】将JdbcUserDAO的@Repository注解去掉。
spring就会自动为UserService装载MybatisUserDAO了
事实上,真实的情况会比例子更加复杂。例如在切换DAO层时,往往会涉及灰度切流或者双读的情况。这个时候中间可以再加一层代理,来控制新旧DAO的使用方式,如下图:
到这里,相信你已经明白【依赖抽象不要依赖具体】的含义和具体用法。
事实上,这个原则可以说是软件系统设计中最重要的原则。
我们常提“高内聚低耦合”,这个原则可以帮助我们尽量地降低耦合。
另外,不仅仅是在数据层,任何层次之间的依赖都应该遵循这个原则,大部分模块的设计其实也应该遵循这个原则。
这个原则以及解题思路也回答了上一篇文章留下来的“依赖不合理”问题。
原则2:不要跨层依赖
系统分层要求调用只能是上层调用下层。在这个大准则下,其实又有两种分层依赖模式,我们称之为【严格分层架构】和【松散分层架构】。
【严格分层架构】:每一层只能调用其下面一层提供的能力。
【松散分层架构】:每一层可以调用处于其下方所有层次的能力。
我们可以结合下面的图示来理解:
这两种分层架构方式各有利弊:
其实从图示你也可以看出,我更推荐【严格分层架构】。虽然在一些情况下,遵循严格的规范会带来一些麻烦,但是其带来的好处还是更多的。图中列了几项【严格分层架构】的好处,其中我觉得最重要的还是第2点,那就是对业务逻辑和数据的保护。
举个例子,如果用户信息表中有保存身份证号,那么领域层可以对身份证号做脱敏再向上返回。如果我们允许接口层直接访问数据层拿用户信息,那身份证号就很容易泄露。
在整个分层架构中,每一层除了提供向上的能力,也提供向下的保护。这一点往往很多同学没有意识到。
原则3:确定每一层的边界
无论你是否根据传统分层方式对系统进行分层,在分层之后,要确定每一层的边界。所谓的边界是指:不能做什么。
例如在传统分层方式中:
【接口层】:对出入参仅做格式上的校验,不能涉及“例如用户是否在黑名单中”这样的校验。
【服务层】:负责编排流程、处理rpc请求、控制同异步。不能涉及领域概念。
【领域层】:针对领域规则来实现具体的能力,不能跨领域。
【数据层】:仅对数据做CRUD,不能涉及对数据的额外加工。
那,为什么要强调边界呢?
【防止冗余】:例如计算支付手续费的逻辑没有内聚在领域层,就等于存在冗余逻辑,如果以后修改就有冗余的工作。更危险的是,容易漏。
【便于拆分】:微服务盛行的当下,拆分服务司空见惯。
【便于替换】:例如数据层要换框架,如果数据层还涉及业务逻辑,就变得复杂多了。
今日总结
讲到这里,我们就把系统分层的设计原则讲完了。