软件系统设计-设计(3) 结构型模式
2023-08-19 00:13:37 # NJU # 软件系统设计

1. (类)适配器模式与(对象)适配器模式

1.1 模式动机

image-20230612095510933

  1. 在软件开发中采用类似于电源适配器的设计和编码技巧被称为适配器模式
  2. 通常情况下,客户端可以通过目标类的接口访问它所提供的服务。有时,现有的类可以满足客户类的功能需要,但是它所提供的接口不一定是客户类所期望的,这可能是因为现有类中方法名与目标类中定义的方法名不一致等原因所导致的。
  3. 在这种情况下,现有的接口需要转化为客户类期望的接口,这样保证了对现有类的重用。如果不进行这样的转化,客户类就不能利用现有类所提供的功能,适配器模式可以完成这样的转化。
  4. 在适配器模式中可以定义一个包装类,包装不兼容接口的对象,这个包装类指的就是适配器(Adapter),它所包装的对象就是适配者(Adaptee),即被适配的类。
  5. 适配器提供客户类需要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作。这就是适配器模式的模式动机。

1.2 模式定义

  1. 适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
  2. Adapter Pattern: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

1.3 模式结构

对象适配器

image-20230612095709256

类适配器

image-20230612095817460

适配器模式包含如下角色:

  1. Target:目标抽象类
  2. Adapter:适配器类
  3. Adaptee:适配者类
  4. Client:客户类

1.4 模式分析

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

1.5 适配器模式实例与解析

实例一:仿生机器人

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

image-20230612100106413

实例二:加密适配器

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

image-20230612100259747

1.6 模式优缺点

适配器模式的优点

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

类适配器模式的优点

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

类适配器模式的缺点

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

对象适配器模式的优点

  1. 一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口

对象适配器模式的缺点

  1. 与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

1.7 模式适用环境

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

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

1.8 模式应用

  1. Sun公司在1996年公开了Java语言的数据库连接工具JDBC,JDBC使得Java语言程序能够与数据库连接,并使用SQL语言来查询和操作数据。JDBC给出一个客户端通用的抽象接口,每一个具体数据库引擎(如SQL Server、Oracle、MySQL等)的JDBC驱动软件都是一个介于JDBC接口和数据库引擎接口之间的适配器软件。抽象的JDBC接口和各个数据库引擎API之间都需要相应的适配器软件,这就是为各个不同数据库引擎准备的驱动程序

1.9 模式应用

JDK类库中的适配器类

如在com.sun.imageio.plugins.common包中定义的InputStreamAdapter类,用于包装ImageInputStream接口及其子类对象。

public class InputStreamAdapter extends InputStream{
ImageInputStream stream;
public InputStreamAdapter(ImageInputStream stream) {
super();
this.stream = stream;
}
public int read() throws IOException {
return stream.read();
}
public int read(byte b[], int off, int len) throws IOException {
return stream.read(b, off, len);
}
}

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

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

image-20230612100912991
image-20230612100934239

双向适配器

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

image-20230612101107369

1.10 适配器小结

  1. 结构型模式描述如何将类或者对象结合在一起形成更大的结构。
  2. 适配器模式用于将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
  3. 适配器模式包含四个角色
    1. 目标抽象类定义客户要用的特定领域的接口
    2. 适配器类可以调用另一个接口,作为一个转换器,对适配者和抽象目标类进行适配,它是适配器模式的核心
    3. 适配者类是被适配的角色,它定义了一个已经存在的接口,这个接口需要适配
    4. 在客户类中针对目标抽象类进行编程,调用在目标抽象类中定义的业务方法。
  4. 在类适配器模式中,适配器类实现了目标抽象类接口并继承了适配者类,并在目标抽象类的实现方法中调用所继承的适配者类的方法;在对象适配器模式中,适配器类继承了目标抽象类并定义了一个适配者类的对象实例,在所继承的目标抽象类方法中调用适配者类的相应业务方法。
  5. 适配器模式
    1. 主要优点是将目标类和适配者类解耦,增加了类的透明性和复用性,同时系统的灵活性和扩展性都非常好,更换适配器或者增加新的适配器都非常方便,符合”开闭原则”
    2. 类适配器模式的缺点是适配器类在很多编程语言中不能同时适配多个适配者类
    3. 对象适配器模式的缺点是很难置换适配者类的方法。
  6. 适配器模式适用情况包括:系统需要使用现有的类,而这些类的接口不符合系统的需要;想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作。

