- 概述
- 面向对象设计原则
- 软件设计
- 面向对象软件设计
- 如何发现合适的对象
- 可维护性和可复用性
- :star:面向对象设计原则概述
- :sob:妈妈我看不懂类图了怎么办
- 单一职责原则(Single Responsibility Principle, SRP)
- 开闭原则(Open-Closed Principle, OCP)
- 里氏代换原则(Liskov Substitution Principle, LSP)
- 依赖倒转原则(Dependence Inversion Principle, DIP)
- 接口隔离原则(Interface Segregation Principle, ISP)
- 合成复用原则(Composite Reuse Principle, CRP)
- 迪米特法则(Law of Demeter, LoD) / 最小知识原则(Least Knowledge Principle, LKP)
- 策略模式(Strategy / Policy Pattern)
- 工厂模式
- 创建型模式
- 状态与命令模式
- 行为型模式
- 适配器和组合
- 桥接与装饰者
- 外观模式、享元模式和代理模式
- 附录
[TOC]
这篇笔记实际上是对熊佬笔记(EagleBear2002 的博客)的整合,所以致谢熊佬。
概述
软件模式
软件模式是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。它并非仅限于设计模式,还包括架构模式、分析模式和过程模式等。
软件模式可以认为是对软件开发这一特定“问题”的“解法”的某种统一表示,软件模式等于一定条件下的出现的问题以及解法。软件模式的基础结构由4个部分构成:问题描述、前提条件(环境或约束条件)、解法和效果。
软件模式与具体的应用域无关,在模式发现过程中需要遵循大三律(Rule of Three),即只有经过三个以上不同类型(或不同领域)的系统的校验,一个解决方案才能从候选模式升格为模式。
【2021】软件模式是什么?能提供架构吗?
:star:设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
关键元素包括:模式名称、问题、解决方案、效果
设计模式的分类:
根据其目的可以分为创建型、结构型和行为型三种:
- (创建型)创建对象
- (结构型)处理类或对象的组合
- (行为型)描述对类或对象怎样交互和怎样分配职责
根据其范围可以分为类模式和对象模式两种:
- 类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时就被确定,是静态的。
- 对象模式处理对象间的关系,这些关系在运行时变化,是动态的。
【2019】设计模式是什么?举例说明类模式和对象模式的区别?
面向对象设计原则
软件设计
需求定义了系统需要满足的目标
规约定义了系统的外部可观察到的行为
架构定义了系统一级的主要组成部分,各部分的交互方法和使用的技术
设计定义了如何完成任务和需要写的代码
面向对象软件设计
将实现的约束条件应用到面向对象分析所产生的概念模型的过程
用方法和属性来描述用于构成系统的类
添加不明显属于领域的类,比如抽象类和接口
描述类是如何构成组件的
如何发现合适的对象
OOD的难点在于将一个系统分解成对象,许多对象直接来自于分析模型或实现空间(数据库、文件、用户界面、IPC)
可维护性和可复用性
一个我不认识但是PPT上说是知名的大师认为一个可维护性较低的软件设计通常由于如下4个原因造成:
- 过于僵硬
- 过于脆弱
- 复用率低
- 黏度过高
另一位大师认为,一个好的系统设计应该具备如下三个性质:
- 可扩展性
- 灵活性
- 可插入性
软件的复用(Reuse)或重用拥有众多优点,如可以提高软件的开发效率,提高软件质量,节约开发成本,恰当的复用还可以改善系统的可维护性。
- 部分复用会破坏系统的可维护性,A 和 B 修改 C,如果 A 需要 C 多提供一个行为,但是 B 不需要则会出现问题。
- 可维护性和可维护性虽然有共性,但是是独立的属性。
面向对象设计复用的目标在于实现支持可维护性的复用。
在面向对象的设计里面,可维护性复用都是以面向对象设计原则为基础的,这些设计原则首先都是复用的原则,遵循这些设计原则可以有效地提高系统的复用性,同时提高系统的可维护性。
面向对象设计原则也是对系统进行合理重构的指南针,重构(Refactoring)是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
:star:面向对象设计原则概述
【2017】请⾄少说出三个⾯向对象的原则,并解释它们如何应⽤于策略模式?
:sob:妈妈我看不懂类图了怎么办
车的类图结构为
<<abstract>>,表示车是一个抽象类;它有两个继承类:小汽车和自行车;它们之间的关系为
实现关系,使用带空心箭头的虚线表示,箭头指向接口;小汽车为与SUV之间也是继承关系,它们之间的关系为
泛化关系(is-a),使用带空心箭头的实线表示,箭头指向父类;小汽车与发动机之间是
组合关系(部分不能离开整体而单独存在),使用带实心菱形的箭头表示,菱形指向整体;(第一张图的箭头不规范,看下面的图)学生与班级之间是
聚合关系(has-a,个体可以离开整体而单独存在),使用带空心菱形的实线表示,菱形指向整体;(第一张图的箭头不规范,看下面的图)学生与身份证之间为
关联关系,使用一根实线(实线是因为双向关联,如果是单向关联要画实线箭头,箭头指向目标类)表示;(在Java中,单向关联表现为:类A当中使用了类B,其中B作为类A的成员变量。即一个对象含有另一个对象的引用)
学生上学需要用到自行车,与自行车是一种
依赖关系(一个类的实现需要另一个类的协助),使用带箭头的虚线表示,箭头指向目标类;
单一职责原则(Single Responsibility Principle, SRP)
定义:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。/ 就一个类而言,应该仅有一个引起它变化的原因。
分析:
- 一个类承担的责任越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
- 类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。
- 单一职责原则是实现高内聚、低耦合的指导方针,是最简单但又最难运用的原则。
实例:
对于如下的类进行单一职责原则重构:
classDiagram
class Login {
+init() void
+display() void
+validate() void
+getConnection() Connection
+findUser(String userName, String userPassword) boolean
+main(String args[]) void
}
重构后:
classDiagram
class MainClass {
+main(String args[]): void
}
class LoginForm {
-dao : UserDAO
+init() void
+display() void
+validate() void
}
class UserDAO {
-db : DBUtil
+findUser(String userName, String userPassword) boolean
}
class DBUtil {
+getConnection() Connection
}
%% Relationships
MainClass ..> LoginForm
LoginForm --> UserDAO
UserDAO --> DBUtil
开闭原则(Open-Closed Principle, OCP)
定义:一个软件实体应当对扩展开放,对修改关闭。也就是说在设计一个模块时,应当使这个模块可以在不被修改的前提下被扩展,即实现在不修改源代码的情况下改变这个模块的行为。(添加新的代码远比修改旧的代码简单得多。)
分析:
- 开闭原则中软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
- 抽象化是开闭原则的关键。
- 开闭原则还可以通过一个更加具体的“对可变性封装原则”来描述,对可变性封装原则(Principle of Encapsulation of Variation, EVP)要求找到系统的可变因素并将其封装起来。
实例:
某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这 些按钮进行编程,用户可能会改变需求要求使用不同的按钮,原始 设计方案如图所示:
现对该系统进行重构,使之满足开闭原则的要求:
里氏代换原则(Liskov Substitution Principle, LSP)
定义:
①如果对每一个类型为 $S$ 的对象 $O_1$,都有类型为 $T$ 的对象 $O_2$,使得以 $T$ 定义的所有程序 $P$ 在所有的对象 $O_2$ 都代换成 $O_1$ 时,$P$ 的行为没有变化,那么类型 $S$ 是类型 $T$ 的子类型。
②所有引用基类(父类)的地方必须能透明地使用其子类的对象。
分析:
- 通俗来讲就是在软件中如果能够使用基类对象,那么一定能够使用其子类对象。
- 里氏代换原则是实现开闭原则的重要方式之一,在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
实例:
某系统需要实现对重要数据(如用户密码)的加密处理,在数据操作类(DataOperator)中需要调用加密类中定义的加密算法,系统提供了两个不同的加密类,CipherA 和 CipherB,它们实现不同的加密方法,在 DataOperator 中可以选择其中的一个实现加密操作。如图所示:
用里氏代换原则重构后:
依赖倒转原则(Dependence Inversion Principle, DIP)
定义:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
人话:要针对接口编程,不要针对实现编程。
分析:
代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。
依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中。
类之间的耦合:①零耦合;②具体耦合;③抽象耦合。依赖倒转要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒转原则的关键。
【2021】依赖倒转原则是什么?如何反映在设计模式中?
实例:
某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如可以转换来自数据库的数据 (DatabaseSource)、也可以转换来自文本文件的数据 (TextSource),转换后的格式可以是XML文件 (XMLTransformer)、也可以是XLS文件(XLSTransformer)等。
由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类 MainClass 都需要修改源代码,以便使用新的类,但违背了开闭原则。现使用依赖倒转原则对其进行重构。
接口隔离原则(Interface Segregation Principle, ISP)
定义:客户端不应该依赖那些它不需要的接口。(一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。)
分析:
- 接口隔离原则是指用多个专门的接口,而不是用单一的总接口。(1)一个接口就只代表一个角色;(2)接口仅仅提供客户端需要的行为,不需要的则隐藏起来。
- 使用接口隔离原则拆分接口时首先必须满足单一职责原则,方法越少越好。
- 可以在进行系统设计时采用定制服务的方式,为不同的客户端提供宽窄不同的接口。
实例:
下图展示了一个拥有多个客户类的系统,在系统中定义了一个巨大的接口(胖接口)AbstractService 来服务所有的客户类。可以使用接口隔离原则对其进行重构。
重构后:
合成复用原则(Composite Reuse Principle, CRP)
定义:尽量使用对象组合,而不是继承来达到复用的目的。
分析:
在一个新对象里通过关联关系(包括组合和聚合)来使用已有的对象,使之成为新对象的一部分。新对象通过委派调用已有对象的方法达到复用其已有功能的目的。简言之:要尽量使用组合/聚合关系,少用继承。
在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。
继承复用:实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。(“白箱”复用)
组合/聚合复用:耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。(“黑箱”复用)
组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
实例:
某教学管理系统部分数据库访问类设计如图所示:
如果需要更换数据库连接方式,如原来采用 JDBC 连接数据库,现在采用数据库连接池连接,则需要修改 DBUtil 类源代码。如果 StudentDAO 采用 JDBC 连接,但是 TeacherDAO 采用连接池连接,则需要增加一个新的 DBUtil 类,并修改 StudentDAO 或 TeacherDAO 的源代码,使之继承新的数据库连接类,这将违背开闭原则,系统扩展性较差。
重构后:(从继承变成关联+聚合)
迪米特法则(Law of Demeter, LoD) / 最小知识原则(Least Knowledge Principle, LKP)
【2019】最小知识原则在设计模式中的应用?
- 中介者模式
- 外观模式
定义:
1)不要和陌生人说话
2)只与你的直接朋友通信
3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
分析:
讲人话就是一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。
在迪米特法则中,对于一个对象,其朋友包括以下几类:
- 当前对象本身(this)
- 以参数形式传入到当前对象方法中的对象
- 当前对象的成员对象
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
- 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
迪米特法则可分为狭义法则和广义法则。在狭义的迪米特法则中,如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。下图中,只允许 A 调用 B 对象的方法,但是不能调用 C 对象的方法(但是我们可以通过在 B 中添加一个 Wrapper 方法来间接调用 C)
狭义的迪米特法则:可以降低类之间的耦合,但是会在系统中增加大量的小方法并散落在系统的各个角落,它可以使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联,但是也会造成系统的不同模块之间的通信效率降低,使得系统的不同模块之间不容易协调。
广义的迪米特法则:指对对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制。信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用和修改,同时可以促进软件的复用,由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显。
迪米特法则的主要用途在于控制信息的过载:
- 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限
- 在类的设计上,只要有可能,一个类型应当设计成不变类
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低
实例:
某系统界面类(如 Form1、Form2 等类)与数据访问类(如 DAO1、DAO2 等类)之间的调用关系较为复杂,如图所示:
重构后:
策略模式(Strategy / Policy Pattern)
:duck:鸭子
:duck::Quack!
鸭子的例子说明要将行为抽象出来,要面向接口编程【例如将鸭子的Fly和Quack写成2个接口】(实现了依赖倒转),并且是完整的封装【具体的Fly和Quack只要在原有的接口上继承就行了,只扩展不删减】(开闭原则)
策略模式定义
策略模式(Strategy / Policy)定义了一系列算法,将每个算法封装在一起,并使它们可替换,策略使算法独立于使用该算法的客户端而变化
例如鸭子的飞行策略:
策略模式的应用
在以下情况下使用策略模式:
- 许多相关的类仅在行为上有所不同,策略提供了一种使用多种行为之一配置类的方法
- 您需要算法的不同变体。例如,您可能定义了反映不同空间/时间权衡的算法。将这些变体实现为算法的类层次结构时,可以使用策略。
- 一种算法使用客户端不应该知道的数据。使用策略模式可避免暴露复杂的、特定于算法的数据结构
- 一个类定义了许多行为,这些行为在其操作中显示为多个条件语句。代替许多条件,将相关的条件分支移到他们自己的策略类中。
很多问题都出现于数据结构被暴露:比如迭代器模式。
策略模式的优点
- 相关算法家族。策略类的层次结构定义了一系列算法或行为,以供上下文重用。继承可以帮助排除算法的通用功能。
- 子类化的替代方法。
- 策略消除条件语句。
条件语句是指以下这种代码:
public class Context{ public void algorithm(String type){ if(type.equals("strategyA")) { this.strategy = new ConcreteStrategyA(); } else if(type.equals("strategyB")) { this.strategy = new ConcreteStrategyB(); } else if(type.equals("strategyC")) { this.strategy = new ConcreteStrategyC(); } } }
- 多种实现方式。策略可以提供相同行为的不同实现。客户可以选择具有不同时间和空间权衡的策略。
策略模式的缺点
- 客户必须意识到不同的策略。这种模式有一个潜在的缺点,即客户在选择合适的策略之前必须先了解策略的不同,不然客户可能会遇到实现问题。
- 策略和上下文之间的通信开销。
- 对象数量增加。
共享模式词汇的力量
设计模式为您提供了与其他开发人员共享的词汇表。通过让您在模式级别(而不是实质性对象级别)进行思考,还可以提高您对体系结构的思考。
- 共享模式词汇的力量
- 当您使用模式与其他开发人员或团队进行沟通时,您不仅在沟通模式名称,还传达了模式所代表的整套质量属性,特征和约束
- 模式可以让您用更少的话表达更多。
- 当您在描述中使用模式时,其他开发人员会快速准确地了解您所考虑的设计。
- 在模式级别进行交谈可以使您在“设计中”停留的时间更长。
- 不要迷失在细节中。
- 共享词汇可以为您的开发团队提供强大的动力。
- 共享的词汇表鼓励更多的初级开发人员快速上手。
工具
- 面向对象的基础 OO Basics
- 抽象 Abstraction
- 封装 Encapsulation
- 多态性 Polymorphism
- 继承 Inheritance
- 面向对象原则 OO Principles
- 封装可变性 Encapsulate what varies
- 选择组合而不是继承 Favor composition over inheritance
- 面向接口编程,而不是面向实现编程 Program to interfaces, not implementation
- 面向对象模式 OO Patterns:策略 Strategy
工厂模式
简单工厂模式(Simple Factory Pattern)
定义:简单工厂模式(Simple Factory Pattern),又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
简单工厂模式包含如下角色:
- Factory:工厂角色
- Product:抽象产品角色
- ConcreteProduct:具体产品角色
分析:
- 将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。
- 在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可,在实际开发中,还可以在调用时将所传入的参数保存在 XML 等格式的配置文件中,修改参数时无须修改任何 Java 源代码。
- 简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。
- 简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
实例:
在某 OA 系统中,系统根据对比用户在登录时输入的账号和密码以及在数据库中存储的账号和密码是否一致来进行身份验证,如果验证通过,则取出存储在数据库中的用户权限等级(以整数形式存储),根据不同的权限等级创建不同等级的用户对象,不同等级的用户对象拥有不同的操作权限。现使用简单工厂模式来设计该权限管理模块。
模式扩展:
简单工厂模式的简化:在有些情况下工厂类可以由抽象产品角色扮演,一个抽象产品类同时也是子类的工厂,也就是说把静态工厂方法写到抽象产品类中。
适用:
- 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂(如果扩展使比较少的)
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数(比如只知道名称参数)
简单工厂模式的优点
- 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
简单工厂模式的缺点
- 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。只是把分散在系统各个地方的变化汇总到了一起。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
简单工厂模式的不足
在简单工厂模式中,只提供了一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,加入必要的处理逻辑,这违背了“开闭原则”。在简单工厂模式中,所有的产品都是由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。
工厂方法模式(Factory Method Pattern)
定义:工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
分析:
- 工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责哪一个产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。
- 当系统扩展需要添加新的产品对象时,仅仅需要添加一个具体产品对象以及一个具体工厂对象,原有工厂对象不需要进行任何修改,也不需要修改客户端,很好地符合了“开闭原则”。而简单工厂模式在添加新产品对象后不得不修改工厂方法,扩展性不好。工厂方法模式退化(抽象工厂和具体工厂合并)后可以演变成简单工厂模式。
// 抽象工厂类代码
public abstract class PayMethodFactory {
public abstract AbstractPay getPayMethod();
}
// 具体工厂类代码
public class CashPayFactory extends PayMethodFactory {
public AbstractPay getPayMethod() {
return new CashPay();
}
}
public class Main {
public static void main(String[] args) {
// 客户类代码片段
PayMethodFactory factory;
AbstractPay payMethod;
factory = new CashPayFactory();
payMethod = factory.getPayMethod();
payMethod.pay();
}
}
为了提高系统的可扩展性和灵活性,在定义工厂和产品时都必须使用抽象层,如果需要更换产品类,只需要更换对应的工厂即可,其他代码不需要进行任何修改。
配置文件代码:在实际的应用开发中,一般将具体工厂类的实例化过程进行改进,不直接使用 new 关键字来创建对象,而是将具体类的类名写入配置文件中,再通过 Java 的反射机制,读取 XML 格式的配置文件,根据存储在 XML 文件中的类名字符串生成对象。
<?xml version="1.0"?>
<config>
<className>CashPayFactory</className>
</config>
Java 反射(Java Reflection):是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、超类等信息,还包括实例的创建和实例类型的判断等。可通过 Class 类的 forName() 方法返回与带有给定字符串名的类或接口相关联的 Class 对象,再通过 newInstance() 方法创建此对象所表示的类的一个新实例,即通过一个类名字符串得到类的实例。
// 创建一个字符串类型的对象
Class c = Class.forName("String");
Object obj = c.newInstance();
return obj;
实例:
某系统日志记录器要求支持多种日志记录方式,如文件记录、数据库记录等,且用户可以根据要求动态选择日志记录方式,现使用工厂方法模式设计该系统。
适用:
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
- 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
工厂方法模式的优点
- 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
- 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
- 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
工厂方法模式的缺点
- 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到 DOM、反射等技术,增加了系统的实现难度。
抽象工厂模式(Abstract Factory Pattern)
有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。
抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构
- 产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL 电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
- 产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。
定义:抽象工厂模式(Abstract Factory Pattern)提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为 Kit 模式,属于对象创建型模式。
抽象工厂模式包含如下角色:
AbstractFactory:抽象工厂ConcreteFactory:具体工厂AbstractProduct:抽象产品Product:具体产品、
抽象工厂类的典型代码如下:
public abstract class AbstractFactory{
public abstract AbstractProductA createProductA();
public abstract AbstractProductB createProductB();
}
具体工厂类的典型代码如下:
public class ConcreteFactory1 extends AbstractFactory{
public AbstractProductA createProductA(){
return new ConcreteProductA1();
}
public AbstractProductB createProductB(){
return new ConcreteProductB1();
}
}
实例:
一个电器工厂可以产生多种类型的电器,如海尔工厂可以生产海尔电视机、海尔空调等,TCL 工厂可以生产 TCL 电视机、TCL 空调等,相同品牌的电器构成一个产品族,而相同类型的电器构成了一个产品等级结构,现使用抽象工厂模式模拟该场景。
某系统为了改进数据库操作的性能,自定义数据库连接对象 Connection 和语句对象 Statement,可针对不同类型的数据库提供不同的连接对象和语句对象,如提供 Oracle 或 SQL Server 专用连接类和语句类,而且用户可以通过配置文件等方式根据实际需要动态更换系统数据库。使用抽象工厂模式设计该系统。
适用:
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
- 系统中有多于一个的产品族,而每次只使用其中某一产品族。
- 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
抽象工厂模式的优点
- 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
- 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
抽象工厂模式的缺点
在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
开闭原则的倾斜性
“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的。对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:
- 增加产品族:对于增加新的产品族,工厂方法模式很好的支持了“开闭原则”,对于新增加的产品族,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。
- 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,不能很好地支持“开闭原则”。
抽象工厂模式的这种性质称为**“开闭原则”的倾斜性**,抽象工厂模式以一种倾斜的方式支持增加新的产品,它为新产品族的增加提供方便,但不能为新的产品等级结构的增加提供这样的方便。
工厂模式的退化
当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;当工厂方法模式中抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。
创建型模式
建造者模式(Builder Pattern)
定义:建造者模式(Builder Pattern),将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。建造者模式属于对象创建型模式。根据中文翻译的不同,建造者模式又可以称为生成器模式。
建造者模式包含如下角色:
Builder:抽象建造者ConcreteBuilder:具体建造者Director:指挥者Product:产品角色
分析:
一个典型的复杂对象其类代码示例如下:
public class Product {
private String partA; // 可以是任意类型
private String partB;
private String partC;
// partA 的 Getter 方法和 Setter 方法省略
// partB 的 Getter 方法和 Setter 方法省略
// partC 的 Getter 方法和 Setter 方法省略
}
抽象建造者类中定义了产品的创建方法和返回方法,其典型代码如下:
public abstract class Builder {
protected Product product = new Product();
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();
public Product getResult() {
return product;
}
}
建造者模式的结构中还引入了一个指挥者类 Director ,该类的作用主要有两个:一方面它隔离了客户与生产过程;另一方面它负责控制产品的生成过程。指挥者针对抽象建造者编程,客户端只需要知道具体建造者的类型,即可通过指挥者类调用建造者的相关方法,返回一个完整的产品对象。
指挥者类的代码示例如下:
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public void setBuilder(Builder builder) {
this.builder = builer;
}
public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}
客户端类代码片段:
Builder builder = new ConcreteBuilder();
Director director = new Director(builder);
Product product = director.construct();
实例:
建造者模式可以用于描述 KFC 如何创建套餐:套餐是一个复杂对象,它一般包含主食(如汉堡、鸡肉卷等)和饮料(如果汁、可乐等)等组成部分,不同的套餐有不同的组成部分,而 KFC 的服务员可以根据顾客的要求,一步一步装配这些组成部分,构造一份完整的套餐,然后返回给顾客。
适用:
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
建造者模式的优点
- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
- 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合”开闭原则”。
建造者模式的缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
建造者模式的应用
JavaMail(一步一步构造一个完整的邮件对象,然后发送)
// 由邮件会话对象新建一个邮件消息对象
MimeMessage message = new MimeMessage(session);
// 设置邮件地址
InternetAddress from = new InternetAddress("sunny@test.com");
message.setFrom(from);// 设置发件人
InternetAddress to = new InternetAddress(to_mail);
message.setRecipient(Message.RecipientType.TO, to);// 设置
收件人,并设置其接收类型为TO
message.setSubject(to_title);// 设置主题
message.setText(to_content);// 设置信件内容
message.setSentDate(new Date());// 设置发信时间
message.saveChanges();// 存储邮件信息
Transport transport = session.getTransport("smtp");
transport.connect("smtp.test.com", "test", "test");
transport.sendMessage(message, message.getAllRecipients());
地图或任务
在很多游戏软件中,地图包括天空、地面、背景等组成部分,人物角色包括人体、服装、装备等组成部分,可以使用建造者模式对其进行设计,通过不同的具体建造者创建不同类型的地图或人物。
建造者模式的比较
建造者模式的简化:
- 省略抽象建造者角色:如果系统中只需要一个具体建造者的话,可以省略掉抽象建造者。
- 省略指挥者角色:在具体建造者只有一个的情况下,如果抽象建造者角色已经被省略掉,那么还可以省略指挥者角色,让 Builder 角色扮演指挥者与建造者双重角色
建造者模式与抽象工厂模式的比较:
- 与抽象工厂模式相比,建造者模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族。
- 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中,客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。
- 如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
原型模式(Prototype Pattern)
原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象
定义:原型模式(Prototype Pattern)是一种对象创建型模式,用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节。
原型模式包含如下角色:
Prototype:抽象原型类ConcretePrototype:具体原型类Client:客户类
分析:
- 在原型模式结构中定义了一个抽象原型类,所有的 Java 类都继承自
java.lang.Object,而 Object 类提供一个clone()方法,可以将一个 Java 对象复制一份。因此在 Java 中可以直接使用 Object 提供的clone()方法来实现对象的克隆,Java 语言中的原型模式实现很简单。 - 能够实现克隆的 Java 类必须实现一个标识接口
Cloneable,表示这个 Java 类支持复制。如果一个类没有实现这个接口但是调用了clone()方法,Java 编译器将抛出一个CloneNotSupportedException异常。
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()方法满足:对任何的对象
x,都有x.clone() != x,即克隆对象与原对象不是同一个对象。对任何的对象
x,都有x.clone().getClass()==x.getClass(),即克隆对象与原对象的类型一样。如果对象
x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。
实例:
由于邮件对象包含的内容较多(如发送者、接收者、标题、内容、日期、附件等),某系统中现需要提供一个邮件复制功能,对于已经创建好的邮件对象,可以通过复制的方式创建一个新的邮件对象,如果需要改变某部分内容,无须修改原始的邮件对象,只需要修改复制后得到的邮件对象即可。使用原型模式设计该系统。在本实例中使用浅克隆实现邮件复制,即复制邮件(Email)的同时不复制附件(Attachment)。
游戏开发中创建多个相似NPC角色,为了方便角色创建可采用原型模式,在创建完单个NPC角色后,剩余角色则从这一角色通过复制后加上些微的变化而来。而这些复制来的NPC与原NPC具有相同的初始装备和状态,但相互独立。当一个NPC的状态发生变化时,比如其装备被损坏或加强时,我们并不希望这些装备变化同时反馈到其他NPC身上。(深克隆)
适用:
- 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其属性稍作修改。
- 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用原型模式配合备忘录模式来应用。相反,如果对象的状态变化很大,或者对象占用的内存很大,那么采用状态模式会比原型模式更好。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
原型模式的优点
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过一个已有实例可以提高新实例的创建效率。
- 可以动态增加或减少产品类。
- 原型模式提供了简化的创建结构。
- 可以使用深克隆的方式保存对象的状态。
原型模式的缺点
- 需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码。
原型模式扩展
带原型管理器的原型模式
相似对象的复制:很多情况下,复制所得到的对象与原型对象并不是完全相同的,它们的某些属性值存在异同。通过原型模式获得相同对象后可以再对其属性进行修改,从而获取所需对象。如多个学生对象的信息的区别在于性别、姓名和年龄,而专业、学院、学校等信息都相同,为了简化创建过程,可以通过原型模式来实现相似对象的复制。
状态与命令模式
状态模式(State Pattern / Objects for States)
定义:状态模式(State Pattern)允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
状态模式包含如下角色:
Context: 环境类State: 抽象状态类ConcreteState: 具体状态类
【2019】策略模式和状态模式的区别?
- 在状态模式中,具体状态类的方法参数中包含上下文对象,需要在状态处理完成后完成状态切换。
- 在策略模式中,直接对上下文类调用set方法设置策略即可,不涉及到策略的切换。
分析:
- 状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为。
- 状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。
- 使用状态模式重构之后的代码:
// 重构之后的“空闲状态类”示例代码
if (预订房间) {
// 预订操作;
context.setState(new 已预订状态类());
} else if (住进房间) {
// 入住操作;
context.setState(new 已入住状态类());
}
- 在状态模式结构中需要理解环境类与抽象状态类的作用:
- 环境类实际上就是拥有状态的对象,环境类有时候可以充当状态管理器(State Manager)的角色,可以在环境类中对状态进行切换操作。
- 抽象状态类可以是抽象类,也可以是接口,不同状态类就是继承这个父类的不同子类,状态类的产生是由于环境类存在多个状态,同时还满足两个条件:这些状态经常需要切换,在不同的状态下对象的行为不同。因此可以将不同对象下的行为单独提取出来封装在具体的状态类中,使得环境类对象在其内部状态改变时可以改变它的行为,对象看起来似乎修改了它的类,而实际上是由于切换到不同的具体状态类实现的。由于环境类可以设置为任一具体状态类,因此它针对抽象状态类进行编程,在程序运行时可以将任一具体状态类的对象设置到环境类中,从而使得环境类可以改变内部状态,并且改变行为。
实例:
在某论坛系统中,用户可以发表留言,发表留言将增加积分;用户也可以回复留言,回复留言也将增加积分;用户还可以下载文件,下载文件将扣除积分。该系统用户分为三个等级,分别是新手、高手和专家,这三个等级对应三种不同的状态,这三种状态分别定义如下:
- 如果积分小于 100 分,则为新手状态,用户可以发表留言、回复留言,但是不能下载文件。如果积分大于等于 1000 分,则转换为专家状态;如果积分大于等于 100 分,则转换为高手状态。
- 如果积分大于等于 100 分但小于 1000 分,则为高手状态,用户可以发表留言、回复留言,还可以下载文件,而且用户在发表留言时可以获取双倍积分。如果积分小于 100 分,则转换为新手状态;如果积分大于等于 1000 分,则转换为专家状态;如果下载文件后积分小于 0,则不能下载该文件。
- 如果积分大于等于 1000 分,则为专家状态,用户可以发表留言、回复留言和下载文件,用户除了在发表留言时可以获取双倍积分外,下载文件只扣除所需积分的一半。如果积分小于 100 分,则转换为新手状态;如果积分小于 1000 分,但大于等于 100,则转换为高手状态;如果下载文件后积分小于 0,则不能下载该文件。
适用:
- 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为。
- 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。在这些条件语句中包含了对象的行为,而且这些条件对应于对象的各种状态。
状态模式的优点
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
状态模式的缺点
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
状态模式扩展
共享模式:在有些情况下多个环境对象需要共享同一个状态,如果希望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象。
简单状态模式:简单状态模式是指状态都相互独立,状态之间无须进行转换的状态模式,这是最简单的一种状态模式。对于这种状态模式,每个状态类都封装与状态相关的操作,而无须关心状态的切换,可以在客户端直接实例化状态类,然后将状态对象设置到环境类中。如果是这种简单的状态模式,它遵循“开闭原则”,在客户端可以针对抽象状态类进行编程,而将具体状态类写到配置文件中,同时增加新的状态类对原有系统也不造成任何影响。
可切换状态的状态模式:大多数的状态模式都是可以切换状态的状态模式,在实现状态切换时,在具体状态类内部需要调用环境类 Context 的 setState() 方法进行状态的转换操作,在具体状态类中可以调用到环境类的方法,因此状态类与环境类之间通常还存在关联关系或者依赖关系。通过在状态类中引用环境类的对象来回调环境类的 setState() 方法实现状态的切换。在这种可以切换状态的状态模式中,增加新的状态类可能需要修改其他某些状态类甚至环境类的源代码,否则系统无法切换到新增状态。
命令模式(Command Pattern / Action / Transaction)
命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如 何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。
定义:命令模式(Command Pattern)将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
命令模式包含如下角色:
Command:抽象命令类ConcreteCommand:具体命令类Invoker:调用者Receiver:接收者Client:客户类
分析:
- 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
- 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。
- 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
- 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
- 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。
- 典型的抽象命令类代码:
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() {
// 具体操作
}
}
- 命令模式顺序图
实例:
电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。显然,电视机遥控器就是一个典型的命令模式应用实例。
为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,如功能键 FunctionButton 可以用于退出系统(SystemExitClass),也可以用于打开帮助界面(DisplayHelpClass)。用户可以通过修改配置文件来改变功能键的用途,现使用命令模式来设计该系统,使得功能键类与功能类之间解耦,相同的功能键可以对应不同的功能。
适用:
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起,即支持宏命令。
命令模式的优点
- 降低系统的耦合度。
- 新的命令可以很容易地加入到系统中。
- 可以比较容易地设计一个命令队列和宏命令(组合命令)。
- 可以方便地实现对请求的 Undo 和 Redo。
命令模式的缺点
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
命令模式扩展
撤销操作的实现
宏命令又称为组合命令,它是命令模式和组合模式联用的产物。宏命令也是一个具体命令,不过它包含了对其他命令对象的引用,在调用宏命令的 execute() 方法时,将递归调用它所包含的每个成员命令的 execute() 方法,一个宏命令的成员对象可以是简单命令,还可以继续是宏命令。执行一个宏命令将执行多个具体命令,从而实现对命令的批处理。
行为型模式
观察者模式(Observer Pattern / Publish/Subscribe Pattern)
建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。发生改变的对象称为观察目标,被通知的对象称为观察者。一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是观察者模式的模式动机。
定义:观察者模式(Observer Pattern)定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。
观察者模式包含如下角色:
Subject:目标ConcreteSubject:具体目标Observer:观察者ConcreteObserver:具体观察者
分析:
- 观察者模式描述了如何建立对象与对象之间的依赖关系,如何构造满足这种需求的系统。
- 这一模式中的关键对象是观察目标和观察者,一个目标可以有任意数目的与之相依赖的观察者,一旦目标的状态发生改变,所有的观察者都将得到通知。
- 作为对这个通知的响应,每个观察者都将即时更新自己的状态,以与目标状态同步,这种交互也称为发布-订阅(publish-subscribe)。目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。
典型的抽象目标类代码如下所示:
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();
}
}
}
典型的抽象观察者代码如下所示:
public interface Observer
{
public void update();
}
典型的具体观察者代码如下所示:
public class ConcreteObserver implements Observer
{
public void update()
{
//具体更新代码
}
}
客户端代码片段如下所示:
Subject subject= new ConcreteSubject();
Observer observer= new ConcreteObserver();
subject.attach(observer);
subject.notify();
观察者模式顺序图如下所示:
实例:
假设猫是老鼠和狗的观察目标,老鼠和狗是观察者,猫叫老鼠跑,狗也跟着叫,使用观察者模式描述该过程。
Java事件处理模型中应用了观察者模式,下面通过一个实例来学习如何自定义Java控件,并给该控件增加相应的事件。该实例基于Java Swing/AWT控件,在Swing/AWT的相关类中封装了对事件的底层处理。
适用:
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
- 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
- 一个对象必须通知其他对象,而并不知道这些对象是谁。
- 需要在系统中创建一个触发链,A 对象的行为将影响 B 对象,B 对象的行为将影响 C 对象……,可以使用观察者模式创建一种链式触发机制。
观察者模式的优点
- 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
- 观察者模式在观察目标和观察者之间建立一个抽象的耦合。
- 观察者模式支持广播通信。
- 观察者模式符合开闭原则的要求。
观察者模式的缺点
- 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
观察者模式扩展
在 JDK 的 java.util 包中,提供了 Observable 类以及 Observer 接口,它们构成了 Java 语言对观察者模式的支持。
(2025)Java内置的
notifyObservers为什么有两种重载版本?(以下回答不是规范回答,属于是自己体会)
先说那个带参数的
notifyObservers(Object arg):
这个参数Object arg 其实就是 Observer接口中的update(Observable o, Object arg)方法中的第二个参数,其实就是一个数据对象,也就是通知观察者,改变的数据对象是什么,这就是一种PUSH的方法,由主题主动的PUSH需要改变的数据对象给观察者。而第二种不带参数的
notifyObservers(),当调用它的时候, 传递一个null的数据对象给观察者.其实也就是说,观察者需要改变什么数据,是需要观察者自己到主题那里去pull.也就是说,通知你主题发生了变化,但是具体需要什么变化的数据,由你自己决定。观察者的update方法里面多写一行从observable中获取这个arg参数的代码。
MVC 模式是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。观察者模式可以用来实现 MVC 模式,观察者模式中的观察目标就是 MVC 模式中的模型(Model),而观察者就是 MVC 中的视图(View),控制器(Controller)充当两者之间的中介者(Mediator)。当模型层的数据发生改变时,视图层将自动改变其显示内容。
响应式编程:是一种以数据流和变化传播为核心的编程范式,它通过声明式的方式描述数据之间的依赖关系,并自动响应数据变化,优势如下:
- 数据流的统一抽象;
- 丰富的操作符与函数式风格;
- 异步与并发支持;
- 统一的错误处理
中介者模式(Mediator Pattern)
对于一个模块,可能由很多对象构成,而且这些对象之间可能存在相互的引用,为了减少对象两两之间复杂的引用关系,使之成为一个松耦合的系统,我们需要使用中介者模式,这就是中介者模式的模式动机。
定义:中介者模式(Mediator Pattern)用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。
中介者模式包含如下角色:
Mediator:抽象中介者ConcreteMediator:具体中介者Colleague:抽象同事类ConcreteColleague:具体同事类
分析:
- 中介者模式可以使对象之间的关系数量急剧减少:
中介者承担两方面的职责:
中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,通过中介者即可。该中转作用属于中介者在结构上的支持。
协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持。
典型的抽象中介者类代码:
public abstract class Mediator {
protected ArrayList colleagues;
public void register(Colleague colleague) {
colleagues.add(colleague);
}
public abstract void operation();
}
典型的具体中介者类代码:
public class ConcreteMediator extends Mediator {
public void operation() {
......
((Colleague) (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);
}
public void method1() {
......
}
public void method2() {
mediator.operation1();
}
}
实例:
某论坛系统欲增加一个虚拟聊天室,允许论坛会员通过该聊天室进行信息交流,普通会员(CommonMember)可以给其他会员发送文本信息,钻石会员(DiamondMember)既可以给其他会员发送文本信息,还可以发送图片信息。该聊天室可以对不雅字符进行过滤,如“日”等字符;还可以对发送的图片大小进行控制。用中介者模式设计该虚拟聊天室。
适用:
- 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解。
- 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的中介者类。
中介者模式的优点
- 简化了对象之间的交互。
- 将各同事解耦。
- 减少子类生成。
- 可以简化各同事类的设计和实现。
中介者模式的缺点
在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。
中介者模式扩展
- 在中介者模式中,通过创造出一个中介者对象,将系统中有关的对象所引用的其他对象数目减少到最少,使得一个对象与其同事之间的相互作用被这个对象与中介者对象之间的相互作用所取代。因此,中介者模式就是迪米特法则的一个典型应用。
迪米特法则:一个软件实体应当尽可能少的与其他实体发生相互作用
- 中介者模式可以方便地应用于图形界面(GUI)开发中,在比较复杂的界面中可能存在多个界面组件之间的交互关系。对于这些复杂的交互关系,有时候我们可以引入一个中介者类,将这些交互的组件作为具体的同事类,将它们之间的引用和控制关系交由中介者负责,在一定程度上简化系统的交互,这也是中介者模式的常见应用之一。
实例:
界面包含 4 个组件:
用户名输入框、密码输入框、登录按钮、状态提示标签。业务要求:
- 用户名/密码实时校验格式;
- 仅当两者均合法时,“登录”按钮才可点击;
- 点击登录或输入变化时,更新状态标签;
- 组件之间状态强关联。
- 无中介者时:输入框需直接持有按钮和标签的引用,调用
btn.setEnabled()、label.setText();按钮也需直接读取输入框值。形成 网状耦合(O(n²)引用),新增校验规则或控件需修改多处。- 引入中介者后:所有组件在状态变化时仅调用
mediator.notify(this, event)。LoginDialogMediator集中处理逻辑:收到输入事件 → 调用校验逻辑 → 若通过:loginButton.setEnabled(true) 否则:loginButton.setEnabled(false) + statusLabel.setText("格式错误")组件之间 零直接引用,依赖全部收敛到中介者。
模板方法模式(Template Method Pattern)
在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。
其实就是子类
extends或implements那些没有具体实现的抽象方法
定义:模板方法模式(Template Method Pattern)定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法是一种类行为型模式。
classDiagram
class AbstractClass {
<<abstract>>
+ templateMethod()
+ primitiveOperation1()
+ primitiveOperation2()
+ primitiveOperation3()
}
class ConcreteClass {
+ primitiveOperation1()
+ primitiveOperation2()
}
AbstractClass <|-- ConcreteClass
模板方法模式包含如下角色:
AbstractClass:抽象类ConcreteClass:具体子类
分析:
- 模板方法模式是一种类的行为型模式,在它的结构图中只有类之间的继承关系,没有对象关联关系。
- 在模板方法模式的使用过程中,要求开发抽象类和开发具体子类的设计师之间进行协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。实现这些具体逻辑步骤的方法称为基本方法(Primitive Method),而将这些基本法方法汇总起来的方法称为模板方法(Template Method),模板方法模式的名字从此而来。
- 模板方法:一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。
- 基本方法:基本方法是实现算法各个步骤的方法,是模板方法的组成部分。
- 抽象方法(Abstract Method)
- 具体方法(Concrete Method)
- 钩子方法(Hook Method):“挂钩”方法和空方法
- 钩子方法(Hook Method)
public void template() {
open();
display();
if (isPrint()) {
print();
}
}
public boolean isPrint() {
return true;
}
典型的抽象类代码如下所示:
public abstract class AbstractClass {
public void templateMethod() {
primitiveOperation1();
primitiveOperation2();
primitiveOperation3();
}
public void primitiveOperation1() {
// implement here
}
public abstract void primitiveOperation2();
public void primitiveOperation3() {
}
}
典型的具体子类代码如下所示:
public class ConcreteClass extends AbstractClass {
public void primitiveOperation2() {
//实现代码
}
public void primitiveOperation3() {
//实现代码
}
}
在模板方法模式中,由于面向对象的多态性,子类对象在运行时将覆盖父类对象,子类中定义的方法也将覆盖父类中定义的方法,因此程序在运行时,具体子类的基本方法将覆盖父类中定义的基本方法,子类的钩子方法也将覆盖父类的钩子方法,从而可以通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制。
实例:
在银行办理业务时,一般都包含几个基本步骤,首先需要取号排队,然后办理具体业务,最后需要对银行工作人员进行评分。无论具体业务是取款、存款还是转账,其基本流程都一样。现使用模板方法模式模拟银行业务办理流程。
对数据库的操作一般包括连接、打开、使用、关闭等步骤,在数据库操作模板类中我们定义了 connDB()、openDB()、useDB()、closeDB()四个方法分别对应这四个步骤。对于不同类型的数据库(如 SQL Server 和 Oracle),其操作步骤都一致,只是连接数据库 connDB() 方法有所区别,现使用模板方法模式对其进行设计。
适用:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
- 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。
- 控制子类的扩展。
模板方法模式的优点
- 模板方法模式在一个类中抽象地定义算法,而由它的子类实现细节的处理。
- 模板方法模式是一种代码复用的基本技术。
- 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”。
模板方法模式的缺点
每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,但是更加符合“单一职责原则”,使得类的内聚性得以提高。
模板方法模式扩展
- 关于继承的讨论:模板方法模式鼓励我们恰当使用继承,此模式可以用来改写一些拥有相同功能的相关类,将可复用的一般性的行为代码移到父类里面,而将特殊化的行为代码移到子类里面。这也进一步说明,虽然继承复用存在一些问题,但是在某些情况下还是可以给开发人员带来方便,模板方法模式就是体现继承优势的模式之一。
- 好莱坞原则:
- 在模板方法模式中,子类不显式调用父类的方法,而是通过覆盖父类的方法来实现某些具体的业务逻辑,父类控制对子类的调用,这种机制被称为好莱坞原则(Hollywood Principle),好莱坞原则的定义为:“不要给我们打电话,我们会给你打电话(Don‘t call us, we’ll call you)”。
- 在模板方法模式中,好莱坞原则体现在:子类不需要调用父类,而通过父类来调用子类,将某些步骤的实现写在子类中,由父类来控制整个过程。
- 钩子方法的使用
- 钩子方法的引入使得子类可以控制父类的行为。
- 最简单的钩子方法就是空方法,也可以在钩子方法中定义一个默认的实现,如果子类不覆盖钩子方法,则执行父类的默认实现代码。
- 比较复杂一点的钩子方法可以对其他方法进行约束,这种钩子方法通常返回一个
boolean类型,即返回 true 或 false,用来判断是否执行某一个基本方法。
适配器和组合
适配器模式(Adapter Pattern / Wrapper)
在适配器模式中可以定义一个包装类,包装不兼容接口的对象,这个包装类指的就是适配器(Adapter),它所包装的对象就是适配者(Adaptee),即被适配的类。
适配器提供客户类需要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作。这就是适配器模式的模式动机。
定义:适配器模式(Adapter Pattern)将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
对象适配器:
类适配器:
分析:
典型的类适配器代码:
public class Adapter extends Adaptee implements Target{
public void request(){
specificRequest();
}
}
典型的对象适配器代码:
public class Adapter extends Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
adaptee.specificRequest();
}
}
实例:
现需要设计一个可以模拟各种动物行为的机器人,在机器人中定义了一系列方法,如机器人叫喊方法 cry()、机器人移动方法 move() 等。如果希望在不修改已有代码的基础上使得机器人能够像狗一样叫,像狗一样跑,使用适配器模式进行系统设计。
// 1. Target Interface (目标接口)
interface Robot {
void cry();
void move();
}
// 2. Adaptee Class (被适配的类,已有代码,不修改)
class Dog {
public void wang() {
System.out.println("Wang! Wang!");
}
public void run() {
System.out.println("Running like a dog.");
}
}
// 3. Adapter Class (适配器)
// 继承 Dog 类并实现 Robot 接口
class DogAdapter extends Dog implements Robot {
@Override
public void cry() {
// 将 cry 适配为 wang
this.wang();
}
@Override
public void move() {
// 将 move 适配为 run
this.run();
}
}
// 测试代码
public class Main {
public static void main(String[] args) {
// 使用适配器,让机器人表现出狗的行为
Robot robot = new DogAdapter();
robot.cry(); // 输出: Wang! Wang!
robot.move(); // 输出: Running like a dog.
}
}
某系统需要提供一个加密模块,将用户信息(如密码等机密信息)加密之后再存储在数据库中,系统已经定义好了数据库操作类。为了提高开发效率,现需要重用已有的加密算法,这些算法封装在一些由第三方提供的类中,有些甚至没有源代码。使用适配器模式设计该加密模块,实现在不修改现有类的基础上重用第三方加密方法。
// 1. 抽象目标类 (Target)
abstract class DataOperator {
private String password;
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
// 抽象加密方法,由子类实现
public abstract String doEncrypt(int key, String ps);
}
// 2. 第三方加密类 (Adaptee - Caesar)
class Caesar {
public String doEncrypt(int key, String ps) {
return "Caesar加密结果: " + ps;
}
}
// 3. 第三方加密类 (Adaptee - NewCipher)
class NewCipher {
public String doEncrypt(int key, String ps) {
return "NewCipher加密结果: " + ps;
}
}
// 4. 适配器类 (Adapter) - 适配 Caesar
class CipherAdapter extends DataOperator {
private Caesar cipher = new Caesar(); // 组合关系
@Override
public String doEncrypt(int key, String ps) {
// 调用第三方类的方法
return cipher.doEncrypt(key, ps);
}
}
// 5. 适配器类 (Adapter) - 适配 NewCipher
class NewCipherAdapter extends DataOperator {
private NewCipher cipher = new NewCipher(); // 组合关系
@Override
public String doEncrypt(int key, String ps) {
// 调用第三方类的方法
return cipher.doEncrypt(key, ps);
}
}
适用:
- 系统需要使用现有的类,而这些类的接口不符合系统的需要。
- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
适配器模式的优点
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
类适配器模式还具有如下优点:由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器模式还具有如下优点:一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
适配器模式的缺点
类适配器模式的缺点如下:对于 Java、C# 等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
对象适配器模式的缺点如下:与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
适配器模式扩展
默认适配器模式(Default Adapter Pattern)或缺省适配器模式:
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。因此也称为单接口适配器模式。
双向适配器:在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器。
组合模式(Composite Pattern)
在树形结构(比如电脑里的文件夹和文件)中:
- 痛点:本来“文件夹”(容器)和“文件”(叶子)是两种不同的东西。如果不使用组合模式,你在写代码操作它们时,就得不停地写
if...else来判断:“这是个文件夹吗?如果是,就递归打开;这是个文件吗?如果是,就直接读取。” 这会让代码非常复杂。 - 解决:组合模式通过设计,让文件夹和文件看起来像是同一种东西(实现相同的接口)。
- 结果:客户端代码在操作时,不需要区分它拿到的是文件夹还是文件,直接统一调用同一个方法即可。
定义:组合模式(Composite Pattern)组合多个对象形成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。
组合模式包含如下角色:
Component:抽象构件Leaf:叶子构件Composite:容器构件Client:客户类
分析:
- 组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。
- 同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
- 文件系统组合模式结构图:
典型的抽象构件角色代码:
public abstract class Component {
public abstract void add(Component c);
public abstract void remove(Component c);
public abstract Component getChild(int i);
public abstract void operation();
}
典型的叶子构件角色代码:
public class Leaf extends Component {
public void add(Component c) { //异常处理或错误提示
}
public void remove(Component c) { //异常处理或错误提示
}
public Component getChild(int i) { //异常处理或错误提示
}
public void operation() {
//实现代码
}
}
典型的容器构件角色代码:
public class Composite extends Component {
private ArrayList list = new ArrayList();
public void add(Component c) {
list.add(c);
}
public void remove(Component c) {
list.remove(c);
}
public Component getChild(int i) {
(Component) list.get(i);
}
public void operation() {
for (Object obj : list) {
((Component) obj).operation();
}
}
}
实例:
在水果盘(Plate)中有一些水果,如苹果(Apple)、香蕉(Banana)、梨子(Pear),当然大水果盘中还可以有小水果盘,现需要对盘中的水果进行遍历(吃),当然如果对一个水果盘执行”吃”方法,实际上就是吃其中的水果。使用组合模式模拟该场景。
文件有不同类型,不同类型的文件其浏览方式有所区别,如文本文件和图片文件的浏览方式就不相同。对文件夹的浏览实际上就是对其中所包含文件的浏览,而客户端可以一致地对文件和文件夹进行操作,无须关心它们的区别。使用组合模式来模拟文件的浏览操作。
适用:
- 需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
- 让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
- 对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们。
组合模式的优点
- 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
- 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
- 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
- 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
组合模式的缺点
- 使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
- 增加新构件时可能会产生一些问题,很难对容器中的构件类型进行限制。
组合模式扩展
更复杂的组合模式
组合模式根据抽象构件类的定义形式,又可以分为透明组合模式和安全组合模式。
透明组合模式
安全组合模式(违反了里氏代换原则)
桥接与装饰者
桥接模式(Bridge Pattern / Interface / Handle and Body)
设想如果要绘制矩形、圆形、椭圆、正方形,我们至少需要 4 个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:
| 方案 1 | 方案 2 |
|---|---|
![]() |
![]() |
对于有两个变化维度(即两个变化的原因)的系统,采用方案二来进行设计系统中类的个数更少,且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
定义:桥接模式(Bridge Pattern)将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
桥接模式包含如下角色:
Abstraction:抽象类RefinedAbstraction:扩充抽象类Implementor:实现类接口ConcreteImplementor:具体实现类
分析:
理解桥接模式,重点需要理解如何将**抽象化(Abstraction)与实现化(Implementation)**脱耦,使得二者可以独立地变化。
- 抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程。
- 实现化:针对抽象化给出的具体实现,就是实现化,抽象化与实现化是一对互逆的概念,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。
- 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。
典型的实现类接口代码:
public interface Implementor {
public void operationImpl();
}
典型的抽象类代码:
public abstract class Abstraction {
protected Implementor impl;
public void setImpl(Implementor impl) {
this.impl = impl;
}
public abstract void operation();
}
典型的扩充抽象类代码:
public class RefinedAbstraction extends Abstraction {
public void operation() {
//代码
impl.operationImpl();
//代码
}
}
实例:
现需要提供大中小 3 种型号的画笔,能够绘制 5 种不同颜色,如果使用蜡笔,我们需要准备 $3 \times 5=15$ 支蜡笔,也就是说必须准备 15 个具体的蜡笔类。而如果使用毛笔的话,只需要 3 种型号的毛笔,外加 5 个颜料盒,用 $3 + 5 = 8$ 个类就可以实现 15 支蜡笔的功能。本实例使用桥接模式来模拟毛笔的使用过程。
如果需要开发一个跨平台视频播放器,可以在不同操作系统平台(如 Windows、Linux、Unix 等)上播放多种格式的视频文件,常见的视频格式包括 MPEG、RMVB、AVI、WMV 等。现使用桥接模式设计该播放器。
适用:
- 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
- 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
桥接模式的优点
- 分离抽象接口及其实现部分。
- 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
桥接模式的缺点
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
桥接模式扩展
桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况。
装饰模式(Decorator Pattern / Wrapper)
定义:装饰模式(Decorator Pattern)动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。
装饰模式包含如下角色:
Component:抽象构件ConcreteComponent:具体构件Decorator:抽象装饰类ConcreteDecorator:具体装饰类
分析:
- 与继承关系相比,关联关系的主要优势在于不会破坏类的封装性,而且继承是一种耦合度较大的静态关系,无法在程序运行时动态扩展。在软件开发阶段,关联关系虽然不会比继承关系减少编码量,但是到了软件维护阶段,由于关联关系使系统具有较好的松耦合性,因此使得系统更加容易维护。当然,关联关系的缺点是比继承关系要创建更多的对象。
- 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
典型的抽象装饰类代码:
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() {
// 新增方法
}
}
变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔。
- Q:为什么
Changer在继承自Transform的同时还要有一个Transform?- A:因为我们希望
Changer能够装饰Car或其他的Transform(如果有,例如Bicycle),因此需要组合一个transform。
某系统提供了一个数据加密功能,可以对字符串进行加密。最简单的加密算法通过对字母进行移位来实现,同时还提供了稍复杂的逆向输出加密,还提供了更为高级的求模加密。用户先使用最简单的加密算法对字符串进行加密,如果觉得还不够可以对加密之后的结果使用其他加密算法进行二次加密,当然也可以进行第三次加密。现使用装饰模式设计该多重加密系统。
适用:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如 final 类)。
装饰模式的优点
- 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
- 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合”开闭原则”。
装饰模式的缺点
- 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
装饰模式扩展
- 装饰模式的简化-需要注意的问题:
- 一个装饰类的接口必须与被装饰类的接口保持相同,对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待。
- 尽量保持具体构件类 Component 作为一个”轻”类,也就是说不要把太多的逻辑和状态放在具体构件类中,可以通过装饰类对其进行扩展。
- 如果只有一个具体构件类而没有抽象构件类,那么抽象装饰类可以作为具体构件类的直接子类。
- 装饰模式的简化
你简化在哪了?
透明装饰模式(多重加密系统)
在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型。
Cipher sc, cc, ac;
sc = new SimpleCipher();
cc = new ComplexCipher(sc);
ac = new AdvancedCipher(cc);
半透明装饰模式(变形金刚)
半透明(semi-transparent)的装饰模式允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。
Transform camaro;
camaro = new Car();
camaro.move();
Robot bumblebee = new Robot(camaro);
bumblebee.move();
bumblebee.say();
外观模式、享元模式和代理模式
外观模式(Facade Pattern)
引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。
定义:外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。
外观模式使用了迪米特法则的思想
外观模式包含如下角色:
Facade:外观角色SubSystem:子系统角色
分析:
- 根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。
- 外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。
- 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。
- 外观模式的目的在于降低系统的复杂程度。
- 外观模式从很大程度上提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观角色即可调用相关功能。
- 典型的外观角色代码:
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();
}
}
实例:
现在考察一个电源总开关的例子,以便进一步说明外观模式。为了使用方便,一个电源总开关可以控制四盏灯、一个风扇、一台空调和一台电视机的启动和关闭。通过该电源总开关可以同时控制上述所有电器设备,使用外观模式设计该系统。
某系统需要提供一个文件加密模块,加密流程包括三个操作,分别是读取源文件、加密、保存加密之后的文件。读取文件和保存文件使用流来实现,这三个操作相对独立,其业务代码封装在三个不同的类中。现在需要提供一个统一的加密外观类,用户可以直接使用该加密外观类完成文件的读取、加密和保存三个操作,而不需要与每一个类进行交互,使用外观模式设计该加密模块。
适用:
- 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
- 客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
- 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
外观模式的优点
- 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少。
- 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
- 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
- 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类。
外观模式的缺点
- 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。
- 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
外观模式的应用
外观模式应用于 JDBC 数据库操作
public class JDBCFacade {
private Connection conn = null;
private Statement statement = null;
public void open(String driver, String jdbcUrl, String userName, String userPwd) {
......
}
public int executeUpdate(String sql) {
......
}
public ResultSet executeQuery(String sql) {
......
}
public void close() {
......
}
}
Session 外观模式是外观模式在 Java EE 框架中的应用。
外观模式扩展
一个系统有多个外观类:
在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类。**在很多情况下为了节约系统资源,一般将外观类设计为单例类。**当然这并不意味着在整个系统里只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。
不要试图通过外观类为子系统增加新行为:
不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。
外观模式与迪米特法则:
外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代。外观类充当了客户类与子系统类之间的“第三者”,降低了客户类与子系统类之间的耦合度,外观模式就是实现代码重构以便达到“迪米特法则”要求的一个强有力的武器。
抽象外观类的引入:
外观模式最大的缺点在于违背了“开闭原则”,**当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。**对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。
享元模式(Flyweight Pattern)
面向对象技术可以很好地解决一些灵活性或可扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时,将导致运行代价过高,带来性能下降等问题。享元模式正是为解决这一类问题而诞生的。享元模式通过共享技术实现相同或相似对象的重用。
享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。
定义:享元模式(Flyweight Pattern)运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
享元模式包含如下角色:
Flyweight:抽象享元类ConcreteFlyweight:具体享元类UnsharedConcreteFlyweight:非共享具体享元类FlyweightFactory:享元工厂类
分析:
- 享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。
- 享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
- 典型的享元工厂类代码:
public class FlyweightFactory {
private HashMap flyweights = new HashMap();
public Flyweight getFlyweight(String key) {
if (flyweights.containsKey(key)) {
return (Flyweight) flyweights.get(key);
} else {
Flyweight fw = new ConcreteFlyweight();
flyweights.put(key, fw);
return fw;
}
}
}
享元模式以共享的方式高效地支持大量的细粒度对象,享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。
内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。
典型的享元类代码:
public class Flyweight {
// 内部状态作为成员属性
private String intrinsicState;
public Flyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
public void operation(String extrinsicState) {
// ......
}
}
实例:
很多网络设备都是支持共享的,如交换机、集线器等,多台终端计算机可以连接同一台网络设备,并通过该网络设备进行数据转发,如图所示,现用享元模式模拟共享网络设备的设计原理。
虽然网络设备可以共享,但是分配给每一个终端计算机的端口(Port)是不同的,因此多台计算机虽然可以共享同一个网络设备,但必须使用不同的端口。我们可以将端口从网络设备中抽取出来作为外部状态,需要时再进行设置。
适用:
- 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式。
享元模式的优点
- 享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
享元模式的缺点
- 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
享元模式的应用
- 享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片对象,通过在应用程序中设置该图片出现的位置,可以实现该图片在不同地方多次重复显示。
- 在 JDK 类库中定义的 String 类使用了享元模式。
public class Demo {
public static void main(String args[]) {
String str1 = "abcd";
String str2 = "abcd";
String str3 = "ab" + "cd";
String str4 = "ab";
str4 += "cd";
System.out.println(str1 == str2);// T
System.out.println(str1 == str3);// T
System.out.println(str1 == str4);// F
}
}
str4是一个变量,不是编译期常量。+=操作在运行时执行,会在堆上创建一个新的String对象(内容为"abcd"),而不是复用常量池中的对象。所以
str1(常量池)和str4(堆中新对象)指向不同的内存地址。
享元模式的扩展
单纯享元模式:在单纯享元模式中,所有的享元对象都是可以共享的,即所有抽象享元类的子类都可共享,不存在非共享具体享元类。
复合享元模式:将一些单纯享元使用组合模式加以组合,可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。
意思是:复合具体享元实现了抽象享元接口,但它内部不存储单一的内部状态,而是持有一个集合(如 Map 或 List),里面存放了多个单纯享元对象。
享元模式与其他模式的联用:
- 在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
- 在一个系统中,通常只有唯一一个享元工厂,因此享元工厂类可以使用单例模式进行设计。
- 享元模式可以结合组合模式形成复合享元模式,统一对享元对象设置外部状态。
代理模式(Proxy Pattern / Surrogate)
在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。
通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
定义:代理模式(Proxy Pattern)给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做 Proxy 或 Surrogate,它是一种对象结构型模式。
熊佬说上图违反了依赖倒置原则,这里
Client应该依赖Subject而不是Proxy,我个人的想法是可能Client是不知道Subject的存在的,毕竟代理模式设计的初衷就有“一个客户不想或者不能直接引用一个对象”,此处的客户端可能只知道Proxy的存在,但是具体情况要具体分析。
Proxy 是符合迪米特法则的
代理模式包含如下角色:
Subject:抽象主题角色Proxy:代理主题角色RealSubject:真实主题角色
分析:
- 代理模式示意结构图比较简单,一般可以简化为如下图所示,但是在现实中要复杂很多。
- 典型的代理类实现代码:
public class Proxy implements Subject {
private RealSubject realSubject = new RealSubject();
public void preRequest() {
// ......
}
public void request() {
preRequest();
realSubject.request();
postRequest();
}
public void postRequest() {
// ......
}
}
实例:
在一个论坛中已注册用户和游客的权限不同,已注册的用户拥有发帖、修改自己的注册信息、修改自己的帖子等功能;而游客只能看到别人发的帖子,没有其他权限。使用代理模式来设计该权限管理模块。
在本实例中我们使用代理模式中的保护代理,该代理用于控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
模拟应用远程代理来访问另外一个应用程序域中的对象,如果在远程实现了加减乘除等运算,在本地需要调用,那么可以考虑在本地设置一个代理。
适用:
根据代理模式的使用目的,常见的代理模式有以下几种类型:
远程(Remote)代理:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)。
虚拟(Virtual)代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
Copy-on-Write 代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write 代理可以让这个操作延迟,只有对象被用到的时候才被克隆。
保护(Protect or Access)代理:控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
缓冲(Cache)代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
防火墙(Firewall)代理:保护目标不让恶意用户接近。
智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等。
代理模式的优点
- 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
- 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
- 保护代理可以控制对真实对象的使用权限。
- C++:智能指针代理
代理模式的缺点
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
代理模式的应用
Java RMI(Remote Method Invocation,远程方法调用)
EJB、Web Service 等分布式技术都是代理模式的应用。在 EJB 中使用了 RMI 机制,远程服务器中的企业级 Bean 在本地有一个桩代理,客户端通过桩来调用远程对象中定义的方法,而无须直接与远程对象交互。在 EJB 的使用中需要提供一个公共的接口,客户端针对该接口进行编程,无须知道桩以及远程 EJB 的实现细节。
代理模式的扩展
远程代理:远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户完全可以认为被代理的远程业务对象是局域的而不是远程的,而远程代理对象承担了大部分的网络通信工作。
虚拟代理:当一个对象的加载十分耗费资源的时候,虚拟代理的优势就非常明显地体现出来了。虚拟代理模式是一种内存节省技术,那些占用大量内存或处理复杂的对象将推迟到使用它的时候才创建。在应用程序启动的时候,可以用代理对象代替真实对象初始化,节省了内存的占用,并大大加速了系统的启动时间。(缩略图)
附录
此附录中如果有内容,一般都是无法理解一些模式而找AI询问的内容
⚠️ AI-Generated Content ⚠️
复合享元模式
典型的复合享元模式应用场景:
文本编辑器 / 富文本系统(最经典的例子):
- 复合对象(不可共享):一篇文档、一个段落、一个文本框。每篇文档的内容和格式都不同,必须独立存在。
- 单纯享元(可共享):文档中的每一个字符、字体样式、颜色配置。
- 作用:一篇 10 万字的文档,如果每个字都
new一个字符对象,内存会撑爆。使用复合享元,文档对象内部只保存字符的编码和位置,具体的字形渲染对象(Glyph)全部从字库享元池中获取。
相关要素:
- 内部状态(可共享):字符本身的形状(如 ‘A’ 的字形)。
- 外部状态(不可共享,动态传入):字符在屏幕上的渲染坐标
(x, y)。 - 单纯享元:
CharacterGlyph(具体的单个字符)。 - 复合享元:
CompositeGlyph(段落、文档等容器,包含多个子 Glyph)。 - 享元工厂:
GlyphFactory(管理所有字符字形的享元池)。
代码:
// ==========================================
// 1. 抽象享元角色 (Flyweight)
// ==========================================
interface Glyph {
// 渲染方法。x, y 是外部状态,由客户端在调用时传入
void draw(int x, int y);
}
// ==========================================
// 2. 单纯具体享元 (ConcreteFlyweight)
// ==========================================
class CharacterGlyph implements Glyph {
// 内部状态:字符本身(如 'A', 'b')。这部分是共享的。
private final char charValue;
public CharacterGlyph(char charValue) {
this.charValue = charValue;
}
@Override
public void draw(int x, int y) {
// 实际应用中,这里会调用底层图形 API 绘制具体字形
System.out.println(" -> 在坐标(" + x + "," + y + ") 绘制字符: [" + charValue + "]");
}
}
// ==========================================
// 3. 复合具体享元 (CompositeFlyweight)
// ==========================================
class CompositeGlyph implements Glyph {
private final List<Glyph> children = new ArrayList<>();
private final String name; // 仅用于打印日志,区分是段落还是文档
public CompositeGlyph(String name) {
this.name = name;
}
// 组合模式的核心:添加子节点
public void add(Glyph glyph) {
children.add(glyph);
}
@Override
public void draw(int x, int y) {
System.out.println("--- 开始渲染复合对象: " + name + " (起点:" + x + "," + y + ") ---");
int currentX = x;
// 遍历子节点,传递外部状态(坐标),并计算下一个字符的偏移量
for (Glyph child : children) {
child.draw(currentX, y);
currentX += 10; // 假设每个字符宽度为 10 像素
}
}
}
// ==========================================
// 4. 享元工厂 (FlyweightFactory)
// ==========================================
class GlyphFactory {
// 享元池:以字符为 key,缓存 CharacterGlyph 对象
private final Map<Character, CharacterGlyph> pool = new HashMap<>();
// 获取单纯享元
public CharacterGlyph getCharacterGlyph(char c) {
// 如果池中没有,则创建并放入池中;如果有,则直接复用
return pool.computeIfAbsent(c, CharacterGlyph::new);
}
// 辅助方法:查看享元池大小,验证共享效果
public int getPoolSize() {
return pool.size();
}
}
// ==========================================
// 5. 客户端 (Client)
// ==========================================
public class TextEditorDemo {
public static void main(String[] args) {
GlyphFactory factory = new GlyphFactory();
// 【构建文档树】(复合对象,不可共享,每次都是 new 出来的)
CompositeGlyph document = new CompositeGlyph("Document");
CompositeGlyph paragraph1 = new CompositeGlyph("Paragraph 1");
CompositeGlyph paragraph2 = new CompositeGlyph("Paragraph 2");
document.add(paragraph1);
document.add(paragraph2);
// 【模拟用户输入文本】
String text1 = "Hello";
for (char c : text1.toCharArray()) {
// 关键:不直接 new CharacterGlyph,而是从工厂获取共享对象
paragraph1.add(factory.getCharacterGlyph(c));
}
String text2 = "World";
for (char c : text2.toCharArray()) {
paragraph2.add(factory.getCharacterGlyph(c));
}
// 【渲染文档】(传入外部状态:起始坐标)
System.out.println("========== 开始渲染文档 ==========");
document.draw(0, 0); // 从屏幕左上角 (0,0) 开始渲染
// 【验证内存优化效果】
System.out.println("\n========== 内存优化统计 ==========");
System.out.println("文档总字符数: " + (text1.length() + text2.length())); // 10个字符
System.out.println("享元池中实际创建的对象数: " + factory.getPoolSize()); // 只有 7 个独立对象 (H,e,l,o,W,r,d)
}
}
代理模式和适配器模式的区别
- 代理模式是为了控制访问(接口相同,增强或限制功能)。
- 适配器模式是为了接口兼容(接口不同,起到翻译/转换作用)。
代理类和真实类必须实现同一个接口。对客户端来说,它们长得一模一样。
适配器实现的是客户端期望的目标接口,但它内部包装的是一个接口完全不同的旧类/外部类。

