首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

架构必备的领域边界划分方法:职责驱动设计

写该文章的目的是对过去一段时间自我学习知识的总结,通过对职责驱动设计(RDD)的学习发现通过该模式进行对领域职责进行划分能够得出很清晰的职责,能有效减少无结论的掰扯,后面部分也有些在实际中的运用,期望能和大家交流起来~

软件在本质上是复杂的,软件本身的复杂性在于除了要解决问题域,还要解决非功能性需求和软件域特有问题:安全性、可用性、可维护性、可扩展性、性能、一致性、容错性、稳定性、可重用性、幂等、兼容等等,软件开发者的任务就是制造“简单”的假象。如何组织复杂的系统?把复杂的事物分解到不同的层次中,层次代表了不同级别的抽象,一层构建于另一层之上,每一层都对上层屏蔽内部复杂度,每一层都需要有清晰的职责防止边界被破坏。边界的划分是重点也是难点,接下来我们来探讨如何划分职责来定义清晰的边界。

1.为什么使用RDD?

在RDD中,我们认为“软件对象具有职责”,这个定义很符合人在社会群体中分工协作的方式,软件也是人编写的,所以根据职责思考设计的软件系统符合人的行为习惯,同时更易于理解和管理。在微服务架构中不同系统由不同的组织和人负责,把系统当作对象(人),系统提供的接口就是对象(人)的职责。

职责驱动设计的核心是考虑怎样给对象分配职责,其适用于大到系统、小到对象等任何规模的软件。职责分配的本质是分工,劳动分工是劳动生产率提高的主要原因

 1. 熟练度的提高,专注于某个领域(降低复杂度)

 2. 时间的节约,同一个人在不同工作来回切换需要耗费大量时间

 3. 人工发明的机器和应用(特定领域的工具)

2.如何给对象(元素)分配职责?

分配职责应当从清晰的描述职责开始,对于软件领域对象来说,领域模型描述了领域对象的属性和关联,对应类的属性和引用,用例模型包含一系列的行为活动,对应类的方法。领域模型创建方式参考:

  1. 学习《UML和模式应用》、UDD、DDD

使用GRASP模式分配职责,GRASP是通用职责分配模式(General Responsibility Assignment Software Patterns),是对一些基本的职责分配原则进行了命名和描述,共9种模式。

一些GRASP原则是对其他原则和设计模式的归纳,设计模式有上百种,只是记住GoF 23种设计模式就已经很困难了更别提还要记住每种模式的细节,因此需要对设计模式进行有效的归类,GRASP中的原则描述了模式的本质,除了有助加速设计模式学习之外,对发现现有设计存在的问题也更有效,这就是归纳的价值

当谈论低耦合、高内聚时,我们具体是在谈什么?问题不在于耦合度高、内聚性低,而是在于其产生的负面影响,负面影响往往是在发生变化时体现出来的,这些负面影响会影响到我们开发的效率、稳定性、可维护性、可扩展性、可复用性等等,所以整个GRASP的核心是如何防止变异(变化)

在学习过程中发现GRASP缺少结构化的展示归纳结果,通过我自己的理解把开发中常用的GoF设计模式、面向对象设计原则、架构设计原则和GRASP进行关联。注:这个图可能总结的还不够准确,正在逐步学习修改。

3.GRASP职责分配模式

3.1 防止变异

该模式基本等同于信息隐藏和开闭原则。如何做到在不修改原来功能的前提下对变化的部分进行扩展?识别不稳定因素是特别困难的,也决定了我们能否做出符合开闭原则的设计。

问题:如何设计对象、子系统和系统,使其内部的变化或不稳定性不会对其他元素产生不良影响。 解决方案:识别预计变化或不稳定之处,分配职责用以在这些变化之外创建稳定接口。 相关原则和模式

  • GRASP:间接性、多态
  • GoF:大量模式
  • 其他:接口、数据封装

3.2 低耦合、高内聚

耦合是对某元素与其他元素之间的连接、感知和依赖程度的度量,内聚是对元素职责的相关性和集中度的度量(这里的元素指类、系统、子系统等等),耦合和内聚是从不同角度看待问题,他们互相依赖的互相影响的:(以下两点也可以反过来说)

  • 内聚过低,相关功能分散在不同模块中,需要增加额外的耦合使这些功能聚合在一起,发生变更时影响多个模块。
  • 内聚过高,不相关的功能聚集在一个模块中,耦合度高,发生变更时会产生意想不到的影响。