2. 组合模式(对象)

2.1 模式动机

image-20230612102923937

  1. 对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象,如子文件夹和文件)并调用执行。(递归调用)
  2. 由于容器对象和叶子对象在功能上的区别,在使用这些对象的客户端代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下客户端希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂
  3. 组合模式描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器对象和叶子对象,这就是组合模式的模式动机。

2.2 模式定义

  1. 组合模式(Composite Pattern):组合多个对象形成树形结构表示”整体-部分”的结构层次。组合模式对单个对象(即叶子对象)组合对象(即容器对象)使用具有一致性
  2. 组合模式又可以称为整体-部分(Part-Whole)模式,属于对象的结构模式,它将对象组织到树结构中,可以用来描述整体与部分的关系
  3. Composite Pattern: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

2.3 模式结构

image-20230612103906865

组合模式包含如下角色:

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

2.4 模式分析

  1. 组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。
  2. 同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构
  3. 文件系统组合模式结构图:

image-20230612104101339

  1. 典型的抽象构件角色代码:
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();
}
  1. 典型的叶子构件角色代码:
public class Leaf extends Component{
public void add(Component c)
{ //异常处理或错误提示
}
public void remove(Component c)
{ //异常处理或错误提示
}
public Component getChild(int i)
{ //异常处理或错误提示
}
public void operation()
{ //实现代码
}
}
  1. 典型的容器构件角色代码:
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) {
return (Component)list.get(i);
}
public void operation() {
for(Object obj:list) {
((Component)obj).operation();
}
}
}

2.5 组合模式实例与解析

实例一:水果盘

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

image-20230612104406439

实例二:文件浏览

文件有不同类型,不同类型的文件其浏览方式有所区别,如文本文件和图片文件的浏览方式就不相同。对文件夹的浏览实际上就是对其中所包含文件的浏览,而客户端可以一致地对文件和文件夹进行操作,无须关心它们的区别。使用组合模式来模拟文件的浏览操作。

image-20230612104525305

2.6 模式优缺点

组合模式的优点

  1. 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
  2. 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象
  3. 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构
  4. 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。组合模式

组合模式的缺点

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

2.7 模式适用环境

在以下情况下可以使用组合模式:

  1. 需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
  2. 让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节
  3. 对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们

2.8 模式应用

  1. XML文档解析
<?xml version="1.0"?>
<books>
<book>
<author>Carson</author>
<price format="dollar">31.95</price>
<pubdate>05/01/2001</pubdate>
</book>
<pubinfo>
<publisher>MSPress</publisher>
<state>WA</state>
</pubinfo>
</books>

image-20230612104708064

  1. 操作系统中的目录结构是一个树形结构,因此在对文件和文件夹进行操作时可以应用组合模式,例如杀毒软件在查毒或杀毒时,既可以针对一个具体文件,也可以针对一个目录。如果是对目录查毒或杀毒,将递归处理目录中的每一个子目录和文件。
  2. JDK的AWT/Swing是组合模式在Java类库中的一个典型实际应用。

image-20230612104736113

2.9 模式扩展

更复杂的组合模式

image-20230612104808582

透明组合模式

image-20230612104847480

安全组合模式

  • 违反了里氏代换原则

image-20230612104908637

2.10 组合小结

  1. 组合模式用于组合多个对象形成树形结构以表示”整体-部分”的结构层次。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。组合模式又可以称为”整体-部分”模式,属于对象的结构模式,它将对象组织到树结构中,可以用来描述整体与部分的关系。
  2. 组合模式包含三个角色
    1. 抽象构件为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现
    2. 叶子构件在组合结构中表示叶子节点对象,叶子节点没有子节点
    3. 容器构件在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为。
  3. 组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。
  4. 组合模式
    1. 主要优点在于可以方便地对层次结构进行控制,客户端调用简单,客户端可以一致的使用组合结构或其中单个对象,用户就不必关心自己处理的是单个对象还是整个组合结构,简化了客户端代码
    2. 缺点在于使设计变得更加抽象,且增加新构件时可能会产生一些问题,而且很难对容器中的构件类型进行限制。
  5. 组合模式适用情况包括
    1. 需要表示一个对象整体或部分层次
    2. 让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节
    3. 对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们。
  6. 组合模式根据抽象构件类的定义形式,又可以分为透明组合模式和安全组合模式。

