GRASP 软件设计的模式和原则

发布于 2023-11-06 12:37:45 字数 10188 浏览 27 评论 0

GRASP

模式:每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。

这是关于模式最经典的定义,作者是建筑大师 Christopher Alexander。如果是第一次看到这句话,多数人会觉得有些抽象难懂。其实 模式 两个字只是一个代号,就像我的英文名字叫 Justin,如果我改叫 Tom 也没什么问题,只是我更喜欢 Justin 这个名字,所以从 Christopher 开始,有了 模式 这个词,人们也都把关于 重复发生的问题的描述和解决办法 统称为模式。

模式 这个词是不局限于软件开发行业的,它几乎无处不在,它其实就是一种经验的积累,就象大多数人的教育经历都是从小学到初中再到高中再到大学,这也是一种模式,是中国的教育模式;现在越来越火的出国热,也是另一种模式:海外留学模式。

因为 GOF 的《设计模式:可复用面向对象软件的基础》一书描述的 23 种经典设计模式,奠定了模式在软件行业的地位,从此人们提到 设计模式 就是默指 面向对象设计模式 ,但是如前文所述,模式绝对不局限于软件行业,即使在软件行业,也不局限于 GOF 描述的 23 种设计模式,例如最著名的 Martin Flower 的《企业架构模式》,还有我们常用的 MVC、IOC 等架构模式。

因为模式是一种经验的积累和总结,所以通过模式,我们可以站在巨人的肩膀上去思考问题、解决问题,熟练使用设计模式可以提高我们的工作效率,改善产品质量,最终带来经济效益。因此对于任何想开发出灵活高效、健壮的软件产品的个人或团体,熟练掌握并正确使用设计模式都是必须掌握的基本技能。

要学习设计模式,有些基础知识是我们必须要先知道的,设计模式是关于类和对象的一种高效、灵活的使用方式,也就是说,必须先有类和对象,才能有设计模式的用武之地,否则一切都是空谈,那么类和对象是从那冒出来的呢?这时就需要比 23 种设计模式更重要更经典的 GRASP 模式登场了。

GRASP,全称为 General Responsibility Assignment Software Pattern,即 通用职责分配软件模式,它由《UML 和模式应用》(Applying UML and Patterns) 一书作者 Craig Larman 提出。与其将它们称之为设计模式,不如称之为设计原则,因为它是站在面向对象设计的角度,告诉我们怎样设计问题空间中的类与分配它们的行为职责,以及明确类之间的相互关系等,而不像 GoF 模式一样是针对特定问题而提出的解决方案。

因此 GRASP 站在一个更高的角度来看待面向对象软件的设计,它是 GoF 设计模式的基础。GRASP 是对象职责分配的基本原则,其核心思想是职责分配(Responsibility Assignment),用职责设计对象(Designing Objects with Responsibilities)。它包含如下 9 个基本特征或原则:

1. 信息专家 (Information Expert)

  1. 问题:给对象分配职责的通用原则是什么?
  2. 解决方案:将职责分配给拥有履行一个职责所必需信息的类,即信息专家。
  3. 分析:信息专家模式是面向对象设计的最基本原则。通俗点来讲,就是一个类只干该干的事情,不该干的事情不干。在系统设计时,需要将职责分配给具有实现这个职责所需要信息的类。信息专家模式对应于面向对象设计原则中的单一职责原则。

例如:常见的网上商店里的购物车(ShopCar),需要让每种商品(SKU) 只在购物车内出现一次,购买相同商品,只需要更新商品的数量即可。如下图:

针对这个问题需要权衡的是,比较商品是否相同的方法需要放到那里类里来实现呢?分析业务得知需要根据商品的编号(SKUID) 来唯一区分商品,而商品编号是唯一存在于商品类里的,所以根据信息专家模式,应该把比较商品是否相同的方法放在商品类里。

2. 创造者(Creator)

(1) 问题:谁应该负责产生类的实例?

(2) 解决方案:如果符合下面的一个或者多个条件,则可将创建类 A 实例的职责分配给类 B:

  • B 包含 A;
  • B 聚合 A;
  • B 拥有初始化 A 的数据并在创建类 A 的实例时将数据传递给类 A;
  • B 记录 A 的实例;
  • B 频繁使用 A。

