24-Spring-软件系统设计期末复习

SYuan03 Lv4

参考 zzb学长 | zhy学长 | xqh学长

lec21-软件系统设计复习

致敬 SEBugMaker quas-modo

zh老师部分不太好整理,花体看的眼花,说的道理

设计模式 |图说设计模式

题型

Important

学长建议架构设计看往年卷,设计模式看ppt

简答+设计

软件模式

image-20240621103632968

image-20240621103643421

大三律Rule Of Three

在模式发现过程中需要遵循大三律(Rule of Three),即只有经过三个以上不同类型(或不同领域)的系统的校验,一个解决方案才能从候选模式升格为模式。

OOA/OOD/OOP

image-20240620232511155

OOA、OOD和OOP是面向对象开发的三个关键步骤。

  1. 面向对象分析(OOA):这是软件开发过程中的第一步,主要目标是理解问题域,并确定软件需要完成的功能。在OOA过程中,分析师会识别出问题域中的关键对象(或称为实体)、对象的属性以及对象之间的关系和交互。结果通常以用例模型,领域模型等表示
  2. 面向对象设计(OOD):在完成OOA之后,接下来就是OOD,也就是确定如何实现这些功能。设计师会考虑如何将分析阶段识别出的对象映射到具体的软件组件,并设计这些组件的内部结构,以及组件之间的交互。设计师还需要考虑非功能性需求,如性能、可用性、安全性等。结果通常以类图、状态图、交互图等形式表示。
  3. 面向对象编程(OOP):在OOD阶段完成后,接下来就是实现设计的具体代码,这就是OOP阶段。程序员会使用面向对象的编程语言(如Java、C++、Python等)来编写代码,实现设计中定义的类和接口。

总的来说,OOA、OOD和OOP是面向对象开发的三个关键步骤,它们分别对应于软件开发过程中的需求分析系统设计编程实现。这三个步骤是紧密相连的,前一个步骤的输出将作为后一个步骤的输入,从而保证整个开发过程的连贯性和一致性。

设计模式与类/库框架

为什么不能建立一个包含所有设计模式的库,让人们不需要自己去创建?

  • 设计模式是比库更高级的概念。设计模式提供了如何结构化类和对象以解决特定问题的指导,但具体实现需要根据具体应用进行调整。设计模式的本质是它们的通用性和灵活性,而不是作为直接可用的代码提供。

库和框架是否也是设计模式?

  • 库和框架本身不是设计模式。它们提供特定的代码实现,可以被直接集成到我们的代码中。然而,库和框架有时会在其实现中使用设计模式,这是有益的,因为一旦你理解了设计模式,就能更快地理解基于设计模式构建的API。

面向对象设计原则

设计模式是设计原则的体现

  1. 这些原则并不是孤立存在的,它们相互依赖,相互补充。
  2. 原则之间也会有相互的作用

Important

设计原则

  1. 目标:开闭原则
  2. 指导:最小知识原则
  3. 基础:单一职责原则、可变性封装原则(对OCP的具体描述)
  4. 实现:依赖倒转原则合成复用原则里氏代换原则接口隔离原则

image-20240621132132392

面向对象设计原则注重软件的可维护性,可复用性和可扩展性

Single Responsibility Principle, SRP

定义

⼀个对象应该只包含单⼀的职责,并且该职责被完整地封装在⼀个类中

Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class.

从可维护性的角度考虑:对一个类而言,仅有一个引起它变化的原因

There should never be more than one reason for a class to change.

分析

一个类所承担的职责越多,它被复用的可能性越小

类的职责主要包括两个方面:

  1. 数据职责:数据职责通过其属性来体现。
  2. 行为/功能职责:而行为职责通过其方法来体现。

单一职责原则是实现高内聚、低耦合的指导方针

实例

C/S系统中的 Login类 包含了 数据库连接,显示窗口,查询用户等,违反了SRP,重构(使用分层结构)如下:

image-20240621205344048

Open-Closed Principle, OCP

定义

一个软件实体应当对扩展开放,对修改关闭

Software entities should be open for extension, but closed for modification.

分析

  1. 软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
  2. 抽象化是开闭原则的关键。
  3. 开闭原则还可以通过一个更加具体的"对可变性封装原则"来描述,对可变性封装原则(Principle of Encapsulation of Variation, EVP)要求找到系统的可变因素并将其封装起来。

与其他人职责的关系

  1. 我们需要知道系统的哪些部分是变化的,开闭原则也是对单一职责原则的增强。
  2. 里氏代换原则依赖倒置原则都是开闭原则的具体实现。

实例

某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,用户可能会改变需求要求使用不同的按钮,原始设计方案上利用抽象化+反射机制改进

抽象化:设计了AbstractButton

image-20240621221506730

Java反射机制参考

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用方法的功能成为反射机制。

Liskov Substitution Principle, LSP

UML关系,泛化关系=继承关系

关联和依赖:

前者通常具有长期性,持有引用,如 LibraryBook 的关系,一个 Library 持有多个 Book(聚合也是关联的一种)

后者感觉是运行时会用到,短期性,如 OrderProcessorOrder 以及 PaymentProcessor 的关系

在这里插入图片描述

定义

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象

通俗定义:在软件中如果能够使用基类对象,那么一定能够使用其子类对象。

分析

里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象

由该原则引出的一条建议:在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

Dependence Inversion Principle, DIP

定义

  1. 高层模块不应该依赖低层模块,它们都应该依赖抽象抽象不应该依赖于细节,细节应该依赖于抽象。High level modules should not depend upon low level modules, both should depend upon abstractions. Abstractions should not depend upon details, details should depend upon abstractions.

  2. 【另一种表述】要针对接口编程,不要针对实现编程。

分析

  1. 简单来说,依赖倒转原则就是指:
    • 代码要依赖于抽象的类,而不要依赖于具体的类;
    • 要针对接口或抽象类编程,而不是针对具体类编程
  2. 依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件
    • 将抽象放进代码,将细节放进元数据
    • Put Abstractions in Code, Details in Metadata