3.2.1 低耦合

耦合是对某元素与其他元素之间的连接、感知和依赖程度的度量。这里的元素指类、系统、子系统等等。

问题:怎样降低依赖性,减少变化带来的影响,提高重用性? 解决方案:分配职责,使耦合尽可能低。利用这一原则评估可选方案。 相关模式或原则

  • GRASP:防止变异

注意:耦合不能脱离专家、高内聚等其他原则独立考虑。 紧密耦合的系统在开发阶段有以下的缺点

  1. 一个模块的修改会产生涟漪效应,其他模块也需随之修改(通常是内聚低引起的)。
  2. 由于模块之间的相依性,模块的组合会需要更多的精力及时间,可复用性低(通常是耦合高引起的)。

解读:耦合表示元素之间存在依赖,当谈论“耦合高”时,我们具体是在谈论什么呢?是依赖产生的负面影响,所以低耦合的核心是解决不良依赖。高低是度量并不是评判耦合结果好坏的标准,使用“不良耦合”、“松耦合”描述的更为准确。不良耦合产生的负面影响主要有两点:

  1. 依赖关系本身错综复杂难以维护和理解,很容易产生遗漏和问题(这点针对人,人处理复杂性事物时能力是局限的)。
  2. 不稳定元素产生依赖时很容易受到变化的影响(通常无法避免不依赖)。

那么如何做呢?先对依赖关系的好坏进行评估: 依赖方式、依赖方向、依赖链路

解决不良依赖:

  1. 管理复杂的依赖关系
  2. 依赖方向:使用单向依赖,去除或弱化双向依赖,不使用循环依赖。
  3. 依赖链路:遵守最少认知原则。
  4. 依赖方式:尽量使用数据耦合,少用控制和特征耦合,控制公共耦合的范围,不使用内容耦合,如果依赖的对象不稳定使用非直接耦合来弱化耦合紧密程度。
  5. 分配正确的职责减少不必要的依赖:专家、创建者
  6. 通过其他原则和模式减少不稳定元素带来的影响:高内聚、纯虚构、控制器、多态、间接性、最少认知

3.2.2 高内聚

内聚是对元素职责的相关性和集中度的度量。

问题:怎么样保持对象是有重点的、可理解的、可维护的,并且能够支持低耦合? 解决方案:按照相关性分配职责,可保持较高的内聚。 优点

  1. 分解后的元素更加简单易于理解和维护。
  2. 按照相关性拆分可以提高重用性。

相关原则和模式:单一职责原则、关注点分离、模块化 低内聚的缺点:内聚性较低的类要做许多不相关的工作,或需要完成大量的工作,这样的类会导致以下问题:

  1. 难以理解
  2. 难以复用
  3. 难以维护
  4. 经常会受到变化影响

例子:A的变更影响从3个模块变为1个。

3.2.3 小结

通过结构化管理来保持低耦合、高内聚。

3.3 创建者

创建者指导我们分配那些与创建对象有关的职责。如此选择是为了保持低耦合。

问题:谁应该负责创建某类的新实例? 解决方案:满足以下条件之一时,将创建类A的职责分配给类B(当满足1条以上时,通常首选包含或聚合)

  1. B“包含”或聚合A。
  2. B记录A。
  3. B频繁使用A。
  4. B具有A的初始化数据,该数据将在创建时传递给A。

优点:支持低耦合,因为创建者和被创建者已经存在关联,所以这种方式不会增加耦合性。 相关模式或原则

  • GRASP:低耦合
  • GoF:具体工厂、抽象工厂
  • 其他:整体-部分

:包含(作者在这里标注了“”,因为包含在uml是表达用例关系的,用来说明对象关系也可以)、聚合、整体-部分 看UML定义;包含强调了强依赖(A是B的子集,A属于B,缺少了A时B不是整体),聚合是弱依赖(B由A组成,A不属于B)