3. 装饰模式(对象)

3.1 模式动机

image-20230612105355434

  1. 一般有两种方式可以实现给一个类或对象增加行为:
    1. 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。
    2. 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为。
  2. 装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机。

3.2 模式定义

  1. 装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为”油漆工模式”,它是一种对象结构型模式
  2. Decorator Pattern: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

3.3 模式结构

image-20230612105517641

装饰模式包含如下角色:

  1. Component: 抽象构件
  2. ConcreteComponent: 具体构件
  3. Decorator: 抽象装饰类
  4. ConcreteDecorator: 具体装饰类

3.4 模式分析

  1. 与继承关系相比,关联关系的主要优势在于不会破坏类的封装性,而且继承是一种耦合度较大的静态关系,无法在程序运行时动态扩展。在软件开发阶段,关联关系虽然不会比继承关系减少编码量,但是到了软件维护阶段,由于关联关系使系统具有较好的松耦合性,因此使得系统更加容易维护。当然,关联关系的缺点是比继承关系要创建更多的对象
  2. 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
  3. 典型的抽象装饰类代码:
public class Decorator extends Component {
private Component component;
public Decorator(Component component) {
this.component=component;
}
public void operation() {
component.operation();
}
}
  1. 典型的具体装饰类代码:
public class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component) {
super(component);
}
public void operation() {
super.operation();
addedBehavior();
}
public void addedBehavior() {
//新增方法
}
}

3.5 装饰模式实例与解析

实例一:变形金刚

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

image-20230612110030272

实例二:多重加密系统

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

image-20230612110111263

3.6 模式优缺点

装饰模式的优点

  1. 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性
  2. 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
  3. 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
  4. 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合”开闭原则”。

装饰模式的缺点

  1. 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
  2. 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐

3.7 模式适用环境

在以下情况下可以使用装饰模式:

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  2. 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销
  3. 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)。

3.8 模式应用

在javax.swing包中,可以通过装饰模式动态给一些构件增加新的行为或改善其外观显示。

  • 如JList构件本身并不支持直接滚动,即没有滚动条,要创建可以滚动的列表,可以使用如下代码实现:
JList list = new JList();
JScrollPane sp = new JScrollPane(list);

装饰模式在JDK中最经典的实例是Java IO。

  • InputStream 为例:

image-20230612110315912

  • 抽象装饰类:FilterInputStream
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
  • 角色分配:

    • 抽象构件类:InputStream
    • 具体构件类:FileInputStream、ByteArrayInputStream等
    • 抽象装饰类:FilterInputStream
    • 具体装饰类:BufferedInputStream、DataInputStream等
  • 客户端使用:

FileInputStream inFS = new FileInputStream("temp/fileSrc.txt");
BufferedInputStream inBS = new BufferedInputStream(inFS);
//定义一个字节数组,用于存放缓冲数据
byte[] data = new byte[1024];
inBS.read(data);

3.9 模式扩展

装饰模式的简化

image-20230612110713084

需要注意的问题

  • 一个装饰类的接口必须与被装饰类的接口保持相同,对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待。
  • 尽量保持具体构件类Component作为一个”轻”类,也就是说不要把太多的逻辑和状态放在具体构件类中,可以通过装饰类对其进行扩展。
  • 如果只有一个具体构件类而没有抽象构件类,那么抽象装饰类可以作为具体构件类的直接子类

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

  1. 在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型。
Cipher sc,cc,ac;
sc = new SimpleCipher();
cc = new ComplexCipher(sc);
ac = new AdvancedCipher(cc);

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

  1. 半透明(semi-transparent)的装饰模式允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。
Transform camaro;
camaro=new Car();
camaro.move();
Robot bumblebee = new Robot(camaro);
bumblebee.move();
bumblebee.say();