类之间的耦合

  1. 零耦合关系:最好情况
  2. 具体耦合关系(比如组合关系)
  3. 抽象耦合关系:依赖倒转要求至少一端是抽象的

Note

在面向对象设计中,**耦合(Coupling)**是描述两个或者多个类之间关系的一个术语。耦合强度的高低直接影响到代码的可维护性和可扩展性。

a. **零耦合关系:**这是最理想的情况,表示两个类之间没有任何关系,修改一个类不会影响到另一个类。

b. **具体耦合关系:**这是一种较强的耦合关系,表示一个类依赖于另一个类的具体实现。这种情况下,如果一个类发生了变化,可能会影响到依赖于它的所有类。

c. **抽象耦合关系:**这是一种较弱的耦合关系,表示一个类依赖于另一个类的抽象(例如,接口或抽象类)。这种情况下,如果抽象没有变化,那么具体的实现可以自由地变化,不会影响到依赖于抽象的类。

依赖倒转原则(Dependency Inversion Principle)就是要求我们在设计类和类之间的关系时,尽可能地使它们成为抽象耦合关系。也就是说,一个类应该依赖于抽象,而不是依赖于具体的实现。这样可以使得代码更加灵活和可扩展,因为抽象的变化通常远小于具体实现的变化。

实例

由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类MainClass都需要修改源代码,以便使用新的类,但违背了开闭原则。现使用依赖倒转原则+反射机制对其进行重构。

image-20240621234959863

Interface Segregation Principle, ISP

组合聚合不一定就产生了具体耦合,比如可以持有抽象引用

定义

  1. 客户端不应该依赖那些它不需要的接口

    Clients should not be forced to depend upon interfaces that they do not use.

  2. 另一种:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
    Once an interface has gotten too ‘fat’ it needs to be split into smaller and more specific interfaces so that any clients of the interface will only know about the methods that pertain to them.

分析

  1. 使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。
  2. 可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。

实例

下图展示了一个拥有多个客户类的系统,在系统中定义了一个巨大的接口(胖接口,fat接口)AbstractService来服务所有的客户类。可以使用接口隔离原则对其进行重构。

image-20240622003533028

Composite Reuse Principle, CRP

又称为组合/聚合复用原则(Composition/ Aggregate Reuse Principle,CARP)

定义

尽量使用对象组合,而不是继承来达到复用的目的。

Favor composition of objects over inheritance as a reuse mechanism.

组合与聚合

  1. 聚合是弱拥有关系,部分可以脱离整体存在;组合是强拥有关系,同生命周期
  2. 聚合是空心菱形箭头,组合是实心菱形箭头

组合:鸟与翅膀。就是关系强;聚合:大雁与雁群。就是关系弱

组合聚合vs继承

简言之:要尽量使用组合/聚合实现复用,少用慎用继承来实现复用

均是实现复用的方法

在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承

  1. **继承复用:**实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。("白箱"复用)
  2. 组合/聚合复用:耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。("黑箱"复用)

继承是强耦合,都"is-a"了还不强?

实例

image-20240622111226459

左边的都叫Util还能去继承也是挺…

左边的问题在于如果来了新的扩展困难,会违反OCP目标

左右设计对比ByGPT

Law of Demeter, LoD

定义

  1. 不要和"陌生人"说话。英文定义为:Don’t talk to strangers.
  2. 只与你的直接朋友通信。英文定义为:Talk only to your immediate friends.
  3. 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。英文定义为:Each unit should have only limited knowledge about other units: only units “closely” related to the current unit.

分析

简单地说,迪米特法则就是指一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易

在迪米特法则中,对于一个对象,其朋友包括以下几类:

  1. 当前对象本身(this)
  2. 以参数形式传入到当前对象方法中的对象
  3. 当前对象的成员对象
  4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
  5. 当前对象所创建的对象(比如某个方法是创建了某个对象返回)

a. 狭义迪米特法则

如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用

优点:可以降低类之间的耦合(直接关联的数量少了自然低了),可以使一个系统的局部设计简化

缺点:会在系统中增加大量的小方法并散落在系统的各个角落,会造成系统的不同模块之间的通信效率降低,使得系统的不同模块之间不容易协调。

img

b. 广义迪米特法则

对对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制

总结:感觉就是狭义的控制的太死了,广义的才算传达一种思想理念

狭义的迪米特法则:具体化,限制直接对象交互,强调局部设计和低耦合。

广义的迪米特法则:概念化,控制信息流量和隐藏,强调模块独立和系统整体性。

迪米特法则的主要用途在于控制信息的过载

  1. 在类的划分上,应当尽量创建松耦合的类
  2. 在类的结构设计上,应当尽量降低其成员变量和成员函数的访问权限
  3. 在类的设计上,一个类型应当设计成不变类
  4. 在对其他类的引用上,一个对象对其他对象的引用应当降到最低

第三条,不变类的状态在创建后不能被修改,这种设计可以帮助减少对象间的复杂依赖。

实例

image-20240622114137553

思考

在JDK 中,java.util.Stack是java.util.Vector类的子类,该设计合理吗?若不合理,请分析解释该设计存在的问题。

  1. 违反LSP(里式替换原则)和合成复用CRP
    • 行为不一致:根据LSP,子类对象应该能够替换任何父类出现的地方,而且保证原来的行为不变。然而,Stack的行为(后入先出)与Vector(可以在任何位置插入和删除元素)的行为是不一致的。
  2. 更好的方式应该是使用合成复用原则,将让stack “has-a” vector,但也有其他的一些问题

(不考)表驱动

https://cchroot.github.io/2020/05/23/表驱动法/

简单讲是指用查表的方法获取值。

表驱动法是一种编程模式 (scheme),从表里面查找信息而不使用逻辑语句 (if 和 case)。表驱动法的另一个好处是可以将复杂逻辑从代码中独立出来,以便于单独维护

image-20240622123138373

