关于软件开发:软件开发必修课你该知道的GRASP职责分配模式

31次阅读

共计 7966 个字符,预计需要花费 20 分钟才能阅读完成。

简介: 软件开发为什么须要职责驱动设计(RDD)?职责应该如何调配?如何联合架构模式在理论开发中实际落地?本文介绍一种通用的职责分配模式——GRASP,通过举例详解 GRASP 的几大准则,并分享两个理论使用的案例。


软件在实质上是简单的,软件自身的复杂性在于除了要解决问题域,还要解决非功能性需要和软件域特有问题:安全性、可用性、可维护性、可扩展性、性能、一致性、容错性、稳定性、可重用性、幂等、兼容等等,软件开发者的工作就是制作“简略”的假象。如何组织简单的零碎?把简单的事物合成到不同的档次中,档次代表了不同级别的形象,一层构建于另一层之上,每一层都对下层屏蔽外部复杂度。

一 为什么应用 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 承当最合适。

正文完
 0