此时,我们称类 B 是类 A 对象的创建者。如果符合多个条件,类 B 聚合或者包含类 A 的条件优先。

(3) 分析:如果一个类创建了另一个类,那么这两个类之间就有了耦合,也可以说产生了依赖关系。依赖或耦合本身是没有错误的,但是它们带来的问题就是在以后的维护中会产生连锁反应,而必要的耦合是逃不掉的,我们能做的就是正确地创建耦合关系,不要随便建立类之间的依赖关系,那么该如何去做呢?就是要遵守创建者模式规定的基本原则,凡是不符合以上条件的情况,都不能随便用 A 创建 B。

创建对象是面向对象系统中最普遍的活动之一,因此,确定一个分配创建对象的通用职责非常重要。如果职责分配合理,设计就能降低耦合,提高设计的清晰度、封装性和重用性。通常情况下,如果对象的创建过程不是很复杂,则根据上述原则,由使用对象的类来创建对象。

但是如果创建过程非常复杂,而且可能需要重复使用对象实例或者需要从外部注入一个对象实例,此时,可以委托一个专门的工厂类来辅助创建对象。创建者模式与各种工厂模式(简单工厂模式、工厂方法模式和抽象工厂模式)相对应。例如:因为订单(Order) 是商品(SKU) 的容器,所以应该由订单来创建商品。如下图:

这里因为订单是商品的容器,也只有订单持有初始化商品的信息,所以这个耦合关系是正确的且没办法避免的,所以由订单来创建商品。

3. Low coupling (低耦合)

(1) 问题:怎样支持低的依赖性,减少变更带来的影响,提高重用性?

(2) 解决方案:分配一个职责,使得保持低耦合度。

(3) 分析:耦合是评价一个系统中各个元素之间连接或依赖强弱关系的尺度,具有低耦合的元素不过多依赖其他元素。此处的元素可以是类,也可以是模块、子系统或者系统。具有高耦合的类过多地依赖其他类,这种设计将会导致:一个类的修改导致其他类产生较大影响;系统难以维护和理解;系统重用性差,在重用一个高耦合的类时不得不重用它所依赖的其他类。因此需要对高耦合的系统进行重构。

类 A 和类 B 之间的耦合关系体现如下:A 具有一个 B 类型的属性;A 调用 B 的方法;A 的方法包含对 B 的引用,如方法参数类型为 B 或返回类型为 B;A 是 B 的直接或者间接子类;B 是一个接口,A 实现了该接口。低耦合模式鼓励在进行职责分配时不增加耦合性,从而避免高耦合可能产生的不良后果。在进行类设计时,需要保持类的独立性,减少类变更所带来的影响,它通常与信息专家模式和高内聚模式一起出现。为了达到低耦合,我们可以通过如下方式对设计进行改进:

  • 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
  • 在类的设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
  • 在类的设计上,只要有可能,一个类型应当设计成不变类;
  • 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

例如:Creator 模式的例子里,实际业务中需要另一个出货人来清点订单(Order) 上的商品(SKU),并计算出商品的总价,但是由于订单和商品之间的耦合已经存在了,那么把这个职责分配给订单更合适,这样可以降低耦合,以便降低系统的复杂性。如下图:

这里我们在订单类里增加了一个 TotalPrice() 方法来执行计算总价的职责,没有增加不必要的耦合。

4. High cohesion (高内聚)

(1) 问题:怎样使得复杂性可管理?

(2) 解决方案:分配一个职责,使得保持高内聚。

(3) 分析:内聚是评价一个元素的职责被关联和关注强弱的尺度。如果一个元素具有很多紧密相关的职责,而且只完成有限的功能,则这个元素就具有高内聚性。此处的元素可以是类,也可以是模块、子系统或者系统。

