设计模型|总览

Yeren Lv3

维基百科

软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领域引入到计算机科学的。

设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别物件来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或物件。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。

并非所有的软件模式都是设计模式,设计模式特指软件“设计”层次上的问题。还有其他非设计模式的模式,如架构模式。同时,算法不能算是一种设计模式,因为算法主要是用来解决计算上的问题,而非设计上的问题。

简单定义

设计模式,即Design Patterns,是指在软件设计中,被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。

引子

目前的程序设计范式有多种,其中包含面向过程的编程方式(C等)与面向对象的编程方式(Java等),两者均为很经典的设计范式。

面向过程是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。

在日常生活或编程中,简单的问题可以用面向过程的思路来解决,直接有效,但是当问题的规模变得更大时,用面向过程的思想是远远不够的。世界上有很多人和事物,每一个都可以看做一个对象,而每个对象都有自己的属性和行为,对象与对象之间通过方法来交互。面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个对象在整个解决问题的步骤中的属性和行为。

拿一个五子棋小游戏来举例子,如果是面向过程的设计思路,会分析这个问题的步骤:

  1. 开始游戏
  2. 走黑子
  3. 绘制画面
  4. 判断输赢
  5. 走白子
  6. 绘制画面
  7. 判断输赢
  8. 返回步骤2
  9. 输出最后结果

但如果是面向对象的设计思路,会分析问题的对象以及属性和行为:

  1. 黑白两方(玩家)
  2. 棋盘系统,负责绘制画面
  3. 规则系统,负责判定犯规,输赢等

对象1接收玩家输入,同时与对象2和对象3进行交互,共同维护棋局直到游戏结束。

可以分析得出面向过程的优点是:

  1. 流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析。
  2. 效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。

缺点是:需要深入的思考,耗费精力,代码重用性低,扩展能力差,后期维护难度比较大。

面向对象的优点是:

  1. 结构清晰,程序是模块化和结构化,更加符合人类的思维方式;
  2. 易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;
  3. 易维护,系统低耦合的特点有利于减少程序的后期维护工作量。

缺点是:

  1. 开销大,当要修改对象内部时,对象的属性不允许外部直接存取,所以要增加许多没有其他意义、只负责读或写的行为。这会为编程工作增加负担,增加运行开销,并且使程序显得臃肿。
  2. 性能低,由于面向更高的逻辑抽象层,使得面向对象在实现的时候,不得不做出性能上面的牺牲,计算时间和空间存储大小都开销很大。

面向对象的设计范式有助于代码的可扩展性和可维护性,利于代码开发,但随着代码规模的不断扩大以及开发经验的不断丰富,人们发现在面向对象的设计范式下,表现出优秀的可扩展性和可维护性的高质量软件代码都有相同的特点,其中有两个非常重要的特点就是高内聚和低耦合。

内聚(Cohesion):内聚是指模块内部各个元素(函数、类、方法等)彼此相关联,共同完成某个单一的目标或任务的度量。高内聚的模块意味着模块内部的元素紧密关联,相互依赖,共同完成一项特定的功能。内聚性强的模块通常具有更高的独立性,易于维护和测试。

耦合(Coupling):耦合是指模块之间的依赖关系,描述了一个模块对其他模块的了解程度。低耦合的模块之间相互独立,一个模块的修改不应该导致其他模块的大规模变动。高耦合的模块之间相互关联紧密,修改一个模块可能会影响其他模块,导致系统脆弱和难以维护。

fig1

高内聚通常与低耦合一起被推崇,因为高内聚可以促使模块更加独立,降低模块之间的相互影响,从而减少系统的复杂度,提高系统的可维护性和可扩展性。

那么如何写出高内聚低耦合的高质量软件代码呢,人们总结出了许多面向对象的设计原则,如下图所示:

fig2

  1. 单一职责原则(Single Responsibility Principle):一个类应该只有一个引起它变化的原因。这意味着一个类应该只负责一个特定的功能,从而使类的设计更加清晰和可维护。

    示例: 考虑一个图书管理系统,其中有一个 Book 类负责管理图书的基本信息(如标题、作者、出版日期等),另有一个 Library 类负责管理图书的借还操作。通过将图书的基本信息和借还操作分离到不同的类中,可以遵循单一职责原则。

  2. 开闭原则(Open-Closed Principle):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着可以通过扩展现有的代码来引入新功能,而不需要修改已经存在的代码。

    示例: 考虑一个绘图应用,现有一个 Shape 类用于表示各种形状(如圆、矩形),而未来可能需要添加新的形状。通过创建一个抽象的 Shape 类并让具体的形状类继承它,可以轻松地扩展应用,例如添加新的 Triangle 类。

  3. 里氏替换原则(Liskov Substitution Principle):子类应该能够替换其基类,而不会影响程序的正确性。即,子类应该保持与基类相同的行为特征,不破坏继承关系。

    示例: 假设有一个 Bird 基类和一个 Sparrow 子类。Sparrow 类应该能够替代 Bird 类,即可以在需要 Bird 对象的地方使用 Sparrow 对象,而不会引起错误或不一致的行为。

  4. 依赖倒置原则(Dependency Inversion Principle):高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这促进了松耦合,使得系统更加灵活。

    示例: 考虑一个订单处理系统,其中有一个高层模块 OrderProcessor 需要进行支付处理。按照依赖倒置原则,OrderProcessor 应该依赖于一个抽象的 PaymentProcessor 接口,而不是具体的支付方式。这样可以轻松地切换不同的支付方式,而不需要修改 OrderProcessor

  5. 接口隔离原则(Interface Segregation Principle):客户端不应该被强迫依赖于它们不使用的接口。即,一个类不应该实现不需要的接口,避免出现臃肿的接口。

    示例: 假设有一个 Worker 接口包含了 workeat 两个方法,但某个具体的工人类只需要实现 work 方法。根据接口隔离原则,应该将 Worker 接口拆分为两个接口:WorkableEatable,从而避免不必要的方法实现。

  6. 合成/聚合复用原则(Composition/Aggregation Reuse Principle):优先使用对象组合或聚合,而不是继承来实现代码复用。这有助于减少类之间的耦合性。

    示例: 考虑一个汽车制造系统,其中一个 Car 类包含引擎、轮胎等各种部件。而不是通过继承,可以使用对象组合或聚合来构建一个 Car 对象,将不同部件作为其成员变量,使得不同的部件可以被独立扩展和修改。

  7. 迪米特法则(Law of Demeter):一个对象应该只与其直接朋友交互,不要与陌生人交谈。这有助于降低类之间的耦合度。

    示例: 考虑一个在线购物系统,客户端代码需要获取订单的总金额。根据迪米特法则,客户端应该通过订单对象的方法来获取金额,而不是直接访问订单内部的各种对象和属性,这样可以降低客户端与订单内部结构的耦合。