例子

  1. Order包含Goods(Order脱离Goods就失去了完整性,没有存在的意义)
  2. Order记录相关的Goods。
  3. Goods初始化数据
  4. 情况一:只需要订单上的Goods数据,这种情况Order具有Goods的初始化数据。
  5. 情况二:订单上的Goods数据不完整,这种情况Order只有Goods初始化数据的一小部分,Order不能做为创建者。

3.4 信息专家(or 专家)

“信息”不单指数据。

问题:给对象分配职责的基本原则是什么? 解决方案:把职责分配给信息专家,它具有实现这个职责所必需的信息 优点

  1. 对象使用自身信息来完成任务,所以信息的封装性得以维持,因此支持了低耦合(至少不会增加耦合性)。
  2. 行为分布在那些具有所需信息的类之间,这样功能更集中,因此支持了高内聚。

相关模式或原则

  • GRASP:低耦合、高内聚

注意:和“关注点分离”一起使用使得对象进一步内聚,从而达到高内聚,也能降低耦合。

举例:获取所有买的商品总金额,Order和Goods是一对多的关系。

分析:Order本身关联了Goods,并且理解Goods的结构。在图例中Client通过Order获取了Goods并做了逻辑运算得出商品总金额,这种做法产生了不必要的依赖增加了耦合数量,商品总金额计算的职责由Order承担最合适。

延伸:在某些情况下,该方案并不合适,通常是由于耦合与内聚问题产生的,如:谁应该把对象A存入数据库?按照原则每个类都应该具有把自己持久化的能力。

3.5 纯虚构

为了保持良好的耦合和内聚,捏造业务上不存在的对象来承担职责。

问题:当你并不想违背高内聚和低耦合或者其他目标,但是基于专家模式所提供的方案又不合适时,哪些对象应该承担这一职责? 解决方案:对人为制造的类分配一组高内聚的职责,该类并不代表问题领域的概念--虚构的事物,用以支持高内聚、低耦合和复用。 优点

  1. 支持高内聚,因为职责被解析为细粒度的类,这种类只着重于极为特定的一组相关任务。
  2. 增加了潜在的复用性

相关原则和模式

  • GRASP:低耦合、高内聚。
  • 通常接纳本来是基于专家模式所分配给领域类的职责。
  • 所有GoF设计模式都是纯虚构,事实上所有其他设计模式也都是纯虚构。

举例:计算商品总数量。 根据专家模式计算商品总数量的职责也应该是分配给Order,照这样分配下去商品相关的还会有:总重量、总体积、总XX,这时Order的职责就会越来越多也可能会产生额外的耦合,通过纯虚构对象把这些职责分配出去能够得到更好的设计。

通过虚构对象GoodsItems承担和商品聚合计算相关的职责。

延伸:经常发现代码中会使用Util、Handler、Service这样的虚构类,缺点是这些类通常是单例并共用的,这些虚构类的职责会越来越多(一个Util类2000行代码),创建和业务更相近的虚构对象才能便于理解和管理耦合关系。

3.6 控制器

解决方案:把职责分配给能代表以下选择之一的类:

  1. 代表整个“系统”、“根对象”、运行软件的设备或主要子系统,这些是外观控制器的所有变体。
  2. 代表用例场景,在该场景中发生系统事件

相关模式

  • GRASP:纯虚构
  • GoF:命令、外观
  • 其他:层

控制器的核心是提供一个统一入口,避免客户对元素内部进行耦合,很好的维护了边界

  • api层
  • 根对象
  • 接口

3.7 多态

问题:如何处理给予类型的选择?如何创建可插拔的软件构件? 解决方案:当相关选择或行为随类型有所不同时,使用多态操作为变化的行为类型分配职责。 优点:可扩展性强,同时不影响客户。 相关原则和模式

  • GRASP:防止变异
  • GoF:大量模式

订单退款时需要计算出用户退款金额和商户扣款金额,在没有新零售业务进来之前直接使用计算服务返回的数据结构,新零售进来后数据结构未统一,需要进行适配,实现多态后的代码扩展性很强。

在微服务架构中,比较复杂的多态问题通常会选择增加一层去解决,如:支付网关、交付网关

3.8 间接性

计算机学科中的大多数问题都可以通过增加一层解决,如果不行再加一层。反过来大多数性能问题都可以通过去掉一层来解决。