在一个低内聚的类中会执行很多互不相关的操作,这将导致系统难于理解、难于重用、难于维护、过于脆弱,容易受到变化带来的影响。因此我们需要控制类的粒度,在分配类的职责时使其内聚保持为最高,提高类的重用性,控制类设计的复杂程度。为了达到低内聚,我们需要对类进行分解,使得分解出来的类具有独立的职责,满足单一职责原则。在一个类中只保留一组相关的属性和方法,将一些需要在多个类中重用的属性和方法或完成其他功能所需的属性和方法封装在其他类中。类只处理与之相关的功能,它将与其他类协作完成复杂的任务。

例如:一个订单数据存取类(OrderDAO),订单即可以保存为 Excel 模式,也可以保存到数据库中;那么,不同的职责最好由不同的类来实现,这样才是高内聚的设计,如下图:

这里我们把两种不同的数据存储功能分别放在了两个类里来实现,这样如果未来保存到 Excel 的功能发生错误,那么就去检查 OrderDAOExcel 类就可以了,这样也使系统更模块化,方便划分任务,比如这两个类就可以分配个不同的人同时进行开发,这样也提高了团队协作和开发进度。

5. 控制器 (Controller)

(1) 问题:谁应该负责处理一个输入系统事件?

(2) 解决方案:把接收或者处理系统事件消息的职责分配给一个类。这个类可以代表:

  • 整个系统、设备或者子系统;
  • 系统事件发生时对应的用例场景,在相同的用例场景中使用相同的控制器来处理所有的系统事件。

(3) 分析:一个控制器是负责接收或者处理系统事件的非图形用户界面对象。一个控制器定义一组系统操作方法。在控制器模式中,要求系统事件的接收与处理通常由一个高级类来代替;一个子系统需要定义多个控制器,分别对应不同的事务处理。通常,一个控制器应当把要完成的功能委托给其他对象,它只负责协调和控制,本身不完成太多的功能。它可以将用户界面所提交的请求转发给其他类来处理,控制器可以重用,且不能包含太多业务逻辑,一个系统通常也不能设计一个统一的控制器。控制器模式与 MVC 模式相对应,MVC 是一种比设计模式更加高级的架构模式

6. Polymorphism (多态)

这里的多态跟 OO 三大基本特征之一的 多态 是一个意思。

(1) 问题:如何处理基于类型的不同选择?如何创建可嵌入的软件组件?

(2) 解决方案:当相关选择或行为随类型(类)变化而变化时,用多态操作为行为变化的类型分配职责。

(3) 分析:由条件变化引发同一类型的不同行为是程序的一个基本主题。如果用 if-else 或 switch-case 等条件语句来设计程序,当系统发生变化时必须修改程序的业务逻辑,这将导致很难方便地扩展有新变化的程序。另外对于服务器/客户端结构中的可视化组件,有时候需要在不影响客户端的前提下,将服务器的一个组件替换成另一个组件。此时可以使用多态来实现,将不同的行为指定给不同的子类,多态是设计系统如何处理相似变化的基本方法,基于多态分配职责的设计可以方便地处理新的变化。在使用多态模式进行设计时,如果需要对父类的行为进行修改,可以通过其子类来实现,不同子类可以提供不同的实现方式,将具体的职责分配给指定的子类。新的子类增加到系统中也不会对其他类有任何影响,多态是面向对象的三大基本特性之一(另外两个分别是封装和继承),通过引入多态,子类对象可以覆盖父类对象的行为,更好地适应变化,使变化点能够 经得起未来验证 。多态模式在多个 GoF 设计模式中都有所体现,如适配器模式、命令模式、组合模式、观察者模式、策略模式等等。

例如:我们想设计一个绘图程序,要支持可以画不同类型的图形,我们定义一个抽象类 Shape,矩形(Rectangle)、圆形(Round) 分别继承这个抽象类,并重写(override)Shape 类里的 Draw() 方法,这样我们就可以使用同样的接口(Shape 抽象类) 绘制出不同的图形,如下图:

这样的设计更符合高内聚和低耦合原则,虽然后来我们又增加了一个菱形(Diamond) 类,对整个系统结构也没有任何影响,只要增加一个继承 Shape 的类就行了。

7. 纯虚构 (Pure Fabrication)

(1) 问题:当不想破坏高内聚和低耦合的设计原则时,谁来负责处理这种情况?