使用表驱动法的两个问题

  1. 在表里存放什么信息

    • 主要存放的是数据,但在一些特殊情况下也存放动作
  2. 如何快速从表中查询条目

    • 直接访问 (Direct access)
      • 最直观朴素的,使用键值对
    • 索引访问 (Indexed access)
      • 间接访问
      • 对于索引访问表,它适用于这样的情况,假设你经营一家商店,有100种商品,每种商品都有一个 id 号,但很多商品的描述都差不多,所以 只有30条不同的描述,现在的问题是建立商品与商品描述的表,如何建立?还是同上面的直接访问表的做法来一一对应吗?那样描述会扩充到100的,会有70个描述是重复的!如何解决这个问题呢?方法是建立一个100长的索引,然后这些索引指向相应的描述,注意不同的索引可以指向相同的描述,这样就解决了表数据冗余的问题啦。
      • image-20240622124042939
    • 阶梯访问 (Stair-step access)
      • 它适用于数据不是一个固定的值,而是一个范围的问题,比如将百分制成绩转成五级分制(我们用的优、良、中、合格、不合格,西方用的 A、B、C、D 和F)
      • 申请一个 100 长度的表,然后在这个表中填充相应的等级就行了?但这样太浪费空间了,有没有更好的方法?
      • image-20240622124249873

设计模式

设计模式(Design Pattern)是一套被反复使用多数人知晓的经过分类编目的代码设计经验的总结

设计模式的基本要素

1.模式名称 (Pattern name) 2.问题 (Problem) 3.解决方案 (Solution) 4.效果 (Consequences)

设计模式的分类

  • 根据其目的(模式是用来做什么的)可分为**创建型(Creational),结构型(Structural)和行为型(Behavioral)**三种:
    1. 创建型模式主要用于创建对象。
    2. 结构型模式主要用于处理类或对象的组合。
    3. 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责。
  • 根据范围,即模式主要是用于处理类之间关系还是处理对象之间的关系,可分为类模式和对象模式两种:
    1. 类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是属于静态的。
    2. 对象模式处理对象间的关系,这些关系在运行时刻变化,更具动态性。

image-20240621125730304

Simple Factory Pattern(类模式+创建型模式)

又称为**静态工厂方法(Static Factory Method)**模式

因为Java一般会提供一个static的方法,这样就不必实例化工厂对象。

在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

工厂角色提供静态工厂方法来创建其他类的实例,这样子满足了原则:

  1. 满足LoD。减少了客户端与具体实现类的直接交互。客户端只需知道如何使用工厂方法,而不需要了解具体类的创建细节。这符合迪米特法则的要求,减少了对象之间的耦合度。
  2. 满足SRP。将对象的创建和使用分离(创建本质上和使用是强耦合的),将复杂度降低,并将变化的部分和不变的部分分离开。
  3. 但是对OCP的支持不太好。内部根据参数用if else返回不同的类,要新增类的话这里就会违反OCP了。也就是说简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 简单工厂类
public class SimpleFactory {
public static Product createProduct(String type) {
switch (type) {
case "ProductA":
return new ProductA();
case "ProductB":
return new ProductB();
default:
throw new IllegalArgumentException("Unknown product type");
}
}
}

示例

根据不同的权限等级创建不同等级的用户对象,不同等级的用户对象拥有不同的操作权限。现使用简单工厂模式来设计该权限管理模块。

img

实线三角:继承;虚线箭头:依赖

优点

  1. (SRP)工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅"消费"产品,从而实现了对责任的分割,它提供了专门的工厂类用于创建对象。
  2. (LoD)客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
  3. 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。

缺点

  1. 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响(单个职责过大)
  2. 增加了系统中类的个数,在一定程序上增加了系统的复杂度和理解难度
  3. 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时会违背OCP原则
  4. 无法形成基于继承的等级结构

适用场景

在以下情况下可以使用简单工厂模式:

  1. 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂(如果扩展使比较少的)
  2. 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数(比如只知道名称参数)

模式应用

  1. 在JDK类库中广泛使用了简单工厂模式,如工具类java.text.DateFormat,它用于格式化一个本地日期或者时间。

    image.png

  2. Java加密技术:对称加密和非对称加密,但是变化比较少

    1
    Cipher encryptCipher = Cipher.getInstance("RSA");

模式扩展

**简单工厂模式的简化:**在有些情况下工厂类可以由抽象产品角色扮演,一个抽象产品类同时也是子类的工厂,也就是说把静态工厂方法写到抽象产品类中。

img

Factory Method Pattern(类模式+创建型模式)

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类创建型模式

延迟

  1. 在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
  2. 工厂方法是以继承方法为主,工厂子类是继承工厂父类的。

img

工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责哪一个产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品

很好的符合了OCP

示例

img

优点

用户无需关心创建细节;工厂自主确定,封装细节在具体工厂;易加入新产品

  1. 用户只需要关心所需产品对应的工厂,无须关心创建细节
  2. 工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。
  3. 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。

缺点

类太多了,并且看起来很复杂

  1. 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  2. 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。

