软件在本质上是复杂的,软件本身的复杂性在于除了要解决问题域,还要解决非功能性需求和软件域特有问题:安全性、可用性、可维护性、可扩展性、性能、一致性、容错性、稳定性、可重用性、幂等、兼容等等,软件开发者的任务就是制造“简单”的假象。如何组织复杂的系统?把复杂的事物分解到不同的层次中,层次代表了不同级别的抽象,一层构建于另一层之上,每一层都对上层屏蔽内部复杂度。
一 为什么使用RDD?
在RDD中,我们认为“软件对象具有职责”,这个定义很符合人在社会群体中分工协作的方式,软件也是人编写的,所以根据职责思考设计的软件系统符合人的行为习惯,同时更易于理解和管理。在微服务架构中不同系统由不同的组织和人负责,把系统当作对象(人),系统提供的接口就是对象(人)的职责。
职责驱动设计的核心是考虑怎样给对象分配职责,其适用于大到系统、小到对象等任何规模的软件。职责分配的本质是分工,劳动分工是劳动生产率提高的主要原因。
- 熟练度的提高,专注于某个领域(降低复杂度)。
- 时间的节约,同一个人在不同工作来回切换需要耗费大量时间。
- 人工发明的机器和应用(特定领域的工具)。
二 如何给对象(元素)分配职责?
分配职责应当从清晰的描述职责开始,对于软件领域对象来说,领域模型描述了领域对象的属性和关联,对应类的属性和引用,用例模型包含一系列的行为活动,对应类的方法。领域模型创建方式可参考《UML和模式应用》、UDD、DDD。
使用GRASP(General Responsibility Assignment Software Patterns)模式分配职责,GRASP是通用职责分配模式,是对一些基本的职责分配原则进行了命名和描述,共9种模式(一些GRASP原则是对其他原则和设计模式的归纳,设计模式有上百种,只是记住GoF 23种设计模式就已经很困难了,更别提还要记住每种模式的细节,因此需要对设计模式进行有效的归类。GRASP中的原则描述了模式的本质,除了有助加速设计模式学习之外,对发现现有设计存在的问题也更有效,这就是归纳的价值)。
当谈论低耦合、高内聚时,我们具体是在谈什么?问题不在于耦合度高、内聚性低,而是在于其产生的负面影响,负面影响往往是在发生变化时体现出来的,这些负面影响会影响到我们开发的效率、稳定性、可维护性、可扩展性、可复用性等等,整个GRASP的核心是如何防止变异(变化)。
在学习过程中发现GRASP缺少结构化的展示归纳结果,通过我自己的理解把开发中常用的GoF设计模式、面向对象设计原则、架构设计原则和GRASP进行关联:
三 GRASP职责分配模式
1 防止变异
该模式基本等同于信息隐藏和开闭原则。如何做到在不修改原来功能的前提下对变化的部分进行扩展?识别不稳定因素是特别困难的,也决定了我们能否做出符合开闭原则的设计。
问题:如何设计对象、子系统和系统,使其内部的变化或不稳定性不会对其他元素产生不良影响。
解决方案:识别预计变化或不稳定之处,分配职责用以在这些变化之外创建稳定接口。
相关原则和模式:
- GRASP:间接性、多态
- GoF:大量模式
- 其他:接口、数据封装
2 低耦合、高内聚
耦合是对某元素与其他元素之间的连接、感知和依赖程度的度量,内聚是对元素职责的相关性和集中度的度量(这里的元素指类、系统、子系统等等),耦合和内聚是从不同角度看待问题,他们互相依赖的互相影响的(以下两点也可以反过来说):
- 内聚过低,相关功能分散在不同模块中,需要增加额外的耦合使这些功能聚合在一起,发生变更时影响多个模块。
- 内聚过高,不相关的功能聚集在一个模块中,耦合度高,发生变更时会产生意想不到的影响。
低耦合
耦合是对某元素与其他元素之间的连接、感知和依赖程度的度量。这里的元素指类、系统、子系统等等。
问题:怎样降低依赖性,减少变化带来的影响,提高重用性?
解决方案:分配职责,使耦合尽可能低。利用这一原则评估可选方案。
相关模式或原则:
- GRASP:防止变异
注意:耦合不能脱离专家、高内聚等其他原则独立考虑。
紧密耦合的系统在开发阶段有以下的缺点:
- 一个模块的修改会产生涟漪效应,其他模块也需随之修改(通常是内聚低引起的)。
- 由于模块之间的相依性,模块的组合会需要更多的精力及时间,可复用性低(通常是耦合高引起的)。
解读:耦合表示元素之间存在依赖,当谈论“耦合高”时,我们具体是在谈论什么呢?是依赖产生的负面影响,所以低耦合的核心是解决不良依赖。高低是度量并不是评判耦合结果好坏的标准,使用“不良耦合”、“松耦合”描述的更为准确。不良耦合产生的负面影响主要有两点:
- 依赖关系本身错综复杂难以维护和理解,很容易产生遗漏和问题(这点针对人,人处理复杂性事物时能力是局限的)。
- 与不稳定元素产生依赖时很容易受到变化的影响(通常无法避免不依赖)。
那么如何做呢?先对依赖关系的好坏进行评估:依赖方式、依赖方向、依赖链路。
方向:
- 双向依赖(差)
-
- 相互依赖的两个元素不能独立行动,在微服务系统架构的系统中类级别不会产生特别复杂的问题,但是在模块 or 系统级别就特别容易受到变化带来的影响。
-
- 举例:A <-> B,A调用B的b接口,B的b接口依赖A的a接口,如果a b接口都要变更,两个系统如何发布?A依赖B先发布,B也依赖A先发布,相互依赖的两个元素不能独立行动。
- 循环依赖(更差)
-
- 循环依赖比双向依赖的的链路更长,影响的范围更大。
- 单向依赖(好)
链路:
- 深度
-
- B调用A.getC().getD().getE().getF() 获取到F。
- 广度
-
- 在链路变宽的过程中不加以约束和管理很容易产生大杂烩的元素,也很容易产生双向和循环依赖。
方式:
- 内容耦合(高)
-
- 当一个模块直接使用另一个模块的内部数据,或通过非正常入口而转入另一个模块内部。
- 共享耦合/公共耦合(高)
-
- 指通过一个公共数据环境相互作用的那些模块间的耦合。
-
- 公共耦合的复杂程度随耦合模块的个数增加而增加。
- 控制耦合(中)
-
- 指一个模块调用另一个模块时,传递的是控制变量(如开关、标志等),被调模块通过该控制变量的值有选择地执行块内某一功能;
- 特征耦合/标记耦合(中)
-
- 指几个模块共享一个复杂的数据结构,如高级语言中的数组名、记录名、文件名等这些名字即标记,其实传递的是这个数据结构的地址;
- 数据耦合(低)
-
- 指模块借由传入值共享数据,每一个数据都是最基本的数据,而且只分享这些数据(例如传递一个整数给计算平方根的函数)。
- 非直接耦合(低)
-
- 两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。耦合度最弱,模块独立性最强。
- 无耦合(无)
-
- 模块完全不和其他模块交换信息。
解决不良依赖:
- 管理复杂的依赖关系
-
- 依赖方向:使用单向依赖,去除或弱化双向依赖,不使用循环依赖。
-
- 依赖链路:遵守最少认知原则。
-
- 依赖方式:尽量使用数据耦合,少用控制和特征耦合,控制公共耦合的范围,不使用内容耦合,如果依赖的对象不稳定使用非直接耦合来弱化耦合紧密程度。
- 分配正确的职责减少不必要的依赖:专家、创建者。
- 通过其他原则和模式减少不稳定元素带来的影响:高内聚、纯虚构、控制器、多态、间接性、最少认知。
高内聚
内聚是对元素职责的相关性和集中度的度量。
问题:怎么样保持对象是有重点的、可理解的、可维护的,并且能够支持低耦合?
解决方案:按照相关性分配职责,可保持较高的内聚。
优点:
- 分解后的元素更加简单易于理解和维护。
- 按照相关性拆分可以提高重用性。
相关原则和模式:单一职责原则、关注点分离、模块化。
低内聚的缺点:内聚性较低的类要做许多不相关的工作,或需要完成大量的工作,这样的类会导致以下问题:
- 难以理解
- 难以复用
- 难以维护
- 经常会受到变化影响
例子:A的变更影响从3个模块变为1个。
小结
通过结构化管理来保持低耦合、高内聚。
3 创建者
创建者指导我们分配那些与创建对象有关的职责。如此选择是为了保持低耦合。
问题:谁应该负责创建某类的新实例?
解决方案:满足以下条件之一时,将创建类A的职责分配给类B(当满足1条以上时,通常首选包含或聚合)。
- B“包含”或聚合A。
- B记录A。
- B频繁使用A。
- B具有A的初始化数据,该数据将在创建时传递给A。
优点:支持低耦合,因为创建者和被创建者已经存在关联,所以这种方式不会增加耦合性。
相关模式或原则:
- GRASP:低耦合
- GoF:具体工厂、抽象工厂
- 其他:整体-部分
注:包含(作者在这里标注了“”,因为包含在uml是表达用例关系的,用来说明对象关系也可以)、聚合、整体-部分 看UML定义;包含强调了强依赖(A是B的子集,A属于B,缺少了A时B不是整体),聚合是弱依赖(B由A组成,A不属于B)。
例子:
- Order包含Goods(Order脱离Goods就失去了完整性,没有存在的意义)。
- Order记录相关的Goods。
- Goods初始化数据:
-
- 情况一:只需要订单上的Goods数据,这种情况Order具有Goods的初始化数据。
-
- 情况二:订单上的Goods数据不完整,这种情况Order只有Goods初始化数据的一小部分,Order不能做为创建者。
4 信息专家(or 专家)
“信息”不单指数据。
问题:给对象分配职责的基本原则是什么?
解决方案:把职责分配给信息专家,它具有实现这个职责所必需的信息
优点:
- 对象使用自身信息来完成任务,所以信息的封装性得以维持,因此支持了低耦合(至少不会增加耦合性)。
- 行为分布在那些具有所需信息的类之间,这样功能更集中,因此支持了高内聚。
相关模式或原则:
- GRASP:低耦合、高内聚
注意:和“关注点分离”一起使用使得对象进一步内聚,从而达到高内聚,也能降低耦合。
举例:获取所有买的商品总金额,Order和Goods是一对多的关系。
分析:Order本身关联了Goods,并且理解Goods的结构。在图例中Client通过Order获取了Goods并做了逻辑运算得出商品总金额,这种做法产生了不必要的依赖增加了耦合数量,商品总金额计算的职责由Order承担最合适。
延伸:在某些情况下,该方案并不合适,通常是由于耦合与内聚问题产生的,如:谁应该把对象A存入数据库?按照原则每个类都应该具有把自己持久化的能力。
5 纯虚构
为了保持良好的耦合和内聚,捏造业务上不存在的对象来承担职责。
问题:当你并不想违背高内聚和低耦合或者其他目标,但是基于专家模式所提供的方案又不合适时,哪些对象应该承担这一职责?
解决方案:对人为制造的类分配一组高内聚的职责,该类并不代表问题领域的概念--虚构的事物,用以支持高内聚、低耦合和复用。
优点:
- 支持高内聚,因为职责被解析为细粒度的类,这种类只着重于极为特定的一组相关任务。
- 增加了潜在的复用性。
相关原则和模式:
- GRASP:低耦合、高内聚。
- 通常接纳本来是基于专家模式所分配给领域类的职责。
- 所有GoF设计模式都是纯虚构,事实上所有其他设计模式也都是纯虚构。
举例:计算商品总数量。根据专家模式计算商品总数量的职责也应该是分配给Order,照这样分配下去商品相关的还会有:总重量、总体积、总XX,这时Order的职责就会越来越多也可能会产生额外的耦合,通过纯虚构对象把这些职责分配出去能够得到更好的设计。
通过虚构对象GoodsItems承担和商品聚合计算相关的职责。
延伸:经常发现代码中会使用Util、Handler、Service这样的虚构类,缺点是这些类通常是单例并共用的,这些虚构类的职责会越来越多(一个Util类2000行代码),创建和业务更相近的虚构对象才能便于理解和管理耦合关系。
6 控制器
解决方案:把职责分配给能代表以下选择之一的类:
- 代表整个“系统”、“根对象”、运行软件的设备或主要子系统,这些是外观控制器的所有变体。
- 代表用例场景,在该场景中发生系统事件。
相关模式:
- GRASP:纯虚构
- GoF:命令、外观
- 其他:层
控制器的核心是提供一个统一入口,避免客户对元素内部进行耦合,很好的维护了边界:
- api层
- 根对象
- 接口
7 多态
问题:如何处理给予类型的选择?如何创建可插拔的软件构件?
解决方案:当相关选择或行为随类型有所不同时,使用多态操作为变化的行为类型分配职责。
优点:可扩展性强,同时不影响客户。
相关原则和模式:
- GRASP:防止变异
- GoF:大量模式
订单退款时需要计算出用户退款金额和商户扣款金额,在没有新零售业务进来之前直接使用计算服务返回的数据结构,新零售进来后数据结构未统一,需要进行适配,实现多态后的代码扩展性很强。
在微服务架构中,比较复杂的多态问题通常会选择增加一层去解决,如:支付网关、交付网关。
8 间接性
计算机学科中的大多数问题都可以通过增加一层解决,如果不行再加一层。反过来大多数性能问题都可以通过去掉一层来解决。
问题:为了避免两个或多个事物之间直接耦合,应该如何分配职责?
解决方案:将职责分配给中介对象,使其作为其他构建或服务之间的媒介,以避免他们之间的直接耦合。
优点:实现了构件之间的低耦合。
相关原则和模式:
- GRASP:防止变异、低耦合、大量间接性中介都是纯虚构
- GoF:大量模式
注意:间接性通常用来支持防止变异。
四 架构模式
除了职责分配原则,还需要一些架构模式帮助我们更好的落地。
1 分层架构
在分布式系统中系统是独立存在的,可以单独变更而不对其他系统产生影响,但是随着业务整体复杂度的提升也带来了一些负面影响:由于整体被分解成大量独立的系统,随着复杂度提升系统之间的依赖关系会变的错综复杂,某个系统的变更会影响其他系统,同时也会产生意想不到的问题,效率也随之下降。这时就需要重新对分布式系统的逻辑架构做设计,以解决系统间的不良耦合和内聚,从而提效。
分层架构是非常实用和常见的方式,TCP/IP、HTTP、操作系统等等都运用了分层,分层的本质很简单:通过分离关注点,达到高内聚;通过向下依赖、拒绝循环依赖、使用接口,达到低耦合。
分层架构也是存在缺点的:按照分层架构定义消息消费应该在基础设施层,但是消息消费是为了执行某个业务逻辑,这样就需要依赖应用层 或 领域层,如果真的这样写就会出现循环依赖问题。通过依赖倒置可以解决依赖问题。
2 六(多)边形架构(洋葱圈架构)
六边形架构(Hexagonal Architecture),又称为端口和适配器架构风格,其中的“六”具体数字没有特殊的含义,仅仅表示一个“量级”的意思,六边形的定义只是方便更加形象的理解。
六边形架构提倡用一种新的视角来看待整个系统,该架构中存在两个区域:“外部区域”和“内部区域”。在外部区域中不同的客户均可以提交输入(网络请求、定时脚本、消息消费等),而内部区域则是处理具体逻辑的地方。
五 案例
案例1:Jpa替换为Mybatis
@Component
public class CloseOrderService {
@Autowired(required = false)
@Qualifier("rstOrderTransactionManager")
JpaTransactionManager tm;
public void invalid_order(Long orderId, Long userId, Short processGroup)
throws UserException, SystemException, UnknownException {
//其他逻辑。。。省略
// 开启事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus ts = tm.getTransaction(def);
try {
order = orderDAO.get(orderId);
order.setStatusCode(toStatus);
order.setUpdatedAt(new Timestamp(System.currentTimeMillis()));
orderDAO.save(order);
//提交事务
tm.commit(ts);
} catch (Exception e) {
if (!ts.isCompleted()) {
//回滚
tm.rollback(ts);
}
if (e instanceof SatisfiedStateException) {
return;
}
throw e;
}
}
@Transactional(transactionManager = "rstOrderTransactionManager", rollbackFor = Exception.class)
public void invalidOrder(){
}
}
@Component
public interface OrderDAO extends JpaRepository<OrderPO, Long> {
@Query(value = "sql语句", nativeQuery = true)
Long generateGlobalOrderId(@Param("userId") Long userId,
@Param("restaurantId") Long restaurantId,
@Param("seqName") String seqName);
}
变化带来的影响:如果不出意外对Jpa的使用方式不会产生变更,意味着其相对稳定,所以在当前阶段来看以上耦合是正常的也不会产生负面影响。但是在以下场景会让我们对高耦合有很明显的体感:大家觉得Jpa不好用,想替换为Mybatis该怎么做?代码中直接使用了继承JpaRepository的OrderDAO做数据操作,由于Jpa和Mybatis的写法不同,所以需要把使用到OrderDAO的地方都做替换:
- 调用OrderDAO的类(70多个类)都需要替换为新的dao。
- 使用JpaTransactionManager.getTransaction()的位置需要替换为MyBatis的TransactionManager。
- 使用@Transactional(transactionManager = "rstOrderTransactionManager")的位置需要改为编写事务提交和回滚的代码块儿,便于做灰度。
- 以上改动的位置需要增加开关做灰度。
结论:由于变更涉及到70多个类,同时事务管理器获取方式也需要修改,其带来的影响还是挺大的,不满足“低耦合”原则,可以使用“多态”原则重新设计。
案例2:订单对应的支付单应该由谁来创建?
拿饿了么交易系统举例,当前创建支付单的职责是由bos服务承担(面向app的一个后端服务)的,接下我们进行分析。
支付单创建分为两种场景:
- 创建订单和支付单是在一次操作中完成。
- 用户回到订单列表页点击“去支付”时创建支付单。
支付单创建依赖:
- 订单号
- 支付金额
- 支付类型
- 一堆支付系统分配的用于识别业务的参数
注1:如果饿了么只会有外卖一种交易业务,当前的设计还是很稳定的,不会出现太大变化。所以识别变化点才能更好的评判当前系统设计是否合理,如:饿了么将升级为本地生活服务公司,根据公司战略多少能看出我们将来不只外卖业务存在,还会有很多和本地生活相关的交易业务,这些业务会有自己的展示层(app、h5、web)同时对应会有类似bos的服务,如果有10个业务方,在支付场景就需要去对接10次,而由order做就只需要一次(支付作为工具已经比较稳定,不会有太大变化)。
- bos比order多出识别订单结构的成本。
- bos比order多出认知交易域业务知识的成本。需要深入了解交易状态,这样才知道什么状态才能去支付(一般是去问order服务的开发),打破了边界。
结论:bos服务不应该承担创建支付单的职责,由order承担最合适。