3.10 小结

  1. 装饰模式用于动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。它是一种对象结构型模式。
  2. 装饰模式包含四个角色
    1. 抽象构件定义了对象的接口,可以给这些对象动态增加职责(方法)
    2. 具体构件定义了具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)
    3. 抽象装饰类是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现
    4. 具体装饰类是抽象装饰类的子类,负责向构件添加新的职责。
  3. 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
  4. 装饰模式
    1. 主要优点在于可以提供比继承更多的灵活性,可以通过一种动态的方式来扩展一个对象的功能,并通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,而且具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类
    2. 主要缺点在于使用装饰模式进行系统设计时将产生很多小对象,而且装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
  5. 装饰模式适用情况包括
    1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
    2. 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销
    3. 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
  6. 装饰模式可分为透明装饰模式和半透明装饰模式
    1. 在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型
    2. 半透明装饰模式允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。

4. 外观模式(对象)

4.1 模式动机

image-20230612111836731

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

4.2 模式定义

  1. 外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式
    1. 简化了接口
    2. 接口相关模式:适配器模式、外观模式,变化决定了要使用哪个模式
  2. Facade Pattern: Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

4.3 模式结构

image-20230612112036071

  • 外观模式包含如下角色:
    1. Facade: 外观角色
    2. SubSystem:子系统角色
  • 使用的设计思想:迪米特法则

4.4 模式分析

  1. 根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口
  2. 外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度
  3. 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道
  4. 外观模式的目的在于降低系统的复杂程度
  5. 外观模式从很大程度上提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观角色即可调用相关功能。
  6. 典型的外观角色代码:
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();
}
}

4.5 外观模式实例与解析

实例一:电源总开关

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

image-20230612112647563

实例二:文件加密

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

image-20230612112820640

可以使用模板方法模式实现嘛?子类提供不同的实现,复用父类方法的步骤

什么时候外观?如果每一个部分都不变,只是提供接口

4.6 模式优缺点

外观模式的优点

  1. 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少。
  2. 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
  3. 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
  4. 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类

外观模式的缺点

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

4.7 模式适用环境

在以下情况下可以使用外观模式:

  1. 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
  2. 客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
  3. 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度

4.8 模式应用

外观模式应用于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框架中的应用。

image-20230612113443164

4.9 模式扩展

一个系统有多个外观类

在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类在很多情况下为了节约系统资源,一般将外观类设计为单例类。当然这并不意味着在整个系统里只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。

不要试图通过外观类为子系统增加新行为

不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。

外观模式与迪米特法则

外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代。外观类充当了客户类与子系统类之间的“第三者”,降低了客户类与子系统类之间的耦合度,外观模式就是实现代码重构以便达到“迪米特法则”要求的一个强有力的武器。

抽象外观类的引入

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

image-20230612113636474

4.10 小结

  1. 在外观模式中,外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。
  2. 外观模式包含两个角色
    1. 外观角色是在客户端直接调用的角色,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。
    2. 在软件系统中可以同时有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能。
  3. 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。
  4. 外观模式
    1. 主要优点在于对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易,它实现了子系统与客户之间的松耦合关系,并降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程
    2. 其缺点在于不能很好地限制客户使用子系统类,而且在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
  5. 外观模式适用情况包括
    1. 要为一个复杂子系统提供一个简单接口
    2. 客户程序与多个子系统之间存在很大的依赖性
    3. 在层次化结构中,需要定义系统中每一层的入口,使得层与层之间不直接产生联系。

5. 代理模式(对象)

5.1 模式动机

在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务

image-20230612114233046 image-20230612114245067

通过引入一个新的对象(如小图片和远程代理对象)来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。

5.2 模式定义

  1. 代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate,它是一种对象结构型模式
  2. Proxy Pattern: Provide a surrogate or placeholder for another object to control access to it.
  3. 思想简单,但是变体很多

5.3 模式结构

image-20230612114401259

  1. 代理模式包含如下角色:
    1. Subject: 抽象主题角色
    2. Proxy: 代理主题角色
    3. RealSubject: 真实主题角色
  2. Proxy是符合迪米特法则的。

5.4 模式分析

  1. 代理模式示意结构图比较简单,一般可以简化为如下图所示,但是在现实中要复杂很多。