问题:为了避免两个或多个事物之间直接耦合,应该如何分配职责? 解决方案:将职责分配给中介对象,使其作为其他构建或服务之间的媒介,以避免他们之间的直接耦合。 优点:实现了构件之间的低耦合。 相关原则和模式

  • GRASP:防止变异、低耦合、大量间接性中介都是纯虚构
  • GoF:大量模式

注意:间接性通常用来支持防止变异

4.架构模式

除了职责分配原则,还需要一些架构模式帮助我们更好的落地。

4.1 分层架构

在分布式系统中系统是独立存在的,可以单独变更而不对其他系统产生影响,但是随着业务整体复杂度的提升也带来了一些负面影响:由于整体被分解成大量独立的系统,随着复杂度提升系统之间的依赖关系会变的错综复杂,某个系统的变更会影响其他系统,同时也会产生意想不到的问题,效率也随之下降。这时就需要重新对分布式系统的逻辑架构做设计,以解决系统间的不良耦合和内聚,从而提效。

分层架构是非常实用和常见的方式,TCP/IP、HTTP、操作系统等等都运用了分层,分层的本质很简单:通过分离关注点,达到高内聚;通过向下依赖、拒绝循环依赖、使用接口,达到低耦合。

分层架构

分层架构也是存在缺点的:按照分层架构定义消息消费应该在基础设施层,但是消息消费是为了执行某个业务逻辑,这样就需要依赖应用层 或 领域层,如果真的这样写就会出现循环依赖问题,通过依赖倒置可以解决依赖问题。

4.2 六(多)边形架构(洋葱圈架构)

六边形架构(Hexagonal Architecture),又称为端口和适配器架构风格,其中的“六”具体数字没有特殊的含义,仅仅表示一个“量级”的意思,六边形的定义只是方便更加形象的理解。

六边形架构

六边形架构提倡用一种新的视角来看待整个系统,该架构中存在两个区域:“外部区域”和“内部区域”。在外部区域中不同的客户均可以提交输入(网络请求、定时脚本、消息消费等),而内部区域则是处理具体逻辑的地方。

图片来源:https://www.jianshu.com/p/d3e8b9ac097b

5.案例

案例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的地方都做替换:

  1. 调用OrderDAO的类(70多个类)都需要替换为新的dao
  2. 使用JpaTransactionManager.getTransaction()的位置需要替换为MyBatis的TransactionManager
  3. 使用@Transactional(transactionManager = "rstOrderTransactionManager")的位置需要改为编写事务提交和回滚的代码块儿,便于做灰度
  4. 以上改动的位置需要增加开关做灰度

结论:由于变更涉及到70多个类,同时事务管理器获取方式也需要修改,其带来的影响还是挺大的,不满足“低耦合”原则,可以使用“多态”原则重新设计。

案例2:订单对应的支付单应该由谁来创建?

拿饿了么交易系统举例,当前创建支付单的职责是由bos服务承担(面向app的一个后端服务)的,接下我们进行分析。

流程图

支付单创建分为两种场景:

  1. 创建订单和支付单是在一次操作中完成
  2. 用户回到订单列表页点击“去支付”时创建支付单

支付单创建依赖:

  1. 订单号
  2. 支付金额
  3. 支付类型
  4. 一堆支付系统分配的用于识别业务的参数

注1:如果饿了么只会有外卖一种交易业务,当前的设计还是很稳定的,不会出现太大变化。所以识别变化点才能更好的评判当前系统设计是否合理,如:饿了么将升级为本地生活服务公司,根据公司战略多少能看出我们将来不只外卖业务存在,还会有很多和本地生活相关的交易业务,这些业务会有自己的展示层(app、h5、web)同时对应会有类似bos的服务,如果有10个业务方,在支付场景就需要去对接10次,而由order做就只需要一次(支付作为工具已经比较稳定,不会有太大变化)。

  • bos比order多出识别订单结构的成本。
  • bos比order多出认知交易域业务知识的成本。需要深入了解交易状态,这样才知道什么状态才能去支付(一般是去问order服务的开发),打破了边界。

结论:bos服务不应该承担创建支付单的职责,由order承担最合适。

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/0f3eab53ac4228d769909425a
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券