适用场景(感觉不重要

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

Abstract Factory Pattern(对象模式+创建型模式)

有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象

两个概念:

  1. 产品等级结构:不同品牌的同一产品
    • 如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构
  2. 产品族:同一品牌的不同产品。
    • 海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。

区分:

工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构

image-20240622153307169

如上图,抽象工厂模式包含如下角色:

  1. AbstractFactory:抽象工厂
  2. ConcreteFactory:具体工厂
  3. AbstractProduct:抽象产品
  4. Product:具体产品

示例

数据库操作工厂。某系统为了改进数据库操作的性能,自定义数据库连接对象Connection和语句对象Statement,可针对不同类型的数据库提供不同的连接对象和语句对象,如提供Oracle或SQL Server专用连接类和语句类,而且用户可以通过配置文件等方式根据实际需要动态更换系统数据库。

img

优点

  1. 隔离了具体类的生成,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。实现了高内聚低耦合
  2. 能够保证客户端始终只使用同一个产品族中的对象(具体工厂是不同的牌子)
  3. 增加新的具体工厂和产品族很方便,无须修改已有系统,符合OCP

缺点

在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。

开闭原则的倾斜性体现在此:增加新的工厂和产品族容易,增加新的产品等级结构麻烦

加一个新的品牌很方便,但是新的产品(产品等级结构),比如品牌123都生产ABC,加一个品牌4也生产123很简单,但是想加一个生产D,就比较麻烦了,会违反OCP

适用场景

  1. 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
  2. 系统中有多于一个的产品族,而每次只使用其中某一产品族。(对应优点2)并且属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
  3. 所有的产品以同样的接口出现,从而使客户端不依赖于具体实现

模式扩展

工厂模式的退化

抽象工厂 -》工厂方法 -》简单工厂

牌子123生产ABC(抽象工厂模式)->只有一个产品等级结构,如只生产A(工厂方法模式)->抽象工厂合并到具体工厂,只用一个具体工厂(简单工厂模式

(不考)Builder Pattern(对象模式+创建型模式)

生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

复用相同的对象构造代码

一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示

img

组装汽车、KFC套餐

生成器模式结构示例

Prototype Pattern(对象模式+创建型模式)

原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 克隆方法。

在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建,原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后通过复制这个原型对象的办法创建出更多同类型的对象,这就是原型模式的意图所在。

img

+表示public,-表示private

  1. 抽象原型类是定义具有克隆自己的方法的接口(比如java中就是Object)
  2. 具体原型类实现具体的克隆方法,在克隆方法中返回自己的一个克隆对象

java.lang.Object | Cloneable接口

1
2
3
4
5
6
7
8
9
10
11
public class PrototypeDemo implements Cloneable{
public Object clone(){
Object object = null;
try {
object = super.clone();
} catch (CloneNotSupportedException exception) {
System.err.println("Not support cloneable");
}
return object;
}
}

Java语言提供的clone()方法将对象复制了一份并返回给调用者。一般而言,clone()方法满足:

  1. 对任何的对象x,都有x.clone() !=x,即克隆对象与原对象不是同一个对象。
  2. 对任何的对象x,都有x.clone().getClass()==x.getClass(),即克隆对象与原对象的类型一样。
  3. 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

深克隆和浅克隆

浅克隆:只克隆对象本身,成员对象的引用保持不变。适用于成员对象不需要独立变化的情况。

深克隆:克隆对象及其所有成员对象。适用于需要完全独立的克隆对象的情况。

java中的clone()方法是浅克隆

在 Java 中,Object 类提供了一个 clone() 方法,这个方法默认实现的是浅克隆(Shallow Clone)。浅克隆的含义是,当一个对象被克隆时,只复制对象本身,而不复制对象包含的引用类型成员对象。即,克隆对象和原对象共享同一个引用类型成员对象。

示例

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

img

优点

  1. 简化对象的创建过程
  2. 可以动态增加或减少产品类
  3. 可以使用深克隆的方式保存对象的状态。

缺点

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

适用场景

  1. 创建新对象成本较大

  2. 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候

  3. 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态

    比如按钮有开关状态,犯不着用工厂类,直接复制一个改下状态就行

模式扩展

带原型管理器的原型模式

img

就是用一个hashtable来存取管理下

(不考)Singleton Pattern(对象模式+创建型模式)

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

Adapter Pattern(类/对象模式+结构型模式)

https://refactoringguru.cn/design-patterns/adapter
这里的两张图很清晰

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

对象适配器

img

Target可以抽象或者具体

类适配器

img

Target只能抽象

  • Target 目标抽象类
  • Adapter 适配器类
  • Adaptee 适配者类
  • Client 客户类

对象适配器

适配器实现了其中一个对象的接口, 并对另一个对象进行封装。

image-20240622170211690

类适配器

适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现, 例如 C++

image-20240622170229492

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

示例

仿生机器人。现需要设计一个可以模拟各种动物行为的机器人,在机器人中定义了一系列方法,如机器人叫喊方法cry()、机器人移动方法move()等。如果希望在不修改已有代码的基础上使得机器人能够像狗一样叫,像狗一样跑,使用适配器模式进行系统设计。

img

类适配器,适配器的方法对应Target的方法

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

img

对象适配器,与Caesar是组合关系

优点

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
  2. 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
  3. 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合"开闭原则"。
类适配器的优缺点

优点:由于Adapter是Adaptee的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强

缺点:对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。

对象适配器模式

优点:同一个适配器可以把适配者类和它的子类都适配到目标接口(因为是用的组合,而不是继承

缺点:想要置换适配者类的方法不容易(因为不是继承,只是关联/组合/聚合这样,继承的话可以重新实现方法之类的

适用场景

在以下情况下可以使用适配器模式:

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

模式应用

每一个具体数据库引擎(如SQL Server、Oracle、MySQL等)的JDBC驱动软件都是一个介于JDBC接口和数据库引擎接口之间的适配器软件

模式扩展

默认适配器模式(Default Adapter Pattern)或缺省适配器模式

当不需要全部实现接口提供的方法时,可以设计一个适配器抽象类实现接口,并为接口中的每个方法提供默认方法,抽象类的子类就可以有选择的覆盖父类的某些方法实现需求,它适用于一个接口不想使用所有的方法的情况。**在java8后,接口中可以有default方法,就不需要这种缺省适配器模式了。**接口中方法都设置为default,实现为空,这样同样可以达到缺省适配器模式同样的效果。

就是给接口先搞一个抽象类,弄点默认方法,然后子类可以有选择地去覆盖,其他的方法保持默认,因为java8之前接口类还没有default关键字,现在没啥用了这个

img

双向适配器(bidirectional adapter)

作为Target和Adaptee之间的互调工具人

在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器

img

Composite Pattern(对象模式+结构型模式)

由于容器对象和叶子对象在功能上的区别,在使用这些对象的客户端代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下客户端希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂

组合模式又可以称为整体-部分(Part-Whole)模式

组合多个对象形成树形结构表示"整体-部分"的结构层次。组合模式对单个对象(即叶子对象)组合对象(即容器对象)使用具有一致性

组合模式包含如下角色:

  1. Component: 抽象构件
  2. Leaf: 叶子构件
  3. Composite: 容器构件
  4. Client: 客户类
img

右边的聚合关系意思就是,容器内可以有多个Component(Leaf或者Composite)

组合模式的关键定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。

同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构

示例

img

水果盘。在水果盘(Plate)中有一些水果,如苹果(Apple)、香蕉(Banana)、梨子(Pear),当然大水果盘中还可以有小水果盘,现需要对盘中的水果进行遍历(吃),当然如果对一个水果盘执行"吃"方法,实际上就是吃其中的水果。使用组合模式模拟该场景。

img

优点

  1. 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
  2. 客户端可以一致的使用组合结构或其中单个对象
  3. 定义了包含叶子对象和容器对象的类层次结构,可以形成复杂的树形结构
  4. 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。(继承下就行,反正容器放的是抽象构件对象

缺点

  1. 使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
  2. 增加新构件时可能会产生一些问题,很难对容器中的构件类型进行限制

模式应用

XML文档解析 | 操作系统中的目录结构 | JDK的AWT/Swing

JDK的AWT/Swing有Component,Container之类的

模式扩展

更复杂的组合模式

img

组合模式根据抽象构件类的定义形式,又可以分为透明组合模式和安全组合模式。

透明组合模式(其实就是普通组合模式?

img

统一了叶子节点和组合节点的接口

优点:简化客户端代码,因为它对待叶子节点和组合节点无需区分。

缺点:叶子节点包含了一些本不应支持的操作,例如添加或删除子节点,这违反了设计的最小知识原则,即一个类不应该包含它不使用的方法。

安全组合模式:违反了里氏代换原则

img

Component只放共有的operation操作

安全性高,一个类不接触它不需要的接口。

Decorator Pattern(对象模式+结构型模式)

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

动态地给一个对象增加一些额外的职责(Responsibility),其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。

装饰模式也有人翻译为"油漆工模式",它是一种对象结构型模式

img

递归构建,上面部分很像组合模式(容器+叶子)。ConcreteComponent和Decorator只有能不能继续装饰的区别,是一种终止条件,其实没有本质区分。

装饰模式包含如下角色:

  1. Component: 抽象构件
  2. ConcreteComponent: 具体构件
  3. Decorator: 抽象装饰类
  4. ConcreteDecorator: 具体装饰类
1
2
3
4
5
6
7
8
9
10
11
12
public class Decorator extends Component
{
private Component component;
public Decorator(Component component)
{
this.component=component;
}
public void operation()
{
component.operation();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConcreteDecorator extends Decorator
{
public ConcreteDecorator(Component component)
{
super(component);
}
public void operation()
{
super.operation();
addedBehavior();
}
public void addedBehavior()
{
//新增方法
}
}

其实感觉就是包了一层,持有了component,所以叫wrapper也合理

示例

变形金刚。变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔。

img

变成Robot

Transform car = new Car()

Transform robot = new Robot(car)

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

img

优点

  1. 装饰模式可以提供比继承更多的灵活性
  2. 通过一种动态的方式来扩展一个对象的功能
  3. 排列组合可以创造很多不同行为的组合
  4. 具体构件类与具体装饰类可以独立变化,符合“开闭原则”

缺点

  1. 产生小对象,系统复杂
  2. 易出错,难排错

模式扩展

如果只有一个具体构件类而没有抽象构件类,那么抽象装饰类可以作为具体构件类的直接子类

可以简化如右图

image-20240622212238300

透明装饰模式(多重加密系统)

在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型。

1
2
3
4
Cipher sc,cc,ac; 
sc=new SimpleCipher();
cc=new ComplexCipher(sc);
ac=new AdvancedCipher(cc);

半透明装饰模式(变形金刚)

**半透明(semi-transparent)**的装饰模式允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。

1
2
3
4
5
6
Transform camaro; 
camaro=new Car();
camaro.move();
Robot bumblebee=new Robot(camaro);
bumblebee.move();
bumblebee.say(); // 调用具体装饰者中新增的方法

Facade Pattern(对象模式+结构型模式)

外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式又称为门面模式,它是一种对象结构型模式。

外观模式是SRPLoD法则的体现

img

Facade:外观角色

SubSystem: 子系统角色

1
2
3
4
5
6
7
8
9
10
11
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();
}
}

示例

案例:电源总开关

img

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

img

优点

  1. 客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易
  2. 实现了子系统与客户之间的松耦合关系
  3. 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程
  4. 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类

缺点

  1. 不能很好地限制客户使用子系统类
  2. 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”

模式应用

Session外观模式是外观模式在Java EE框架中的应用。

img

模式扩展

  1. 一个系统卡哇伊有多个外观类,通常会是单例,也可以多个
  2. 不要试图通过外观类为子系统增加新行为
  3. 外观模式和迪米特法则
    • 外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代。外观类充当了客户类与子系统类之间的“第三者”,降低了客户类与子系统类之间的耦合度,外观模式就是实现代码重构以便达到“迪米特法则”要求的一个强有力的武器。
  4. 抽象外观类的引入

抽象外观类

  • 外观模式最大的缺点在于违背了“开闭原则”,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程
  • 对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。
image-20240622215105460

Template Method Pattern(类模式+行为型模式)

img

定义

定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法是一种行为型模式。(因为是基于继承的代码复用方式嘛)

结构

img

模板方法模式包含如下角色:

  1. AbstractClass: 抽象类
  2. ConcreteClass: 具体子类

模式分析

  1. 模板方法模式是一种类的行为型模式,在它的结构图中只有类之间的继承关系,没有对象关联关系

  2. 在模板方法模式的使用过程中,要求开发抽象类和开发具体子类的设计师之间进行协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。实现这些具体逻辑步骤的方法称为基本方法(Primitive Method)而将这些基本法方法汇总起来的方法称为模板方法(Template Method),模板方法模式的名字从此而来。

  3. 模板方法即templateMethod:一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。(用于组合的那个方法)

  4. 基本方法(有三种):基本方法是实现算法各个步骤的方法,是模板方法的组成部分。

    1. 抽象方法(Abstract Method)

    2. 具体方法(Concrete Method)

    3. 钩子方法(Hook Method):挂钩方法和空方法

      • 挂钩方法:让子类可以影响父类中定义的算法的执行路径,从而让模板方法中的某些步骤是可选的。比如下列例子中列举的isPrint()算法。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        // 钩子方法示例
        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
31
32
33
34
35
36
37
38
39
40
public abstract class AbstractClass {
// 模板方法,定义算法的骨架
public final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
primitiveOperation3();
}

// 基本方法 - 具体方法
public void primitiveOperation1() {
// 实现代码
System.out.println("Operation 1");
}

// 基本方法 - 抽象方法,由子类实现
public abstract void primitiveOperation2();

// 基本方法 - 钩子方法,提供默认实现,子类可以选择性重写
public void primitiveOperation3() {
// 默认实现
System.out.println("Default hook operation 3");
}
}

public class ConcreteClass extends AbstractClass {
// 实现抽象方法
@Override
public void primitiveOperation2() {
// 实现代码
System.out.println("Concrete operation 2");
}

// 重写钩子方法
@Override
public void primitiveOperation3() {
// 实现代码
System.out.println("Customized hook operation 3");
}
}

看起来钩子方法和具体方法好像没啥区别,应该是用处不太一样?

示例

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

img

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

img

优点

  • 模板方法模式在一个类中抽象地定义算法,而由它的子类实现细节的处理。
  • 模板方法模式是一种代码复用的基本技术。
  • 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”

缺点

每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,但是更加符合“单一职责原则”,使得类的内聚性得以提高。

适用环境

  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
  • 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。
  • 控制子类的扩展

模式应用

  1. 模板方法模式广泛应用于框架设计(如Spring,Struts等)中,以确保父类控制处理流程的逻辑顺序(如框架的初始化)。

  2. Java单元测试工具JUnit中的TestCase类的设计

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void runBare() throws Throwable {
    setUp();
    try {
    runTest();
    }
    finally {
    tearDown();
    }
    }

模式扩展

关于继承的讨论

  1. 模板方法模式鼓励我们恰当使用继承,此模式可以用来改写一些拥有相同功能的相关类,将可复用的一般性的行为代码移到父类里面,而将特殊化的行为代码移到子类里面。模板方法模式就是体现继承优势的模式之一

好莱坞原则

  1. 在模板方法模式中,子类不显式调用父类的方法,而是通过覆盖父类的方法来实现某些具体的业务逻辑,父类控制对子类的调用,这种机制被称为好莱坞原则(Hollywood Principle),好莱坞原则的定义为:“不要给我们打电话,我们会给你打电话(Don‘t call us, we’ll call you)”。
  2. 在模板方法模式中,好莱坞原则体现在:子类不需要调用父类,而通过父类来调用子类,将某些步骤的实现写在子类中,由父类来控制整个过程。

钩子方法的使用

  1. 钩子方法的引入使得子类可以控制父类的行为。
  2. 最简单的钩子方法就是空方法或者在钩子方法中定义一个默认的实现,如果子类不覆盖钩子方法,则执行父类的默认实现代码。
  3. 比较复杂一点的钩子方法可以对其他方法进行约束,这种钩子方法通常返回一个boolean类型,即返回true或false,用来判断是否执行某一个基本方法。(挂钩方法?)

Command Pattern(对象模式+行为型模式)

模式动机:发送者和接收者完全解耦(请求和执行解耦),invoker只知道发送命令,不需要知道如何完成命令

将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化对请求排队或者记录请求日志,以及支持可撤销的操作

别名为动作(Action)模式或事务(Transaction)模式。

  • Command 抽象命令类
  • ConcreteCommand 具体命令类
  • Invoker 调用者
  • Receiver 接收者
  • Client 客户类
img

Client和ConcreteCommand之间的关联是因为在创建Command的时候需要把Receiver给它

invoker下达指令执行某个Command,然后command利用持有的receiver去执行,执行调用的是receiver的一些action

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
public abstract class Command{
public abstract void execute();
}

public class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void setCommand(Command command) {
this.command = command;
}
public void call() {
command.execute();
}
}

public class ConcreteCommand extends Command {
private Receiver receiver;
public void execute() {
receiver.action();
}
}

public class Receiver {
public void action() {
// Do something
}
}

分析

  1. 命令模式的本质是对命令进行封装将发出命令的责任和执行命令的责任分割开
  2. 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作
  3. 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
  4. 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
  5. 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联

顺序图(有助理解)

img

示例

电视机遥控器。电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。显然,电视机遥控器就是一个典型的命令模式应用实例。

img

功能键设置。为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,如功能键FunctionButton可以用于退出系统(SystemExitClass),也可以用于打开帮助界面(DisplayHelpClass)。用户可以通过修改配置文件来改变功能键的用途,现使用命令模式来设计该系统,使得功能键类与功能类之间解耦,相同的功能键可以对应不同的功能。

img

优点

  1. 降低系统耦合度
  2. 新的命令可以很容易地加入到系统中
  3. 可以比较容易地设计一个命令队列宏命令
  4. 可以方便地实现对请求的Undo和Redo

缺点

导致系统有过多的具体命令类

模式适用

将请求调用者和请求接收者解耦

  1. 需要在不同的时间指定请求、将请求排队和执行请求
  2. 需要支持命令的撤销操作和回复操作
  3. 需要将一组操作组合在一起

撤销操作的实现

img

在Command的抽象和实现里,加个undo就行了,执行相反/撤销操作

宏命令

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

img

Composite Command就用不着关联Receiver了

Mediator Pattern(对象模式+行为型模式)

模式动机

img

在用户与用户直接聊天的设计方案中,用户对象之间存在很强的关联性,将导致系统出现如下问题:

  1. 系统结构复杂:对象之间存在大量的相互关联和调用,若有一个对象发生变化,则需要跟踪和该对象关联的其他所有对象,并进行适当处理。
  2. 对象可重用性差:由于一个对象和其他对象具有很强的关联,若没有其他对象的支持,一个对象很难被另一个系统或模块重用,这些对象表现出来更像一个不可分割的整体,职责较为混乱。
  3. 系统扩展性低:增加一个新的对象需要在原有相关对象上增加引用,增加新的引用关系也需要调整原有对象,系统耦合度很高,对象操作很不灵活,扩展性差。

根据SRP,我们应该尽量将对象细化,使其只负责或呈现单一的职责

对于一个模块,可能由很多对象构成,而且这些对象之间可能存在相互的引用,为了减少对象两两之间复杂的引用关系,使之成为一个松耦合的系统,我们需要使用中介者模式,这就是中介者模式的模式动机。

模式定义

用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互

中介者模式又称为调停者模式,它是一种对象行为型模式

模式结构

img

中介者模式包含如下角色:

  1. Mediator: 抽象中介者
  2. ConcreteMediator: 具体中介者
  3. Colleague: 抽象同事类
  4. ConcreteColleague: 具体同事类

模式分析

中介者模式可以使对象之间的关系数量急剧减少:

img

中介者承担两方面的职责:

  1. 中转作用(结构性):通信时通过Mediator进行中转。该中转作用属于中介者在结构上的支持
  2. 协调作用(行为性):中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持

典型代码

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
46
47
48
import java.util.ArrayList;

public abstract class Mediator {
protected ArrayList<Colleague> colleagues = new ArrayList<>();

public void register(Colleague colleague) {
colleagues.add(colleague);
}

public abstract void operation();
}

public class ConcreteMediator extends Mediator {
@Override
public void operation() {
// 示例操作,调用第一个同事的方法1
if (!colleagues.isEmpty()) {
colleagues.get(0).method1();
}
}
}

public abstract class Colleague {
protected Mediator mediator;

public Colleague(Mediator mediator) {
this.mediator = mediator;
}

public abstract void method1();
public abstract void method2();
}

public class ConcreteColleague extends Colleague {
public ConcreteColleague(Mediator mediator) {
super(mediator);
}

@Override
public void method1() {
// 具体实现
}

@Override
public void method2() {
mediator.operation();
}
}

示例

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

img

注意ChatGroup的sentText和Member的sendText参数不一样(多一个from),这就是中介者式的关键了,题目中的权限控制的话直接在各自的具体类做就行了

优点

  • 简化了对象之间的交互
  • 将各同事解耦
  • 减少子类生成
  • 可以简化各同事类的设计和实现

缺点

在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂使得系统难以维护。

适用环境

  1. 对象之间存在复杂的引用关系
  2. 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象**。**
  3. 想通过一个中间类来封装多个类的行为,又不想生成太多子类。改变行为可以增加新的中介者类。

应用

  1. 事件驱动类(GUI组件之间的交互)
  2. MVC - Controller作为一种中介者img

模式扩展

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

将系统中有关的对象所引用的其他对象数目减少到最少,因此,中介者模式就是迪米特法则的一个典型应用。

2.中介者模式与GUI开发

中介者模式可以方便地应用于图形界面(GUI)开发中,在比较复杂的界面中可能存在多个界面组件之间的交互关系。

对于这些复杂的交互关系,有时候我们可以引入一个中介者类,将这些交互的组件作为具体的同事类,将它们之间的引用和控制关系交由中介者负责,在一定程度上简化系统的交互,这也是中介者模式的常见应用之一。

Observer Pattern(对象模式+行为型模式)

image.png

建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。

  1. 发生改变的对象称为观察目标
  2. 被通知的对象称为观察者

一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是观察者模式的模式动机。

定义

观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新

观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。

  • publisher, subscriber这种其实用的更多

模式结构

  1. Observer抽象观察者
  2. ConcreteObserver具体观察者
  3. Subject抽象主题:目标,会拥有一个observers列表,方法上会有比如attach,detach,notify等
  4. ConcreteSubject具体主题

img

  1. 一般说抽象,不区分抽象类或者抽象接口,但在观察者模式,明确区分了目标对象是抽象类,观察者是抽象接口,为什么?

    • 对于Subject类,可以复用attach和detach的实现。类承担了通信职责和数据职责。
    • 对于Observer类,update的方法都一样,数据观察是通用的。其实只有一个通信职责,所以接口就差不多了,没必要抽象类?
      • 个人感觉还是主要出于java不能多继承的考虑…,设计成接口更灵活的去implement
    • 更好的方法是,subject和observer都声明成抽象接口。因为通信职责和本来的职责正交,可以分解。
  2. 观察者对象其实持有目标对象的引用(图上没画,可见后面具体代码),目的是什么?

    • 对于observer来说,是否监听应该由observer决定(调用attach和detach)。
    • 不能称作对subject的依赖,这个持有引用是由观察者模式决定的。

松耦合

img

松耦合不是依赖倒转原则,而是最小知识原则

而依赖倒置原则则是说,高层模块不应该依赖低层模块,两者都应该依赖抽象。

因为依赖倒转是一方具体一方抽象?而这里是两边都是松耦合

示例

image.png

代码

image-20240623104518370

如上文所说,具体观察者类持有对于Subject的引用,因为其实Observer这边才是主导的一端,具有主动性

优点

  • 实现表示层和数据逻辑层的分离,使得可以有不同的表示层作为具体观察者角色
  • 在观察目标和观察者之间建立一个抽象的耦合(松耦合)
  • 支持广播通信
  • 符合开闭原则

缺点

  • 观察者太多,通知所有观察者耗时长
  • 如果有循环依赖->可能会有系统崩溃的风险
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

模式适用

一个改变->多个改变,不知具体有多少对象,不知道是谁

需要在系统中创建一个触发链

Java语言提供的对观察者模式的支持

在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了Java语言对观察者模式的支持。

img

MVC模式

算是中介者+观察者?(思想上算是结合了

  • 模型Model:目标
  • 视图View :观察者
  • 控制器Controller:中介者

当模型层的数据发生改变时,视图层将自动改变其显示内容。

img

push/pull模式

当数据量差不多的时候,使用push模式。subject把数据都传给observer。

当数据需要定制化更新的时候,使用pull模式,subject告知更新,observer自己update所需属性。

**观察者模式又分为两种模式:push和pull。**push是指suject在状态变化时将所有的状态信息都发给observer,pull则是suject通知observer更新时,observer获取自己感兴趣的状态。

两种模式在实现上的区别

  1. push模式下,observer的update方法接收的是状态信息
  2. pull模式下,update方法接收的是suject对象,这种情况下,suject须提供状态信息的get方法,让observer可以获取自己感兴趣的信息。

两种模式的优劣

  1. push模式要求suject必须了解observer需要的状态,pull则是observer按需获取
  2. push模式下observer的update方法的参数是具体的状态信息,发生变化时必须要重写update方法,pull模式则是将suject对象本身传给update,是最大的参数集合。

State Pattern(对象模式+行为型模式)

一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫作状态,这样的对象叫做有状态的(stateful)对象。

状态模式的关键引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换

状态是最可能变化的地方,需要做一个封装。

img

状态模式包含如下角色

  • Context: 环境类
  • State: 抽象状态类
  • ConcreteState: 具体状态类

在结构上策略模式和状态模式是一致的,但是在使用上是很不同的

  • Context是状态模式关联的上下文环境
  • 策略模式是封装好的,而状态模式为了追求对用户透明则牺牲了开闭原则(状态的切换在Context中无法避免修改

具体耦合

继承是强耦合

状态模式存在具体耦合,状态切换写在环境类中比较好,因为都有具体耦合,都写在一个地方比较好,不至于太分散。因此状态模式不能完全支持OCP。因为产生了具体耦合,存在从一个状态类转变到另一个状态类,就必定会有具体耦合

房间入住示例

img

https://refactoringguru.cn/design-patterns/state

房间类里面的比如预订等操作应该是调用状态的

state.预订()

论坛用户等级示例(感觉有点乱,不看也罢)

img

上图的checkState位置其实可以放在account?

同时注意到环境类ForumAccount和状态类AbstractState是相互持有引用的,为什么不合并为一个类?将设置状态的职责抽离出来,更符合状态管理类的单一职责。

状态的转换是最重要的,什么时候由环境类转换,什么时候由状态类转换?

由属性(积分)转变引发的转换vs由行为(入住退房)引发的转换:属性由环境类承担

所有类的转换行为一样vs与现在处于哪个状态有关

优点

  1. 封装了转换原则

  2. 枚举了可能的状态

  3. 可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。

    新增状态有可能破坏OCP,因为状态转换的代码会要变

  4. 状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。

  5. 让多个环境对象共享状态对象(多个房间都是预定的状态),从而减少系统中对象的个数。

缺点

  1. 状态模式的使用必然会增加系统类和对象的个数
  2. 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱
  3. 对”开闭原则“的支持并不太好,产生了具体耦合(在具体对象中切换)

适用环境

  1. 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为
  2. 代码中包含大量与对象状态有关的条件语句

如工作流、游戏

状态模式扩展

  1. 共享状态:在有些情况下多个环境对象需要共享同一个状态,如果希望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象
  2. **简单状态模式:**状态都相互独立,**状态之间无须进行转换(额…)**的状态模式。它遵循“开闭原则”,可以在客户端实例化状态类,可以将具体状态类写入配置文件。
  3. **可切换状态的状态模式(大多数状态模式):**在具体状态类内部需要调用环境类Context的setState()方法进行状态的转换操作。

Strategy Pattern(对象模式+行为型模式)

别名:Policy Pattern

定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换一个类的行为或其算法可以在运行时更改。

引入示例的解决:

想让鸭子飞起来……(添加fly的方法),可是鸭子会飞,橡皮鸭和木鸭子不会飞:

  1. 鸭子抽象类实现fly,不会飞的自己覆盖掉
  2. 实现flyable的接口,会飞的自己接口

单独设计行为类,动态修改鸭子的行为

1
2
3
4
5
6
7
8
// 聚合鸭子的行为
public class Duck{
QuackBehavior quackBehavior;

public void performQuack{
quackBehavior.quack();
}
}

用到的设计原则:

单一职责 、依赖倒转、合成复用

启发:

  1. 所有子类都一样,在父类中实现
  2. 子类都不一样,在子类中实现
  3. 部分子类一样,在接口中实现
img

环境类(Context):维护一个指向策略对象的引用。

抽象策略类(Strategy):定义所有支持的算法的公共接口。

具体策略类(Concrete Strategy):实现抽象策略定义的接口,提供具体的算法实现。

Applicability-适用性

  1. 许多相关的类仅在行为上有所不同,策略提供了一种使用多种行为之一配置类的方法。
  2. 您需要算法的不同变体。**例如,您可能定义了反映不同空间/时间权衡的算法。将这些变体实现为算法的类层次结构时,可以使用策略。**往往实现的是相同的行为,但是是不同的算法
  3. 一种算法使用客户端不应该知道的数据。使用策略模式可避免暴露复杂的、特定于算法的数据结构
  4. **一个类定义了许多行为,这些行为在其操作中显示为多个条件语句。**代替许多条件,将相关的条件分支移到他们自己的策略类中。

Consequences-结果

一个策略模式的潜在缺陷:客户必须意识到不同的策略。即客户在选择合适的策略之前必须先了解策略的不同,不然客户可能会遇到实现问题。

  • 用户需要去了解实现的细节
  • 其实是破坏了封装

模式一般都会有的缺点:

  1. 增加设计的复杂度和增加类的个数(增加辅助类)

一些疑惑

  1. 为什么工厂方法模式是类创建型,抽象工厂模式就是对象创建型了

    A:个人理解是工厂方法模式通过定义一个创建对象的接口,让子类决定实例化哪一个类。其重点在于让子类来决定创建哪种具体类型的对象,因此更强调继承关系,因此是类模式;抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。其重点在于产品族的创建,通过组合多个产品对象来实现不同的品牌或系列,因此是对象模式。

  • 标题: 24-Spring-软件系统设计期末复习
  • 作者: SYuan03
  • 创建于 : 2024-06-19 18:07:06
  • 更新于 : 2024-06-23 12:00:17
  • 链接: https://bblog.031105.xyz/posts/期末复习/软件系统设计/24-spring-软件系统设计期末复习.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
24-Spring-软件系统设计期末复习