上述原则分别从提高内聚和降低耦合以及方便变更的角度阐述了设计高质量代码应该遵循的原则,在进行代码设计与实现的时候时刻牢记并遵循上述原则有助于写出高质量的代码。

既然我们了解了设计高质量代码应该遵循的原则,那么在此基础上我们可以更进一步,针对不同的特定问题领域,按照设计原则来编写具体的解决方案模板,当程序员需要解决类似的问题时就不需要自己设计代码的结构,只需要按照已经提供好的设计模式编写代码,就可以做到遵循设计原则,写出高内聚低耦合的、具有良好的可扩展性和可维护性的高质量软件代码了。

于是,什么是设计模式?设计模式是将设计原则应用于特定问题领域的具体实现方式。每个设计模式都是一种经过验证的、可复用的设计解决方案,它们体现了一个或多个设计原则的实际应用。通过使用设计模式,开发人员可以在特定情境下遵循设计原则,从而达到更好的代码组织和架构。

设计模式的存在是建立在设计原则之上的。设计模式的实现方式涵盖了多个设计原则,而设计原则则为这些模式提供了共同的理论基础。通过理解和遵循设计原则,开发人员可以更好地理解为什么要使用某个设计模式以及如何正确地应用它。设计原则为设计模式提供了基础理论,而设计模式是这些原则在实际应用中的具体体现。通过结合设计原则和设计模式,开发人员能够创建出更加健壮、灵活和易维护的软件系统。

上述提到不同的设计原则是针对不同的特定问题领域,按照我们对问题领域的划分,我们将常见的设计模式分为三类。

  1. 创建型模式:涉及对象的创建机制,例如工厂模式、单例模式。
  2. 结构型模式:涉及对象之间的组合,例如桥接模式、组合模式、装饰模式。
  3. 行为型模式:涉及对象之间的交互,例如观察者模式、策略模式。

其中举例的部分均为我们在之后会具体讲解的、我认为较为重要的设计模式。

例子

设计一个支付系统,包含微信支付和支付宝支付两种支付模式,并且需要支持未来可能推出的更多支付模式。

一段不使用设计模式的代码方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("wechat")) {
// 处理微信支付
System.out.println("使用微信支付:" + amount);
} else if (paymentType.equals("AliPay")) {
// 处理支付宝支付
System.out.println("使用支付宝支付:" + amount);
} else {
System.out.println("不符合已有支付类型");
}
}
}

// 在主程序中使用
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();
processor.processPayment("wechat", 100.0);
processor.processPayment("AliPay", 50.0);
}
}

一段使用设计模式的代码方案(策略模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
interface PaymentStrategy {
void processPayment(double amount);
}

class WeChatPayment implements PaymentStrategy {
@Override
public void processPayment(double amount) {
System.out.println("使用微信支付:" + amount);
}
}

class AliPayment implements PaymentStrategy {
@Override
public void processPayment(double amount) {
System.out.println("使用支付宝支付:" + amount);
}
}

class PaymentProcessor {
private PaymentStrategy strategy;

public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}

public void processPayment(double amount) {
strategy.processPayment(amount);
}
}

// 在主程序中使用
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();

PaymentStrategy wechatStrategy = new WeChatPayment();
PaymentStrategy aliPayStrategy = new AliPayment();

processor.setPaymentStrategy(wechatStrategy);
processor.processPayment(100.0);

processor.setPaymentStrategy(aliPayStrategy);
processor.processPayment(50.0);
}
}

原本的代码编写方式会导致修改和增添新功能的时候对源代码的改动,随着功能的逐步增多,修改和增添新功能将变成困难的事,并且所付出的代价将会指数上升(出现bug等)。

使用了策略模式的编程方式在增添新功能时不需要对源代码进行改动,并且在修改代码时只需要对对应模块的代码进行修改,不会涉及到其他代码模块,使得bug产生的范围得到控制,提高了代码的可扩展性和可维护性。

  • Title: 设计模型|总览
  • Author: Yeren
  • Created at : 2023-08-12 00:00:00
  • Updated at : 2023-08-12 00:00:00
  • Link: https://blog.yeren.xyz/2023/08/12/DP-overview/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments