代理模式(Proxy)

代理模式是通过创建一个代理对象,用这个代理对象去代表真实的对象,客户端得到这个代理对象后,对客户端并没有什么影响,就跟得到了真实对象一样来使用。

当客户端操作这个代理对象的时候,实际上功能最终还是会由真实的对象来完成,只不过是通过代理操作的,也就是客户端操作代理,代理操作真正的对象。

img

■ Proxy:代理对象,通常具有如下功能。实现与具体的目标对象一样的接口,这样就可以使用代理来代替具体的目标对象。保存一个指向具体目标对象的引用,可以在需要的时候调用具体的目标对象。

可以控制对具体目标对象的访问,并可以负责创建和删除它。

■ Subject:目标接口,定义代理和具体目标对象的接口,这样就可以在任何使用具体目标对象的地方使用代理对象。

■ RealSubject:具体的目标对象,真正实现目标接口要求的功能。

事实上代理又被分成多种,大致有如下一些。

■ 虚代理:根据需要来创建开销很大的对象,该对象只有在需要的时候才会被真正创建。

■ 远程代理:用来在不同的地址空间上代表同一个对象,这个不同的地址空间可以是在本机,也可以在其他机器上。在Java里面最典型的就是RMI技术。

■ copy-on-write代理:在客户端操作的时候,只有对象确实改变了,才会真的拷贝(或克隆)一个目标对象,算是虚代理的一个分支。

■ 保护代理:控制对原始对象的访问,如果有需要,可以给不同的用户提供不同的访问权限,以控制他们对原始对象的访问。

■ Cache代理:为那些昂贵操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。

■ 防火墙代理:保护对象不被恶意用户访问和操作。

■ 同步代理:使多个用户能够同时访问目标对象而没有冲突。

■ 智能指引:在访问对象时执行一些附加操作,比如,对指向实际对象的引用计数、第一次引用一个持久对象时,将它装入内存等。

代理模式的本质:控制对象访问。代理模式通过代理目标对象,把代理对象插入到客户和目标对象之间,从而为客户和目标对象引入一定的间接性。正是这个间接性,给了代理对象很多的活动空间。代理对象可以在调用具体的目标对象前后,附加很多操作,从而实现新的功能或是扩展目标对象的功能。

适配器模式(Adaptor)

适配器模式: 将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

适配器模式的结构如下图所示:

image-20210805223021245

■ Client:客户端,调用自己需要的领域接口Target。

■ Target:定义客户端需要的跟特定领域相关的接口。

■ Adaptee:已经存在的接口,通常能满足客户端的功能要求,但是接口与客户端要求的特定领域接口不一致,需要被适配。

■ Adapter:适配器,把Adaptee适配成为Client需要的Target。

适配器模式的主要功能是进行转换匹配,目的是复用已有的功能,而不是来实现新的接口。也就是说,客户端需要的功能应该是已经实现好了的,不需要适配器模式来实现,适配器模式主要负责把不兼容的接口转换成客户端期望的样子就可以了。

适配器模式的本质是:转换匹配,复用功能。

装饰器模式(Decorator)

装饰器模式: 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成子类更为灵活。装饰模式能够实现动态地为对象添加功能,是从一个对象外部来给对象增加功能,相当于是改变了对象的外观。当装饰过后,从外部使用系统的角度看,就不再是使用原始的那个对象了,而是使用被一系列的装饰器装饰过后的对象。

其实,现在在面向对象的设计中,有一条基本的规则就是“尽量使用对象组合,而不是对象继承”来扩展和复用功能。装饰模式的思考起点就是这个规则。

装饰模式的本质:动态组合。 动态是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰器的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明地增加功能。

Java的InputStream,装饰器模式

装饰模式在Java中最典型的应用,就是I/O流,简单回忆一下,如果使用流式操作读取文件内容,会怎样实现呢?简单的代码示例如下:

1
2
3
DataInputStream in = new DataInputStream(
            new BufferedInputStream(
                new FileInputStream("filename")));

仔细观察上面的代码,会发现最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream处理,再把处理后的对象传递给了DataInputStream对象进行处理,这个过程其实就是装饰器的组装过程,FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器。

image-20210805225712408

■ InputStream就相当于装饰模式中的Component。

■ 其实FileInputStream、ObjectInputStream、StringBufferInputStream这几个对象是直接继承了InputSream,还有几个直接继承InputStream的对象,比如ByteArrayInputStream、PipedInputStream等。这些对象相当于装饰模式中的ConcreteComponent,是可以被装饰器装饰的对象。

■ FilterInputStream就相当于装饰模式中的Decorator,而它的子类DataInputStream、BufferedInputStream、LineNumberInputStream和PushbackInputStream就相当于装饰模式中的ConcreteDecorator了。另外FilterInputStream和它的子类对象的构造器,都是传入组件InputStream类型,这样就完全符合前面讲述的装饰器的结构了。

外观模式(Facade)

外观模式: 为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 外观模式就是通过引入这么一个外观类,在这个类里面定义客户端想要的简单的方法,然后在这些方法的实现里面,由外观类再去分别调用内部的多个模块来实现功能,从而让客户端变得简单。这样一来,客户端就只需要和外观类交互就可以了。

外观模式的目的不是给子系统添加新的功能接口,而是为了让外部减少与子系统内多个模块的交互,松散耦合,从而让外部能够更简单地使用子系统。

外观模式也有缺点,过多的或者是不太合理的Facade也容易让人迷惑。到底是调用Facade好呢,还是直接调用模块好。

外观模式的本质是:封装交互,简化调用。 外观模式很好地体现了“最少知识原则”。

在Java中,Slf4j (Simple Logging Facade For Java) 就是在日常开发中使用的比较多的门面模式(外观模式)。

外观模式与适配器模式的区别

  • 适配器模式的对象适配器利用一个新的类实现当前所需的接口,然后包装旧的类来完成适配,避免对旧代码的更改下支持新的代码。
  • 实现适配器需要的工作与接口大小成正比,但是适配器将端的变化封装在里面,更易用有效。客户不用为了新的接口而改变自己,减少了新旧代码混用时可能发生的问题(要记得设计模式的重要目的就是减少旧代码的损耗提高维护效率)
  • 外观模式不改变接口,不进行封装,只是对一系列复杂的调用的接口简化整合,创造出新的简化用的接口,方便用户调用
  • 外观模式一般需要外观类能访问子系统的所有组件,然后才用简化的接口来对子系统的一些接口包装整合

外观模式与策略模式的区别

乍一看,外观模式和策略模式看起来很像。策略模式也是对于同一个要实现的目标,提供多种不同的,可以相互替换的实现。

  • 策略模式强调的是不同的实现策略之间可以相互转换。
  • 外观模式是将相同意义的操作,进行封装,减少服务之间的依赖关系,提高灵活性。 两者的目的并不相同。

桥接模式(Bridge)

桥接模式: 将抽象与实现分离,使它们可以独立变化。它是用关联关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式。

我们大家都熟悉,顾名思义就是用来将河的两岸联系起来的。而此处的桥是用来将两个独立的结构联系起来,而这两个被联系起来的结构可以独立的变化,所有其他的理解只要建立在这个层面上就会比较容易。

下面是一些官方的说明,比较晦涩,必须等你有一定的经验后才能理解:

  • 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。

  • “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。

  • 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。

  • 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

JDBC与桥接

在Java应用中,对于桥接模式有一个非常典型的例子,就是应用程序使用JDBC驱动程序进行开发的方式。所谓驱动程序,指的是按照预先约定好的接口来操作计算机系统或者是外围设备的程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
String sql = "";
    
Class.forName("com.mysql.xxx");
    
Connection conn = DriverManager.getConnection("url", "user", "pass");

PreparedStatement pstmt = conn.prepareStatement(sql);

ResultSet rs = pstmt.executeQuery();
    
while(rs.next()){
     // 
    rs.getInt(2);
}
    
rs.close();
pstmt.close();
conn.close();

从上面的示例可以看出,我们写的应用程序,是面向JDBC的API开发,这些接口就相当于桥接模式中抽象部分的接口。那么怎样得到这些API呢?是通过DriverManager来得到的。

那么这些JDBC的API,谁去实现呢?光有接口,没有实现也不行啊。

该驱动程序登场了,JDBC的驱动程序实现了JDBC的API,驱动程序就相当于桥接模式中的具体实现部分。而且不同的数据库,由于数据库实现不一样,可执行的sql也不完全一样,因此对于JDBC驱动的实现也是不一样的,也就是不同的数据库会有不同的驱动实现。

有了抽象部分——JDBC的API和具体实现部分——驱动程序,那么它们如何连接起来呢?就是如何桥接呢?

就是前面提到的DriverManager来把它们桥接起来。从某个侧面来看,DriverManager在这里起到了类似于简单工厂的功能,基于JDBC的应用程序需要使用JDBC的API,如何得到呢?就通过DriverManager来获取相应的对象。

那么此时系统的整体结构如下图所示。

image-20210806173721445

基于JDBC的应用程序,使用JDBC的API,相当于是对数据库操作的抽象扩展,算做桥接模式的抽象部分;而具体的接口实现是由驱动来完成的,驱动就相当于桥接模式的实现部分了。而桥接的方式,不再是让抽象部分持有实现部分,而是采用了类似于工厂的做法,通过DriverManager来把抽象部分和实现部分对接起来,从而实现抽象部分和实现部分解耦。

JDBC的这种架构,把抽象部分和具体部分分离开来,从而使得抽象部分和具体部分都可以独立扩展。对于应用程序而言,只要选用不同的驱动,就可以让程序操作不同的数据库,而无需更改应用程序,从而实现在不同的数据库上移植;对于驱动程序而言,为数据库实现不同的驱动程序,并不会影响应用程序。而且,JDBC的这种架构,还合理地划分了应用程序开发人员和驱动程序开发人员的边界。

对于有些朋友会认为,从局部来看,体现了策略模式,比如,在上面的结构中删除“JDBC的API和基于JDBC的应用程序”,那么剩下的部分,看起来就是一个策略模式的体现。此时的DriverManager相当于上下文,而各个具体驱动的实现就相当于是具体的策略实现。这个理解也不算错,但是在这里看来,这样理解是比较片面的。

这两个模式虽然相似,但也还是有区别的。最主要的是模式的目的不一样,策略模式的目的是封装一系列的算法,使得这些算法可以相互替换;而桥接模式的目的是分离抽象部分和实现部分,使得它们可以独立地变化。

组合模式(Composite)

组合模式: 将对象组合成树型结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

image-20210806174946867

■ Component:抽象的组件对象,为组合中的对象声明接口,让客户端可以通过这个接口来访问和管理整个对象结构,可以在里面为定义的功能提供缺省的实现。

■ Leaf:叶子节点对象,定义和实现叶子对象的行为,不再包含其他的子节点对象。

■ Composite:组合对象,通常会存储子组件,定义包含子组件的那些组件的行为,并实现在组件接口中定义的与子组件有关的操作。

■ Client:客户端,通过组件接口来操作组合结构里面的组件对象。

组合模式的本质:统一叶子对象和组合对象。

组合模式在实际的应用中用的也比较多,一般用于表示树形结构,例如项目树、行政区划的树形结构等。

享元模式(Flyweight)

享元模式: 享元模式也称蝇量模式。在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。 享元模式运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

在Java中,很多地方也使用了享元模式,例如比较常见的Integer包装类型,对于-127 ~ 128 之间的Integer对象,都是通过缓存直接获取的,并不会新建对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 public static void main(String[] args) {
     Integer n1 = 3;
     Integer n2 = 3;

     System.out.println(n1 == n2);

    Integer n3 = 345;
    Integer n4 = 345;
    System.out.println(n3 == n4);

}

// 打印 true
//     false

Java中使用享元模式来生成常用的Integer类型。

1
2
3
4
5
    public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
    }

享元模式与对象池化技术

享元模式与对象池化技术(线程池、连接池等)看起来比较类似,他们的区别在于使用的方式不同。对象池只能同时被一个客户端同时使用,池化的对象要从池中检出来,用完之后还要归还到池中。相反,享元的对象是单例的,它可以被多个客户端同时使用。

参见: https://stackoverflow.com/questions/30422525/what-are-the-differences-between-flyweight-and-object-pool-patterns