(2) 解决方案:将一组高内聚的职责分配给一个虚构的或处理方便的 行为 类,它并不是问题域中的概念,而是虚构的事务,以达到支持高内聚、低耦合和重用的目的。

(3) 分析:纯虚构模式用于解决高内聚和低耦合之间的矛盾,它要求将一部分类的职责转移到纯虚构类中,在理想情况下,分配给这种虚构类的职责是为了达到高内聚和低耦合的目的。在实际操作过程中,纯虚构有很多种实现方式,例如将数据库操作的方法从数据库实体类中剥离出来,形成专门的数据访问类,通过对类的分解来实现类的重用,新增加的数据访问类对应于数据持久化存储,它不是问题域中的概念,而是软件开发者为了处理方便而产生的虚构概念。纯虚构可以消除由于信息专家模式带来的低内聚和高耦合的坏设计,得到一个具有更好重用性的设计。在系统中引入抽象类或接口来提高系统的扩展性也可以认为是纯虚构模式的一种应用。纯虚构模式通常基于相关功能的划分,是一种以功能为中心的对象或行为对象。在很多设计模式中都体现了纯虚构模式,例如适配器模式、策略模式等等。

例如:上面多态模式的例子,如果我们的绘图程序需要支持不同的系统,那么因为不同系统的 API 结构不同,绘图功能也需要不同的实现方式,那么该如何设计更合适呢?如下图:

这里我们可以看到,因为增加了纯虚构类 AbstractShape,不论是哪个系统都可以通过 AbstractShape 类来绘制图形,我们即没有降低原来的内聚性,也没有增加过多的耦合,可谓鱼肉和熊掌兼得!

8. Indirection (中介)

(1) 问题:如何分配职责以避免两个(或多个)事物之间的直接耦合?如何解耦对象以降低耦合度并提高系统的重用性?

(2) 解决方案:分配职责给中间对象以协调组件或服务之间的操作,使得它们不直接耦合。中间对象就是在其他组件之间建立的中介。

(3) 分析:要避免对象之间的直接耦合,最常用的做法是在对象之间引入一个中间对象或中介对象,通过中介对象来间接相连。中介模式对应于面向对象设计原则中的迪米特法则,在外观模式、代理模式、中介者模式等设计模式中都体现了中介模式。

中介 顾名思义,就是这个事不能直接来办,需要绕个弯才行。绕个弯的好处就是,本来直接会连接在一起的对象彼此隔离开了,一个的变动不会影响另一个。就像前面的低耦合模式里说的一样, 两个不同模块的内部类之间不能直接连接 ,但是我们可以通过中间类来间接连接两个不同的模块,这样对于这两个模块来说,他们之间仍然是没有耦合/依赖关系的。

9. Protected Variations (受保护变化)

(1) 问题:如何分配职责给对象、子系统和系统,使得这些元素中的变化或不稳定的点不会对其他元素产生不利影响?

(2) 解决方案:找出预计有变化或不稳定的元素,为其创建稳定的 接口 而分配职责。

(3) 分析:受保护变化模式简称 PV,它是大多数编程和设计的基础,是模式的基本动机之一,它使系统能够适应和隔离变化。它与面向对象设计原则中的开闭原则相对应,即在不修改原有元素(类、模块、子系统或系统)的前提下扩展元素的功能。开闭原则又可称为 可变性封装原则(Principle of Encapsulation of Variation, EVP) ,要求找到系统的可变因素并将其封装起来。如将抽象层的不同实现封装到不同的具体类中,而且 EVP 要求尽量不要将一种可变性和另一种可变性混合在一起,这将导致系统中类的个数急剧增长,增加系统的复杂度。在具体实现时,为了符合受保护变化模式,我们通常需要对系统进行抽象化设计,定义系统的抽象层,再通过具体类来进行扩展。如果需要扩展系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,在不修改已有代码的基础上扩展系统的功能。大多数设计原则和 GoF 模式都是受保护变化模式的体现。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

时光病人

暂无简介

0 文章
0 评论
21 人气
更多

推荐作者

内心激荡

文章 0 评论 0

JSmiles

文章 0 评论 0

左秋

文章 0 评论 0

迪街小绵羊

文章 0 评论 0

瞳孔里扚悲伤

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文