# GRASP 设计模式 & SOLID 设计原则

对象设计并不是凭空瞎想的, 是有章法可寻的。

在设计, 建模和编写代码时, 我们可以应用各种验证可行的 OO 「 设计原则 」和「 设计模式

设计模式,就是对「 已有问题 」及其「 解决方案 」的总结和描述

  • 已有问题,指的是在前人开发时出现过的问题,很大可能性我以后在开发时也会遇到。它不是一个之前从未出现过的新问题。
  • 设计模式可以帮我在遇着同类问题时,提供一个验证可行的解决方案。

这些设计模式总的来说遵循了『 职责驱动设计 Responsibility-driven design, RDD 』原则。

在『 职责驱动设计 Responsibility-driven design , RRD 』中认为软件对象是具有职责的. 也就是对象自身是具备行为的.

  • 职责和方法并非同一个事物, 职责是一种抽象, 而方法实现了职责。
  • 根据职责粒度的不同, 一个职责可能会被转化成一个或多个方法。

一个 OO 软件就可以看作是一堆有职责的对象进行协作构成的共同体

  • 简单的面向对象和优良的面向对象设计的区别,在于如何更合理的划分对象的角色,赋予对象职责,以及安排对象之间的关系。

# GRASP 设计模式

GRASP 是『 通用职责分配软件模式 General Responsibility Assignment Software Patterns 』的缩写. 它是一套依据「 职责驱动 」定义的设计原则。

它作为一个设计工具, 以一种系统的, 合理的, 可以解释的方式来指导你进行对象设计和职责分配。

GRASP 中定义了 9 个基本的 OO 设计模式:

2020-08-21-16-29-32

设计模式有很多很多种, 但是基本上都可以视为是对于 GRASP 的应用。

# 创建者 Creator

果以下条件之一 ( 越多越好 ) 为真时,就将创建类 A 实例的职责分配给类 B:

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

基本原则就是,寻找任何情况下都与被创建对象具有连接的对象作为创建者 」。这样做的原因是为了保持低耦合性, 因为创建者类无论如何都与被创建类有关系, 所以让他作为创建者不会增加什么耦合性.

# 信息专家 Information Expert

把职责分配具有实现这个职责所必须的信息的对象,这种对象被称为「 信息专家 」。

为了完成职责往往需要分布在不同对象中的信息, 每个对象都具有各自的职责, 这些局部的 "信息专家" 互相通过信息来协作, 共同完成职责。

# 低耦合 Low Coupling

关于「 耦合度 」,可以简单地理解为当一个类发生变更时,对其他类造成的影响程度,影响越小则耦合度越弱,影响越大耦合度越强。

以下是一些耦合关系的体现:

  • A 具有一个 B 类型的属性;
  • A 调用 B 的方法;
  • A 的方法包含对 B 的引用,如方法参数类型为 B 或返回类型为 B;
  • A 是 B 的直接或者间接子类;
  • B 是一个接口,A 实现了该接口。

在分配职责时,要使耦合度尽可能的低。在以上的这些耦合条件中,出现得越多代表耦合程度越高。

「 高耦合 」带来的问题:

  • 一个类的改变,造成其他类也需要随之改变。
  • 难以单独地理解。
  • 一个类的使用需要配合其他类,导致很难复用。

高耦合本身并不带来问题, 带来问题的是与「 不稳定的元素 」进行耦合.

  • 例如 A 对象与 B 对象耦合在一起去实现一个功能, 但是 B 对象的实现可能随时会发生改变, 那么这种设计就是危险的。因为 A 对 B 是不可控的.
  • J2EE 能够安全的与 Java 官方提供的库 ( 例如 java.util ) 耦合在一起, 是因为他们是稳定的, 可控的。Java 官方不会突然间给你改一套完全不兼容的实现.

# 高内聚 High Cohesion