image-20230612114500888

  1. 典型的代理类实现代码:
public class Proxy implements Subject{
private RealSubject realSubject = new RealSubject();
public void preRequest() {......}
public void request() {
preRequest();
realSubject.request();
postRequest();
}
public void postRequest() {......}
}

5.5 代理模式实例与解析

实例一:论坛权限控制代理

  1. 在一个论坛中已注册用户和游客的权限不同,已注册的用户拥有发帖、修改自己的注册信息、修改自己的帖子等功能;而游客只能看到别人发的帖子,没有其他权限。使用代理模式来设计该权限管理模块。
  2. 在本实例中我们使用代理模式中的保护代理,该代理用于控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。

image-20230612114629895

实例二:数学运算代理

模拟应用远程代理来访问另外一个应用程序域中的对象,如果在远程实现了加减乘除等运算,在本地需要调用,那么可以考虑在本地设置一个代理。

image-20230612114715673

5.6 模式优缺点

代理模式的优点

  1. 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  2. 远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  3. 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
  4. 保护代理可以控制对真实对象的使用权限。
  5. C++:智能指针代理

代理模式的缺点

  1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢
  2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂

5.7 模式适用环境

  1. 根据代理模式的使用目的,常见的代理模式有以下几种类型:
    1. 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)。
    2. 虚拟(Virtual)代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
    3. Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。
  2. 根据代理模式的使用目的,代理模式有以下几种类型(续):
    1. 保护(Protect or Access)代理:控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
    2. 缓冲(Cache)代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
    3. 防火墙(Firewall)代理:保护目标不让恶意用户接近。
    4. 智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等。

5.8 模式应用

  1. Java RMI (Remote Method Invocation,远程方法调用)。

image-20230612114851066

  1. EJB、Web Service等分布式技术都是代理模式的应用。在EJB中使用了RMI机制,远程服务器中的企业级Bean在本地有一个桩代理,客户端通过桩来调用远程对象中定义的方法,而无须直接与远程对象交互。在EJB的使用中需要提供一个公共的接口,客户端针对该接口进行编程,无须知道桩以及远程EJB的实现细节。

5.9 模式扩展

几种常用的代理模式

远程代理

远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户完全可以认为被代理的远程业务对象是局域的而不是远程的,而远程代理对象承担了大部分的网络通信工作。

image-20230612115001201

虚拟代理

  1. 当一个对象的加载十分耗费资源的时候,虚拟代理的优势就非常明显地体现出来了。虚拟代理模式是一种内存节省技术,那些占用大量内存或处理复杂的对象将推迟到使用它的时候才创建。

  2. 在应用程序启动的时候,可以用代理对象代替真实对象初始化,节省了内存的占用,并大大加速了系统的启动时间。

  3. 一个很常见的代理模式的应用实例就是对大图浏览的控制
  4. 用户通过浏览器访问网页时先不加载真实的大图,而是通过代理对象的方法来进行处理,在代理对象的方法中,先使用一个线程向客户端浏览器加载一个小图片,然后在后台使用另一个线程来调用大图片的加载方法将大图片加载到客户端。当需要浏览大图片时,再将大图片在新网页中显示。如果用户在浏览大图时加载工作还没有完成,可以再启动一个线程来显示相应的提示信息。通过代理技术结合多线程编程将真实图片的加载放到后台来操作,不影响前台图片的浏览

5.10 小结

  1. 在代理模式中,要求给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做Proxy或Surrogate,它是一种对象结构型模式。
  2. 代理模式包含三个角色
    1. 抽象主题角色声明了真实主题和代理主题的共同接口
    2. 代理主题角色内部包含对真实主题的引用,从而可以在任何时候操作真实主题对象
    3. 真实主题角色定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的方法。
  3. 代理模式
    1. 优点在于能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
    2. 缺点在于由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,并且实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
  4. 远程代理为一个位于不同的地址空间的对象提供一个本地的代表对象,它使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  5. 如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建,这个小对象称为虚拟代理。虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
  6. 保护代理可以控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。

6. 桥接模式(对象)

6.1 模式动机

设想如果要绘制矩形、圆形、椭圆、正方形,我们至少需要4个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:

  1. 第一种设计方案是为每一种形状都提供一套各种颜色的版本。
  2. 第二种设计方案是根据实际需要对形状和颜色进行组合。

image-20230627215938461对于有两个变化维度(即两个变化的原因)的系统,采用方案二来进行设计系统中类的个数更少,且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量

image-20230627220126858

6.2 模式定义

  1. 桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
  2. Bridge Pattern: Decouple an abstraction from its implementation so that the two can vary independently.

6.3 模式结构

image-20230627220159169

桥接模式包含如下角色:

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

6.4 模式分析

理解桥接模式,重点需要理解如何将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化。

  1. 抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程
  2. 实现化针对抽象化给出的具体实现,就是实现化,抽象化与实现化是一对互逆的概念,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。
  3. 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。

典型的实现类接口代码:

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();
//代码
}
}

6.5 桥接模式实例与解析

实例一:模拟毛笔

现需要提供大中小3种型号的画笔,能够绘制5种不同颜色,如果使用蜡笔,我们需要准备3*5=15支蜡笔,也就是说必须准备15个具体的蜡笔类。而如果使用毛笔的话,只需要3种型号的毛笔,外加5个颜料盒,用3+5=8个类就可以实现15支蜡笔的功能。本实例使用桥接模式来模拟毛笔的使用过程。

image-20230627220431701

实例二:跨平台视频播放器

如果需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Linux、Unix等)上播放多种格式的视频文件,常见的视频格式包括MPEG、RMVB、AVI、WMV等。现使用桥接模式设计该播放器。

image-20230627220841600

6.6 模式优缺点

桥接模式的优点

  1. 分离抽象接口及其实现部分。
  2. 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法
  3. 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
  4. 实现细节对客户透明,可以对用户隐藏实现细节。

桥接模式的缺点

  1. 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
  2. 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性

6.7 模式适用环境

在以下情况下可以使用桥接模式:

  1. 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  2. 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
  3. 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
  4. 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
  5. 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

6.8 模式应用

  1. Java语言通过Java虚拟机实现了平台的无关性。

image-20230627221049267

  1. 一个 Java桌面软件总是带有所在操作系统的视感(LookAndFeel),如果一个Java软件是在Unix系统上开发的,那么开发人员看到的是Motif用户界面的视感;在Windows上面使用这个系统的用户看到的是Windows用户界面的视感;而一个在Macintosh上面使用的用户看到的则是Macintosh用户界面的视感,Java语言是通过所谓的Peer架构做到这一点的。Java为AWT中的每一个GUI构件都提供了一个Peer构件,在AWT中的Peer架构就使用了桥接模式

  2. JDBC驱动程序也是桥接模式的应用之一。使用JDBC驱动程序的应用系统就是抽象角色,而所使用的数据库是实现角色。一个JDBC驱动程序可以动态地将一个特定类型的数据库与一个Java应用程序绑定在一起,从而实现抽象角色与实现角色的动态耦合

6.9 模式扩展

适配器模式与桥接模式的联用

  1. 桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况。

image-20230627221147628

6.10 小结

  1. 桥接模式将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
  2. 桥接模式包含如下四个角色:
    1. 抽象类中定义了一个实现类接口类型的对象并可以维护该对象
    2. 扩充抽象类扩充由抽象类定义的接口,它实现了在抽象类中定义的抽象业务方法,在扩充抽象类中可以调用在实现类接口中定义的业务方法
    3. 实现类接口定义了实现类的接口,实现类接口仅提供基本操作,而抽象类定义的接口可能会做更多更复杂的操作
    4. 具体实现类实现了实现类接口并且具体实现它,在不同的具体实现类中提供基本操作的不同实现,在程序运行时,具体实现类对象将替换其父类对象,提供给客户端具体的业务操作方法。
  3. 在桥接模式中,抽象化(Abstraction)与实现化(Implementation)脱耦,它们可以沿着各自的维度独立变化。
  4. 桥接模式
    1. 主要优点是分离抽象接口及其实现部分,是比多继承方案更好的解决方法,桥接模式还提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,实现细节对客户透明,可以对用户隐藏实现细节
    2. 主要缺点是增加系统的理解与设计难度,且识别出系统中两个独立变化的维度并不是一件容易的事情
  5. 桥接模式适用情况包括:需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系;抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响;一个类存在两个独立变化的维度,且这两个维度都需要进行扩展;设计要求需要独立管理抽象化角色和具体化角色;不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统。

