01-设计模式

Charlie

1. 概括

现有设计经验的总结

模式:问题和约束可以反复使用

  • 目的
    1. 创建型:创建对象
    2. 结构型:处理类和对象的组合
    3. 行为型:描述对类或对象怎样交互和怎样分配职责
  • 范围
    1. 类模式:式处理类和子类之间的关系,这些关系通过继承建立, 在编译时刻就被确定下来,是属于静态的。
    2. 对象模式:处理对象间的关系,这些关系在运行时刻变化,更具动态

image.png

2. 策略模式

  • Strategy/Policy Pattern

  • 使用哪些原则

    1. 单一职责
      • 封装变化
      • Seperation
    2. 依赖倒转
    3. 合成复用

delegate: 委托另外一个模块,通过引用调用
OOA到OOD:添加辅助类,设计初期会增加复杂性

image.png

2.1. 定义

完成的任务+实现的方式

  • 让算法的变化独立于使用它的客户端
  • 定义一个可变的算法族,并将它们封装起来

2.2. 适用性

  1. 相关的类只在某些行为上不同,每个类在这些行为上都有合适的策略(多个类)
  2. 同一个算法需要不同的实现
  3. 算法封装不同数据结构
  4. 一个类定义了有很多行为,使用条件判断觉得使用哪个行为(一个类里有多种行为)

2.3. 影响

原来不需要知道类的细节,但使用策略模式之后需要暴露细节,客户必须理解每一种策略的差异和哪些任务适合哪些细节

Design Pattern比Framework更高层

3. 简单工厂模式

3.1. 👍定义

  • Simple Factory Pattern/Static Factory Pattern
    • 往往是静态方法
    • 根据参数不同,返回不同类的实例
      • 复用创建方法
    • 属于类创建型模式
  • 原则
    • 最小知识原则,构造方法无法抽象但是可以封装

3.2. 结构

  • Factory: 工厂角色
  • Product: 抽象产品角色
  • ConcreteProduct: 具体产品角色

返回的是抽象的产品类,符合依赖倒置原则

image.png

3.3. 分析

  1. 将对象的创建和具体业务处理分离,降低系统耦合(把具体耦合替换成抽象耦合),使得两者修改起来更容易。
  2. 工厂方法是静态方法,而且只需要传入简单参数,因此可以把调用时需要传入的参数写在配置文件中,无需修改Java源代码。
  3. 工厂类的职责过重,增加新的产品需要修改工厂类判断逻辑,违反开闭原则
  4. 当你需要什么,只需要传入一个正确的参数,就可以获取对象,无需知道其创建的细节

3.4. 扩展

有些情况下可以把静态工厂方法放到抽象产品类中

image.png

4. 工厂方法模式

4.1. 动机

简单工厂违反了开闭原则

image.png

使用抽象工厂,抽象的工厂方法只负责生产抽象的产品类,把具体的按钮了创建过程嫁给专门的工厂子类来完成。
image.png

4.2. 👍定义

  • Factory Method Pattern/ Virtual Constructor/ Polymorphic Factory
    • 工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象
    • 目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类
    • 类创建模式
  • 可以时间不修改原有代码的情况下,增加新的产品

4.3. 结构

image.png

用户代码只使用抽象层,与具体实现解耦

抽象工厂类:

1
2
3
public abstract class PayMethodFactory{
public abstract AbstractPay getPayMethod();
}

具体工厂类:

1
2
3
4
5
public class CashPayFactory extends PayMethodFactory{
public AbstractPay getPayMethod(){
return new CashPay();
}
}

客户代码:

1
2
3
4
5
PayMethodFactory factory;
AbstractPay payMethod;
factory=new CashPayFactory();
payMethod =factory.getPayMethod();
payMethod.pay();

使用XML指定抽象工厂具体类型的客户代码:

1
2
3
4
5
PayMethodFactory factory;
AbstractPay payMethod;
factory=(PayMethodFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,此处需要进行强制类型转换
payMethod =factory.getPayMethod();
payMethod.pay();

4.4. 适用性

  1. 一个类不需要知道它所需要的对象的类
  2. 一个类通过其子类来指定创建哪个对象
  3. 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无需关心是哪一个工厂类创建产品子类,需要时再动态指定

只能扩展具体产品类型
支持开闭原则

5. 抽象工厂模式

产品等级结构:即产品的继承结构 ,如抽象类是电视机,子类是不同具体品牌
产品族:由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产电视机、冰箱,海尔电视机属于电视机等级结构,冰箱属于冰箱等级结构

5.1. 👍定义

  • 区别

    1. 工厂模式:针对一个产品等级结构
    2. 抽象工厂:需要面对多个产品等级结构
  • Abstract Factory Pattern/Kit Pattern

    • 提供创建一系列相关或互依赖对象的接口,而无需指定它们具体的类
    • 对象创建模式:扩展能力由对象耦合产生,所以不是类创建模式

image.png

1
2
3
4
public abstract class AbstractFactory{
public abstract AbstractProductA createProductA();
public abstract AbstractProductB createProductB();
}
1
2
3
4
5
6
7
8
public class ConcreteFactory1 extends AbstractFactory{
public AbstractProductA createProductA(){
return new ConcreteProductA1();
}
public AbstractProductB createProductB(){
return new ConcreteProductB1();
}
}

5.2. 分析

开闭原则的倾斜性

  • 优点:增加新的具体工厂和产品族很方便,符合开闭原则
  • 缺点:不能完全支持开闭原则,需要增加新的产品等级结构时需要修改抽象类
    • 不支持增加新的产品等级结构

6. 建造者模式

一个复杂对象有许多成员对象

6.1. 定义

  • **建造者模式(Builder Pattern)**:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
    • 表示:对象的静态内容
    • 构建过程可以复用,组件的表示可以分离
    • 属于对象创建型模式

封装变化,复用不变

6.2. 结构

  1. Product
  2. Builder:抽象建造者
  3. ConcreteBuilder:负责A, B, C等对象的创建
  4. Director:创建A,B,C的构建顺序
  • 为什么是对象创建模式?
    • 最重要的是通过Director和Builder的合成关系,而不是继承

image.png

  • 例子:
    • KFC的指挥者是服务员,而不是顾客

image.png

6.3. 分析

  • Director

    1. 隔离客户与生产过程
    2. 负责控制产品的生产过程
  • 为什么不对Director抽象提供各种策略?

    • 初衷就是为了隔绝用户和复杂的构造过程,如果再对Director做抽象,用户就需要知道各种复杂的构造过程

6.4. 适用性

  1. 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品
  2. 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
  3. 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
  4. 对象的创建过程独立于创建该对象的类

2,3,4是为了分解复杂性;
1是为了封装变化

6.5. 化简

  1. 省略抽象建造者:如果系统中只需要一个具体建造者的话,可以省略掉抽象建造者。
  2. 省略指挥者:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略指挥者角色,让Builder角色扮演指挥者与建造者双重角色。

6.6. 建造者模式与抽象工厂模式的比较

  • 与抽象工厂模式相比,建造者模式返回一个组装好的完整产品,而抽象工 厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成 了一个产品族。
    • 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产
    品对象,而在建造者模式中,客户端可以不直接调用建造者的相关方法,
    而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,
    它侧重于一步步构造一个复杂对象,返回一个完整的对象。
    • 如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那
    么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完
    整的汽车。

7. 原型模式

创建型模式
在面向对象系统中,使用原型模式来复制一个对象自身, 从而克隆出多个与原型对象一模一样的对象。

7.1. 定义

  • 原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。允许一个对象再创建一个可定制的对象,无需知道任何创建的细节。

    • 对象创建型模式
  • 基本工作原理是通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝原型自己来实现创建过程。

7.2. 结构

  1. Prototype:抽象原型类
    • 原型类有一个clone接口
  2. ConcretePrototype:具体原型类
  3. Client:客户类

image.png

7.3. 分析

抽象类:IS A关系,主职责
接口:非功能主职责的职责

copy需要根据具体类来执行,而且不是IS A关系

  • Java中实现

    • 在原型模式结构中定义了一个抽象原型类,所有的Java 类都继承自java.lang.Object,而Object类提供一个 clone()方法,可以将一个Java对象复制一份。因此在 Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。
    • 能够实现克隆的Java类必须实现一个标识接口Cloneable, 表示这个Java类支持复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个 CloneNotSupportedException异常
      • 把一些工作交给编译器
    • 在浅克隆中,当对象被复制时它所包含的成员对象却没有被复制;在深克隆中,除了对象本身被复制外,对象包含的引用也被复制,也就是其 中的成员对象也将复制。在Java语言中,通过覆盖Object类的clone()方 法可以实现浅克隆。
  • 优点

    1. 创建新对象成本较大,使用原型模式可以简化对象创建过程
    2. 可以使用深克隆的方式保存对象的状态
    3. 可以动态增加或减少产品类
      • 写一个工厂类,维护一个原型的注册表,通过这个表动态增加或减少产品类
    4. 原型模式提供了简化的创建结构
  • 缺点

    1. 需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难
      • 但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”
    2. 在实现深克隆时需要编写较为复杂的代码。

7.4. 适用性

  • 适用环境
    1. 创建新对象成本较大
    2. 系统要保存对象的状态,而对象状态变化小/对像本身内存占用不大
      • 如果对象的状态变化很大,或者对象占用的内存很大,那么采用状态模式会比原型模式更好。
    3. 避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象 得到新实例可能比使用构造函数创建一个新实例更加方便

7.5. 例子

7.5.1. 邮件复制(浅克隆)

由于邮件对象包含的内容较多(如发送者、接收者、标 题、内容、日期、附件等),某系统中现需要提供一个邮件复制功能,对于已经创建好的邮件对象,可以通过复制的方式创建一个新的邮件对象,如果需要改变某部分内容,无须修改原始的邮件对象,只需要修改复制后得到的邮件对象即可。使用原型模式设计该系统。在本实例中使用浅克隆实现邮件复制,即复制邮件(Email)的同时不复制附件(Attachment)

image.png

7.5.2. 工厂方法+原型

在客户端可以通过注册新的原型增加产品类,注销原型减少产品类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 工厂类
class PrototypeFactory {
private Map<String, Prototype> prototypes = new HashMap<>();

public void registerPrototype(String key, Prototype prototype) {
prototypes.put(key, prototype);
}

public void unregisterPrototype(String key) {
prototypes.remove(key);
}

public Prototype createPrototype(String key) {
Prototype prototype = prototypes.get(key);
if (prototype != null) {
return prototype.clone();
}
return null;
}
}

7.6. 扩展

  • 原型管理器:带管理器的原型模式,系统中有大量相似的对象

image.png

  • 相似对象的复制:通过原型模式获得相同对象后可以再对其属性进行修改,从而获取所需对象,大部分内容相同只是某些属性存在差异。

8. 状态模式

8.1. 动机

  • 状态:一个或多个变化的属性
  • 有状态的对象:有这样属性的对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。

UML中使用状态图来描述有状态对象的变化,一张图只能描述一个对象

如果写很多if来判断状态,不符合开闭模式

8.2. 结构

  1. Context: 环境类
  2. State: 抽象状态类
  3. ConcreteState: 具体状态类

image.png

8.3. 定义

  • State Pattern:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类
    • 通过对象组合来减少类型定
  • 对象行为模式
  • 别名为状态对象(Objects for States)

8.4. 分析

  1. 引入一个抽象类,专门表示对象的状态变化,以及对象如何在每一种状态中表现出的行为
  2. Context应当尽量承担数据职责,State承担行为职责
    • 可能相互持有引用:State类持有Context类的引用是为了访问数据,Context类持有State类是为了执行行为
  3. 封装了状态转换规则
    • 自已完成行为切换,行为是具体的,会产生具体耦合。开闭原则支持不好,增加新的状态要修改负责状态转换的源代码。
    • 与策略模式区别:策略模式中策略的切换在Client中,由使用者切换,开闭原则支持好。
  4. 状态转换的触发者
    1. Context类
    2. Concrete State类
  5. 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类 的行为也需修改对应类的源代码

8.5. 适用性

  1. 对象的行为依赖于状态属性,并且随着属性改变
  2. 代码中由大量与状态有关的条件判断

8.6. 例子

8.6.1. 预定房间

image.png

image.png

  • 不使用状态模式:
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
if(state=="空闲")
{
if(预订房间)
{
预订操作;
state="已预订";
}
else if(住进房间)
{
入住操作;
state="已入住";
}
}
else if(state=="已预订")
{
if(住进房间)
{
入住操作;
state="已入住";
}
else if(取消预订)
{
取消操作;
state="空闲";
}
}
……
  • 使用状态模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
//重构之后的“空闲状态类”示例代码
……
if(预订房间)
{
预订操作;
context.setState(new 已预订状态类());
}
else if(住进房间)
{
入住操作;
context.setState(new 已入住状态类());
}
……

8.6.2. 论坛用户等级

image.png
image.png

8.6.3. 模拟电梯

行为型模式-状态模式-CSDN博客

8.7. 扩展

  • 共享状态
    • 在有些情况下多个环境对象需要共享同一个状态,如果希望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象。
  • 简单状态模式
    • 所有状态相互独立,无需转换。可以在客户端直接实例化状态类,然后设置到环境类中。
    • 遵循开闭原则,和策略模式只是含义不一样,设计完全相同。
  • 可切换的状态模式
    • 大多数的状态模式都是可以切换状态的状态模式,在实现状态切换时,在具体状态 类内部需要调用环境类Context的setState()方法进行状态的转换操作,在具体状态类中可以调用到环境类的方法,因此状态类与环境类之间通常还存在关联关系或者 依赖关系。通过在状态类中引用环境类的对象来回调环境类的setState()方法实现状态的切换。
    • 在这种可以切换状态的状态模式中,增加新的状态类可能需要修改其他某些状态类甚至环境类的源代码,否则系统无法切换到新增状态。

9. 命令模式

9.1. 动机

  • 我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可。
  • 让发送者和接收者完全解耦,发送者完全不知道接收者是谁
    • 如果无法抽象,可以用wrapper

9.2. 定义

  • 命令模式(Command Pattern):将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化对请求排队或者记录请求日志(队列),以及支持可撤销的操作
  • 命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式

9.3. 结构

  1. Invoker
    • 被参数化
  2. Command
  3. Receiver

Invoker只知道Command类,不知道调用的是Receiver,也不知道命令具体怎么执行。
execute()是receiver.action()的wrapper方法。

image.png

线程池:同一个线程对象,传入不同Command,让它执行不同的命令

9.4. 分析

  1. 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递
    • 行为数据化,同一个行为可以在不同时间/空间执行
    • 参数化
    • 行为封装成对象,可以存储成队列
  2. 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。
  3. 本质:是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
  4. 每个命令都是一个操作:请求方发出请求,要求执行一个操作;接收方接收请求,执行操作
  5. 请求方和接收方解耦,使得Invoker和Command都可以被复用
    1. 不同的具体Command可能对应不同Receiver
  • 优点

    1. 降低耦合
    2. 容易加入新的命令
    3. 容易设计一个命令队列和宏命令(组合命令)
    4. 实现undo和redo
      • 在Command接口里除了execute()还可以有undo()
  • 执行时序图:
    image.png

9.5. 例子

9.5.1. 电视机遥控器

Invoker: Contoller
Receiver: Television

image.png

command的reciver相同

9.5.2. 功能键

用户可以通过配置文件来改变功能键的用途,使用命令模式来设计该系统,是的功能键类与功能类解耦,相同的功能键可以对应不同的功能。

command的reciver不同

image.png

文本编辑器

image.png

Site Unreachable

客户端代码 (GUI 元素和命令历史等) 没有和具体命令类相耦合。

9.6. 适用场景

  1. 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
  2. 系统需要在不同的时间指定请求,将请求排队和执行请求
  3. 支持命令的Undo和Redo
  4. 需要将一组操作在一起,即支持宏命令
  5. 实现**委派事件模型(Delegation Event Model, DEM)**。

9.7. 扩展

9.7.1. 宏命令

  1. 宏命令又称为组合命令,它是命令模式和组合模式联用的产物。
  2. 宏命令也是一个具体命令,不过它包含了对其他命令对象的引用,在调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法
  3. 宏命令的成员对象可以是简单命令,还可以继续是宏命令。执行 一个宏命令将执行多个具体命令,从而实现对命令的批处理
  4. 宏命令的undo:从ArrayList的尾到头做undo

image.png

9.7.2. 撤销操作

image.png

10. 中介者模式

10.1. 动机

  • 多个用户之间的群聊,引入中间层,减少对象两两之间复杂的引用关系
    • 迪米特法则

10.2. 定义

  • 中介者模式(Mediator Pattern) 定义:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  • 中介者模式又称为调停者模式
  • 对象行为型模式。

10.3. 结构

image.png

  1. Mediator:抽象中介类
  2. ConcreteMediator: 具体中介类
  3. Colleague: 抽象同事类
  4. ConcreteColleague: 具体同事类
1
2
3
4
5
6
7
8
9
public abstract class Mediator
{
protected ArrayList colleagues;
public void register(Colleague colleague)
{
colleagues.add(colleague);
}
public abstract void operation();
}
1
2
3
4
5
6
7
8
9
public class ConcreteMediator extends Mediator
{
public void operation()
{
......
((Colleague)(colleagues.get(0))).method1();
......
}
}
1
2
3
4
5
6
7
8
9
10
public abstract class Colleague
{
protected Mediator mediator;
public Colleague(Mediator mediator)
{
this.mediator=mediator;
}
public abstract void method1();
public abstract void method2();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConcreteColleague extends Colleague
{
public ConcreteColleague(Mediator mediator)
{
super(mediator);
}

public void method1()
{
......
}

public void method2()
{
mediator.operation1();
}
}

10.4. 分析

  1. 中介者模式使得对象之间的关系数量急剧减少image.png
  2. 为什么要多个中介者?
    • 修改对象交互关系时,不需要修改原来的中介关系,可以新增中介关系替换原有的关系。
  3. 如果每个同事都持有数据,中介者数据同步压力大
  • 中介者承担两方面的职责:
    1. 中转作用(结构):通过中介的中转作用,各个同时不需要显示引用其他同时
    2. 协调作用(行为):中介者可以进一步对同事之间的关系进行封装,同事可以一致地与中介者进行交互,而不需要指明中方接着需要具体则么做,中介者根据内部的逻辑对同事的请求进行进一步处理,将同事成员的关系和行为进行分离和封装

10.5. 例子

某论坛系统欲增加一个虚拟聊天室,允许论坛会员通过该聊天室进行信息交流,普通会员 (CommonMember) 可以给其他会员发送文本信息,钻石会员 (DiamondMember) 既可以给其他会员发送文本信息,还可以发送图片信息。该聊天室可以对不雅字符进行过滤,如“日”等字符;还可以对发送的图片大小进行控制。用中介者模式设计该虚拟聊天室。

image.png

10.6. 适用环境

  1. 系统中对象之间的引用关系很复杂,相互依赖关系混乱,难以复用
  2. 对象的交互行为很复杂。想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的中介者类。

10.7. 扩展

  1. 中介者模式与迪米特法则

    • 在中介者模式中,通过创造出一个中介者对象,将系统中有关的对象所引用的其他对象数目减少到最少,使得一个对象与其同事之间的相互作用被这个对象与中介者对象之间的相互作用所取代。因此,中介者模式就是迪米特法则的一个典型应用。
  2. 图形界面

    1. 工厂,抽象工厂
    2. 命令模式
    3. 建造者模式
    4. 中介者模式

11. 观察者模式

11.1. 动机

  • 建立一种对象和对象之间的依赖关系,一个对象改变时将字段通知其他对象,其他对象做出反应。
  • 发生改变的对象称为目标对象,被通知者称为观察者,一个观察目标对应多个观察者,而且这些观察者直接没有相互联系,可以根据增加和删除观察者,使得系统已于扩展

11.2. 定义

  • 观察者模式(Observer Pattern):定义了对象间的一种一对多依赖关系,使得每一个对象状态发生变化改变时,相关的依赖对象都得到通知并自动更新
    • 别名:发布-订阅(Publish/Subscribe)模式、模型-视图 (Model/View)模式、源-监听器(Source/Listener) 模式或从属者(Dependents)模式
  • 对象行为模式

11.3. 结构

  1. Subject: 目标
    • 抽象类:notify()方法每个subject实现都一样,使用抽象类,子类可以完全复用。
    • 折中,虽然希望是接口,但为了复用,抽象类更合适
  2. ConcreteSubject: 具体目标
  3. Observer: 观察者
    • 接口:观察不能作为类的主要职责/主要类型体系(所以接口比抽象类更好),而只是一个通信行为
  4. ConcreteObserver: 具体观察者
    • 只有知道自己需要哪些数据,按照单一职责目标不应该知道这些细节

image.png

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
import java.util.*;
public abstract class Subject
{
protected ArrayList observers = new ArrayList();
public abstract void attach(Observer observer);
public abstract void detach(Observer observer);
public abstract void notify();
}

public class ConcreteSubject extends Subject
{
public void attach(Observer observer)
{
observers.add(observer);
}
public void detach(Observer observer)
{
observers.remove(observer);
}
public void notify()
{
for(Object obs:observers)
{
((Observer)obs).update();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public interface Observer
{
public void update();
}

public class ConcreteObserver implements Observer
{
public void update()
{
//具体更新代码
}
}

11.4. 分析

  1. 集中管理数据,观察目标可以以单一职责管理数据;观察者只有行为职责,数据都从观察目标获取。
  2. 一对多关系实现类数据同步
  3. 对同一个观察目标而言,观察目标和观察者是一对多的关系,单一个观察者也可以订阅多个订阅目标。
  4. 更新数据方式:
    1. 推送:实现简单,松耦合。目标去通知观察者。
    2. 拉取:定制化,但是实现复杂。观察者调用目标的getter方法(减少松耦合)

松耦合目标:体现了最小知识原则

观察者模式可以实现广播通信,符合开闭原则

11.5. 例子

11.5.1. 猫、老鼠、狗

假设猫是老鼠和狗的观察目标,老鼠和狗是观察者,猫叫则老鼠跑,狗也跟着叫,使用观察者模式描述这个过程。

image.png

11.6. Java Swing自定义登录控件

Java的事件处理模型使用了观察者模式,下面是使用Java自定义控件并给控件增加相应的事件。

image.png

11.7. 适用环境

  1. 需要独立双向变化,分隔数据和表示职责
  2. 一个抽象模型有两个方面,一个方面依赖另一个方面,将这些方面封装在独立的对象中使它们可以独立地改变和复用
  3. 系统中需要创建一个触发链
  4. 一个对象变化导致其他对象变化,而不知道具体有多少对象
  5. 一个对象必须通知其他对象,而不知道这些对象是谁

11.8. 扩展

11.8.1. JDK中的观察者模式

image.png

  • Observer接口
    • void update(Observable o, Object arg);
  • Observable类
    1. Observable()
    2. addObserver(Observer o)
    3. deleteObserver (Observer o)
    4. notifyObservers() 拉取notifyObservers(Object arg) 推送
    5. deleteObservers
    6. setChanged(): 控制变化的粒度,当变化达到某个程度才更新,notify时会检查是否更新。
      • Hook function
    7. clearChanged()
    8. hasChanged()
    9. countObservers()

11.8.2. 谁触发更新

  1. 目标对象:状态改变后自动触发更新。简单但是可能会有过多更新,效率差。
  2. 观察者对象:增加观察者的责任,可能忘记更新导致数据不同步。

11.8.3. 更改管理器 Change Manager

需要在多个目标中进行协调,一个目标并不知道其他目标的状态。

把维持观察队列的职责外部化,合成复用原则

Change Manager实现类类似中介模式。

image.png
image.png

11.8.4. MVC模式

MVC:可以看作有Change Manager的观察者模式

观察目标:Model
观察者:View
中介:Controller

当Model的数据发生改变时,view也会发生改变。model是subject,view是observer
image.png

12. 模板方法模式

12.1. 动机

很多复杂的事情只需要某个步骤发生变化,就能发生任务的变化

12.2. 定义

  • 模板方法模式(Template Method Pattern):希望定义一个操作中算法的骨架,而将一些步骤延迟到子类中;在子类不改变算法的结构即可重定义算法的某些特定步骤。
    • 类行为模式,只有类的继承关系,没有对象关联关系
    • 基于继承的复用,把相同代码放到父类中,不同的方法实现放在不同子类中

12.3. 结构

image.png

  1. AbstractClass: 模板,抽象
  2. ConcreteClass: 具体子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//钩子方法 Hook function
public void template()
{
open();
display();
if(isPrint())
{
print();
}
}
public boolean isPrint()
{
return true;
}
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
//典型的抽象类
public abstract class AbstractClass
{
public void templateMethod() //模板方法
{
primitiveOperation1();
primitiveOperation2();
primitiveOperation3();
}
public void primitiveOperation1() //基本方法—具体方法
{
//实现代码
}
public abstract void primitiveOperation2(); //基本方法—抽象方法
public void primitiveOperation3() //基本方法—钩子方法
{
}
}
//典型的具体子类
public class ConcreteClass extends AbstractClass
{
public void primitiveOperation2()
{
//实现代码
}
public void primitiveOperation3()
{
//实现代码
}
}

12.4. 分析

  1. 模板方法 Template Method:定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。不能被修改,往往是final。
  2. 基本方法 Primitive Method:实现具体步骤的方法。
    1. 钩子方法:挂钩方法或空方法,控制算法中某些可选的步骤
    2. 抽象方法:提供具体的必选行为的变化
    3. 具体方法
  3. 模板方法模式是体现继承优点的模式之一,恰当使用继承,把可复用的一般性代码一道父类里
  • 优点
    1. 父类调用子类的操作,通过子类扩展新的行为,符合开闭原则
    2. 每个不同实现都需要定义子类,符合单一职责
  • 缺点
    1. 每个不同的实现都需要定义一个子类,导致类的个数增加

12.5. 适用性

  1. 一次性定义算法不变的部分,把可变的行为留给子类来实现
  2. 各个子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复
  3. 对一些复杂的算法进行分割
  4. 控制子类的扩展

12.6. 例子

12.6.1. 银行办理业务

在银行办理业务时,一般都包含几个基本步骤,首先需要号排队,然后办理具体业务,最后需要对银行工作人员进行评分。无论具体业务是取款、存款还是转账,其基本流程都一样。现使用模板方法模式模拟银行业务办理流程。

不管办理哪种业务,都只需要执行process()
image.png

12.6.2. 数据库操作模板

对数据库的操作一般包括连接、打开、使用、关闭等步骤,在数据库操作模板类中我们定义了 connDB() 、 openDB() 、useDB() 、 closeDB() 四个方法分别对应这四个步骤。对于不同类型的数据库(如 SQL server 和 Oracle) ,其操作步骤都一致,只是连接数据库 connDB() 方法有所区别,现使用模板方法模式对其进行设计。

不同数据库的连接方式不同,因此把连接操作延迟到子类中:
image.png

12.7. 扩展

  1. 好莱坞原则:子类不显示调用父类,而是通过覆盖父类的方法实现某些具体的业务逻辑,父类控制对子类的调用
  2. 和工厂方法的联系:都封装了具体的任务,把任务的具体实现延迟到子类中。如果模板中的任务只有对象创建,两者就是一致的。
  3. 钩子方法
    1. 最简单的实现是空方法,也可以定义一个默认实现。如果子类不覆盖钩子方法,则执行父类的默认实现。
    2. 复杂一点的钩子方法可以对其他方法进行约束,通常返回boolean类型,用来判断是否执行某一个基本方法。

13. 适配器模式

13.1. 动机

  • 在适配器模式中可以定义一个包装类,包装不兼容接口的对象,这个包装类指的就是适配器(Adapter),它所包装的对象就是适配者 (Adaptee),即被适配的类。
  • 让接口不兼容的类也能一起工作

13.2. 定义

  • 适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作
    • 别名为**包装器(Wrapper)**。
  • 适配器模式既可以作为类结构型模式,也可以作为对象结构型模式

13.3. 结构

Adapter复用Target的接口。复用Adaptee的实现

  1. Target:抽象目标类,定义客户要用的特定领域接口
  2. Adapter:调用另一个接口,作为转换器,对适配者和抽象目标类进行适配
  3. Adaptee:适配者是被适配的对象,已经定义了一个接口,这个接口需要被适配成Target中的接口。
  4. Client:调用Target中的业务方法。

13.3.1. 对象适配器

  1. 适配器转发Client对Target的request()请求
  2. Adater合成封装一个Adaptee
  3. 适配器更灵活,可以适配多个适配者,灵活更改适配者

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
//典型的对象适配器代码
public class Adapter extends Target
{
private Adaptee adaptee;
public Adapter(Adaptee adaptee)
{
this.adaptee=adaptee;
}
public void request()
{
adaptee.specificRequest();
}
}

13.3.2. 类适配器

Java用不了,不支持多继承。

Adapter继承Adaptee;
复用Adaptee的实现,可以用继承,也可以用合成。但继承可以修改Adaptee的实现。

image.png

1
2
3
4
5
6
7
8
//经典的类适配器代码
public class Adapter extends Adaptee implements Target
{
public void request()
{
specificRequest();
}
}

13.4. 适用环境

  1. 需要使用现有的类,但接口不符合系统的需要。
  2. 想建立一个可以重复使用的类,用于与一些彼此之间没有太大联系的类,包括一些将来可能引进的类一起工作。

13.5. 扩展

13.5.1. 默认适配器模式(Default Adapter Pattern)

  • 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并**为该接口中每个方法提供一个默认实现(空方法)**, 那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
  • 适用于一个接口不想使用其所有的方法的情况
  • 因此也称为单接口适配器模式

image.png

13.5.2. 双向适配器

  • 适配器中同时包含对目标类和适配者类的引用,适配器可以通过它调用目标类中的方法,目标类也可以通过它调用适配者中的方法

image.png

13.6. 例子

13.6.1. 仿生机器人

image.png

13.6.2. 加密适配器

某系统需要提供一个加密模块,将用户信息(如密码等机密信息)加密之后再存储在数据库中,系统已经定义好了数据库操作类。为了提高开发效率,现需要重用已有的加密算法,这些算法封装在一些由第三方提供的类中,有些甚至没有源代码。使用适配器模式设计该加密模块,实现在不修改现有类的基础上重用第三方加密方法

image.png

14. 组合模式

14.1. 动机

对于树形结构,容器对象(如文件夹)的某个方法被调用时,将遍历整个树型结构,修找也包含这个方法的成员对象(可能是容器对象(文件夹),也可以是叶子对象(文件))并执行。

虽然容器对象和叶子对象有区别,但客户端希望统一处理。

组合模式描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器对象和叶子对象,这就是组合模式的模式动机

14.2. 定义

  • 组合模式(Composite Pattern):组合多个对象形成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。
    • 可以减少定义新的类型

14.3. 结构

叶子对象和容器对象都可能继承到一些不属于自己的方法

结构与命令模式类似

image.png

14.4. 分析

  1. 定义一个抽象的Component类,统一处理leaf和copomsite
  2. 容器对象与抽象构件类之间由聚合关系,实现递归

14.5. 扩展

  1. 安全组合模式image.png

  2. 透明组合模式image.png

15. 桥接模式

15.1. 动机

image.png

对于有两个变化维度的系统,使用方案二设计系统类更少,而且系统扩展更方便。

15.2. 定义

  • 桥接模式(Bridge Pattern):将抽象部分和实现部分相分离,使得它们可以独立地变化。
    • 柄体模式(Handle and Body)/接口(Interface)模式
    • 是继承关系的替换

15.3. 结构

image.png

  1. Abstraction:抽象类
  2. RefinedAbstraction:扩充抽象类
  3. Implementor:实现类接口
  4. ConcreteImplementor:具体实现类

用户不知道对象是合成的还是继承的。

15.4. 分析

  1. 抽象化Abstraction:忽略一些信息,把对象共同性质抽取出来形成类
  2. 实现化Implementation:针对抽象化给出的具体实现
  3. 脱耦:把抽象化和实现化之间的耦合脱开,强关联改成弱关联,继承关系改成关联关系

如果只有一端变化,合成和继承产生的类是一样的。
两端变化,且可以独立变化适合桥接。
如果两端变化且不独立,继承和组合都不合适。

15.5. 扩展

  • 桥接模式和适配器模式联合
    • 桥接用于系统的初步设计。当发现系统与已有类无法协同工作时,可以采用适配器模式
    • image.png

16. 装饰模式

16.1. 动机

对用户透明的方式动态地给一个对象附加上更多责任,用户不觉得对象在装饰前后有什么不同。对用户透明的方式动态地给一个对象附加上更多责任
装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展

希望在一个对象上附加数量不定的责任,需要合成关系可递归

16.2. 定义

  • 装饰模式(Decorator Pattern):动态给一个对象增加一些额外的职责。就增加对象功能来说,装饰模式比生成此类实现更加灵活。
    • 别名包装器Wrapper,与适配器别名相同,但是它们适用于不同场合
  • 对象结构型模式

16.3. 结构

image.png

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
//抽象装饰器类
public class Decorator extends Component
{
private Component component;
public Decorator(Component component)
{
this.component=component;
}
public void operation()
{
component.operation();
}
}
//具体装饰器类
public class ConcreteDecorator extends Decorator
{
public ConcreteDecorator(Component component)
{
super(component);
}
public void operation()
{
super.operation();
addedBehavior();
}
public void addedBehavior()
{
//新增方法
}
}

16.4. 分析

  1. 关联关系不会破坏类的封装性,而且继承是一种耦合较大的静态关系,无法在程序运行时动态扩展。虽然关联关系不能减少代码,但是因为松耦合,更容易维护。关联的缺点是需要创建更多对象。
  2. 装饰模式,动态扩展比继承更灵活,可以不增加子类,动态地给对象附加职责。

符合开闭原则,具体构件类和具体装饰器类可以独立变化

16.5. 适用环境

  1. 动态、透明给对象添加职责
  2. 也可以动态撤销职责
  3. 当不能采取继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。

16.6. 例子

多重加密系统:某系统提供了一个数据加密功能,可以对字符串进行加密。最简单的加密算法通过对字母进行移位来实现,同时还提供了稍复杂的逆向输出加密,还提供了更为高级的求模加密。用户先使用最简单的加密算法对字符串进行加密,如果觉得还不够可以对加密之后的结果使用其他加密算法进行二次加密,当然也可以进行第三次加密。现使用装饰模式设计该多重加密系统。

image.png

17. 外观模式

17.1. 动机

引入外观角色,用户只需要直接与外观角色交互,用户与子系统的复杂关系由外观角色来实现,从而降低系统的耦合度。

image.png

17.2. 定义

  • 外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的 一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
  • 外观模式又称为门面模式。
  • 对象结构型模式

17.3. 结构

迪米特法则,外部内部通信都通过facade,细节被屏蔽,符合最小知识(狭义迪米特)

image.png

1
2
3
4
5
6
7
8
9
10
11
12
public class Facade
{
private SubSystemA obj1 = new SubSystemA();
private SubSystemB obj2 = new SubSystemB();
private SubSystemC obj3 = new SubSystemC();
public void method()
{
obj1.method();
obj2.method();
obj3.method();
}
}

17.4. 分析

单一职责,引入外观对象,作为各个子系统的单一入口
迪米特法则

17.5. 使用场景

简化接口
对用户屏蔽子系统

17.6. 优缺点

  1. 无法很好地限制用户使用子系统,如果对用户访问子系统做太多限制则减少了可变性和灵活性
  2. 在不引入抽象类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背开闭原则

17.7. 例子

17.7.1. 电源总开关

现在考察一个电源总开关的例子,以便进一步说明外观模式。为了使用方便,一个电源总开关可以控制四盏灯、一个风扇、一台空调和一台电视机的启动和关闭。通过该电源总开关可以同时控制上述所有电器设备,使用外观模式设计该系统。

image.png

17.7.2. 文件加密

某系统需要提供一个文件加密模块,加密流程包括三个操作,分别是读取源文件、加密、保存加密之后的文件。读取文件和保存文件使用流来实现,这三个操作相对独立,其业务代码封装在三个不同的类中。现在需要提供一个统一的加密外观类,用户可以直接使用该加密外观类完成文件的读取、加密和保存三个操作,而不需要与每一个类进行交互,使用外观模式设计该加密模块。

image.png

17.8. 扩展

17.8.1. 一个系统多个外观类

通常,一个系统只需要一个外观类,一般设计为单例。

但是,一个系统也可以设计多个外观类,每个外观了都负责和一些特定子系统交互,向用户提供相应的业务功能。

17.8.2. 抽象外观类

解决外观模式违反开闭原则的问题。

加入抽象外观类之后,只需要增加一个新的具体外观类,同时通过修改配置文件达到不用修改源代码并更换外观类的目的。
image.png

18. 享元模式

18.1. 动机

对象数量过多,导致运行代价过高。通过共享模式实现相同或相似对象的重用。

把对象不变的部分和变化的部分分开,用到的时候把两个组合起来。

外部状态(Intrinsic State):变化部分
内部状态(Extrinsic State):不变部分,可以共享的部分

常常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool) 用于存储具有相同内部状态的享元模式。

18.2. 定义

运用共享技术实现大量细粒度对象的复用

结构型模式

18.3. 结构

没有完成对象创建,外部状态从外部引入然后组合

image.png

19. 代理模式

19.1. 动机

引入新对象来实现对真实对象的操作或将新对象作为真实对象的一个替身
用户禁止使用原来的对象

19.2. 定义

proxy pattern:由代理对象控制对源对象的引用

Surrogate

Proxy Pattern: Provide a surrogate or placeholder for another object to control access to it.

虚拟代理:缩略图,不看内部内容的时候只返回一个很小的图、

19.3. 结构

和装饰区别:不需要动态添加职责,不用递归

image.png

20. 其他

20.1. 继承和合成的区别?

  1. 继承:复用接口、类型定义,不太容易改变;白盒继承,拥有父类所有代码,复用代码时可以部分改变父类代码
  2. 合成:复用实现,容易改变;黑盒,不易改变对象内部实现,只能完全更换对象
  • 标题: 01-设计模式
  • 作者: Charlie
  • 创建于 : 2024-03-05 15:03:00
  • 更新于 : 2024-07-05 12:55:04
  • 链接: https://chillcharlie357.github.io/posts/946442dc/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
01-设计模式