从对象设计的角度上来说,「 内聚 」( 更准确的说是功能内聚 ) 是对元素职责的相关性和集中度的度量。

  • 如果一个元素 ( 类,子系统,etc ) 具有高度相关的职责,没有做多余的工作,那么它就具有「 高内聚
  • 高内聚的好处是,一个类不会太过于膨胀,导致维护性变差。同时,高内聚也代表了高隔离,在修改某一个方法的时候,不至于影响到太多其他类。

在进行职责分配时,要尽可能保持高内聚

「 低内聚 」带来的问题:

  • 难以复用。
  • 难以理解。
  • 难以维护。
  • 脆弱,容易受到变化的影响。

低内聚的类通常表示「 大粒度的抽象 」,或者承担了本应该委托给其他类的职责

# 控制器 Controlle

把接收或者处理系统事件消息的职责分配给一个类。这个类可以代表,整个系统、设备,子系统,或系统事件发生时对应的用例场景。

  • 这个类称为「 控制器
  • 控制器应当把接收到的系统事件,委托给对应的业务处理对象。它只负责协调和控制业务流程,尽量不要包含太多具体的业务逻辑。

# 多态性 Polymorphism

当相关选择或行为随类型有所不同时,使用多态为变化的行为分配职责

  • 不要测试对象的类型,也不要使用条件逻辑来执行基于类型的不同选择。

🌰 假设我们有一个画图 Draw 类,有多个图形类 Rectangle、Circle、Square。

  • 如果要按照不同图形类进行绘制的话,就需要在 Draw 类的方法中使用 if-else 的程序结构,依次判断类型进行绘制。如果新增一个图形类的话,就又需要对这段代码进行更改。
  • 采用多态的形式,将绘制的具体步骤交给图形类的子类实现。就不用使用 if-else 的程序结构,在新增图形类的时候也不需要修改 Draw 类。

# 纯虚构 Pure Fabrication

在 OO 设计时,系统内的大多数软件类,都是来源于从现实世界中抽象出的概念类。然而,在给这些类分配职责时,有可能会遇到一些很难满足低耦合高内聚的设计原则。

此时,将一组高内聚的职责分配给一个虚构的类,它并不是领域模型中的概念类,而是虚构的事物

🌰 许多项目都需要对数据库进行操作,将系统中的一些对象进行持久化。信息专家模式给出的建议是将持久化的职责分配给具体的每一个实体类。但是这种建议已经被证明是不符合高内聚低耦合原则的。于是,现在的做法往往会在项目中加入类似于 DAO 或者 Repository 这样的类。这些类在领域模型中是并不存在的。

# 间接性 Indirection

分配职责给一个中介对象以协调组件或服务之间的操作,使得它们不直接耦合。这个中介对象使得构件之间具有「 间接性

简单来说就是通过一个中间人来处理一件事。本来直接联系的两个对象可以通过另一个中间对象进行交互,这样做便实现了隔离和解耦,一个对象的变动不会影响另一个对象,仅会影响到中间对象。

🌰 系统要使用外部的服务去计算税费,外部提供的 API 可能会变化。此时我创建一个中介对象,通过多态,我为内部对象提供一致的接口,然后内部逻辑根据具体的外部 API 去实现。如果外部 API 发生变化,我只需要改一下中介对象的实现,对于系统内部不造成影响。

# 防止变异 Protected Variations

防止变异模式关注这样一个问题:如何设计对象、子系统和系统,使其内部的变化或不稳定性不会对其他元素产生不良影响?

识别预计变化或不稳定之处,在这些变化之外创建稳定的接口

防止变异(PV)是一个软件设计根本原则,几乎所有的设计原则和模式都是防止变异的特例。

「 防止变异 」与「 脆弱设计 」之间的平衡:

  • 在程序中会出现两种变更点:
    • 变化点:当前系统或需求中存在的变化。例如,系统必须支持多个税金计算接口。
    • 进化点:预测将来可能会产生的变化点。但当前需求中并不存在。
  • 对于「 变化点 」和「 进化点 」我们应该进行「 防止变异 」的设计。
  • 但是对于「 进化点 」我们不应该做过度的预测,有时候等变化真正发生了,再去重构所花费的成本可能比一开始就过度设计要少。
  • 初学者倾向于做出脆弱的设计,中等程度开发者倾向于过度设计,专家则会理智的进行选择。

2020-08-21-16-39-29

# SOLID 设计原则

# S-O-L-I-D

SOLID 可以看作是对于 GRASP 设计模式的一种总结,从中提炼出了 5 种设计原则。

单一职责原则 Single Responsibility Principle

  • 一个对象应该只包含单一的职责
  • 单一并非指严格意义上的唯一的一个职责,而应该是指一类联系紧密的职责。这也是高内聚的一种体现。
  • 之所这样,是为了防止当一个类中既有职责 A 又有职责 B 时,当修改职责 A 相关的代码时,因为某些原因导致职责 B 出现异常的可能性。

开放/关闭原则 Open/Closed Principle

  • 应该对扩展是开放的,对修改是封闭的
  • 意思就是一个类在进行扩展时,是不需要对其内部进行修改的。

里氏替换原则 Liskov Substitution Principle

  • 一个对象在其出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误。这种继承关系才是合理的

接口隔离原则 Interface Segregation Principle

  • 不应该让一个类依赖它不使用的方法
  • 如果一个类实现的接口中,包含了它不需要的方法。就需要接口拆分成更小和更具体的接口,有助于解耦,从而更容易重构、更改。
  • 也是高内聚低耦合的一种表现形式。如果一个类依赖了它不需要的接口,那么在系统中便存在了这样一种没有意义的耦合,不利于耦合度的降低。反过来,在建立接口时也不要建立臃肿的、包含一切的接口。这样的接口反而失去了高内聚性。

依赖反转原则 Dependency Inversion Principle

  • 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
  • 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
  • 类 A 依赖于类 B 实现。那么,当类 A 需要改变对类 B 的依赖,转而依赖类 C 时,类 A 必须修改源代码才能实现。而如果类 A 依赖于一个接口 X 实现,而类 B 和类 C 都实现这个接口,那么之后无论是类 B 和类 C 之间的替换,抑或是让类 A 去依赖新的类 D,都是非常易于实现的。
  • 本质也是降低了耦合,将一个类与细节的耦合降低到了与接口的耦合,而与一个稳定的接口之间耦合是良好的。

SOLID 原则之间并不是相互孤立的,彼此间存在着一定关联,一个原则可以是另一个原则的加强或基础.违反其中的某一个原则,可能同时违反了其他原则。其中,

  • 开闭原则和里氏替换原则是设计目标
  • 单一职责原则、接口分隔原则和依赖倒置原则是设计方法

# 其他设计原则

# DRY

DRY 的意思是 Don't Repeat Yourself

  • 指在程序设计和计算中避免重复代码;
  • 系统的每一个功能都应该有唯一的实现;
  • 如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能。

# YAGNI

YAGNI 的意思是 You Ain't Gonna Need It

  • 是指你自以为有用的功能,实际上都是用不到的。
  • Some programmers fall into is trying to make their code too extensible, and adding hooks for every possible variation of everything they could ever possibly see. Abstraction is good, and we want to be able to extend our programs, but abstracting too much will mean more testing, more debugging, and code bloat.
  • 除了核心的功能之外,其他的功能一概不要提前设计,这样可以大大加快开发进程。

# Rule of Three

你会发现 DRY 原则和 YAGNI 原则是不兼容的。前者追求“抽象化”,要求找到通用的解决方法;后者追求“快和省”,意味着不要把精力放在抽象化上面,因为很可能“你不会需要它”。因此,就有了 Rule of Three 原则。

Rule of Three 也被称为“三次原则”,是指当某个功能第三次出现时,就有必要进行“抽象化”了。

上次更新: 9/6/2020, 4:49:29 AM