7. 享元模式(对象)

7.1 模式动机

  • 面向对象技术可以很好地解决一些灵活性或可扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时,将导致运行代价过高,带来性能下降等问题

  • 享元模式正是为解决这一类问题而诞生的。享元模式通过共享技术实现相同或相似对象的重用

image-20230627202717629

  • 在享元模式中可以共享的相同内容称为内部状态(Intrinsic State),而那些需要外部环境来设置的不能共享的内容称为外部状态(Extrinsic State),由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。
  • 在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)用于存储具有相同内部状态的享元对象
  • 在享元模式中共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称为细粒度对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用

7.2 模式定义

  1. 享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式
  2. Flyweight Pattern: Use sharing to support large numbers of fine-grained objects efficiently.

7.3 模式结构

image-20230627210428493

  • 享元工厂和原型管理器非常相似

  • 享元模式包含如下角色:

    • Flyweight: 抽象享元类
    • ConcreteFlyweight: 具体享元类
    • UnsharedConcreteFlyweight: 非共享具体享元类
    • FlyweightFactory: 享元工厂类
  • 享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。

image-20230627210554872

7.4 模式分析

  • 享元模式的核心在于享元工厂类享元工厂类的作用在于提供一个用于存储享元对象的享元池,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象

  • 比较原型管理模式和享元工厂模型

  • 典型的享元工厂类代码:

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) {
......
}
}

7.5 享元模式实例与解析

实例一:共享网络设备(无外部状态)

很多网络设备都是支持共享的,如交换机、集线器等,多台终端计算机可以连接同一台网络设备,并通过该网络设备进行数据转发,如图所示,现用享元模式模拟共享网络设备的设计原理。

image-20230627210852187
image-20230627210918202

实例二:共享网络设备(有外部状态)

虽然网络设备可以共享,但是分配给每一个终端计算机的端口(Port)是不同的,因此多台计算机虽然可以共享同一个网络设备,但必须使用不同的端口。我们可以将端口从网络设备中抽取出来作为外部状态,需要时再进行设置。

image-20230627210952014

7.6 模式优缺点

享元模式的优点

  1. 享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
  2. 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享

享元模式的缺点

  1. 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化
  2. 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长

7.7 模式适用环境

在以下情况下可以使用享元模式:

  1. 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。
  2. 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
  3. 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式

7.8 模式应用

  1. 享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片对象,通过在应用程序中设置该图片出现的位置,可以实现该图片在不同地方多次重复显示。
  2. 在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
}
}

7.9 模式扩展

单纯享元模式和复合享元模式

单纯享元模式:在单纯享元模式中,所有的享元对象都是可以共享的,即所有抽象享元类的子类都可共享,不存在非共享具体享元类。

image-20230627211404485

复合享元模式:将一些单纯享元使用组合模式加以组合,可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。

image-20230627211422141

享元模式与其他模式的联用

  1. 在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
  2. 在一个系统中,通常只有唯一一个享元工厂,因此享元工厂类可以使用单例模式进行设计
  3. 享元模式可以结合组合模式形成复合享元模式,统一对享元对象设置外部状态。

7.10 小结

  1. 享元模式运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用,它是一种对象结构型模式。
  2. 享元模式包含四个角色
    1. 抽象享元类声明一个接口,通过它可以接受并作用于外部状态
    2. 具体享元类实现了抽象享元接口,其实例称为享元对象;非共享具体享元是不能被共享的抽象享元类的子类
    3. 享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中。
  3. 享元模式以共享的方式高效地支持大量的细粒度对象,享元对象能做到共享的关键是区分内部状态和外部状态。其中内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享;外部状态是随环境改变而改变的、不可以共享的状态。
  4. 享元模式
    1. 主要优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份
    2. 其缺点是使得系统更加复杂,并且需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
  5. 享元模式适用情况包括
    1. 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费
    2. 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中;多次重复使用享元对象。