文章目录[隐藏]
结构型模式
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性
结构型模式分为以下 7 种:
- 代理模式
- 适配器模式
- 装饰者模式
- 桥接模式
- 外观模式
- 组合模式
- 享元模式
1. 代理模式
1.1 介绍
代理模式(Proxy)是一种常见的设计模式,它允许通过代理对象控制对某个对象的访问。在代理模式中,代理类扮演着客户端和真正的目标对象之间的中介角色,代理类可以为目标对象提供额外的功能,例如远程访问、延迟加载、权限控制等
使用代理模式可以实现对象的封装,同时也能够降低系统耦合度,增强了系统的灵活性和可扩展性。如果在开发过程中需要对某个对象进行控制,并且希望保持系统的高内聚、低耦合特性,那么代理模式是一个不错的选择
1.2 结构
代理(Proxy)模式分为三种角色:
- 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法
- 真实主题(Real Subject)类: 实现了抽象主题类中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象
- 代理(Proxy)类 : 提供了与真实主题类相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能
1.3 静态代理
在静态代理中,代理类和真实类都要实现相同的接口或者继承相同的抽象类。代理类负责将客户端请求转发给真实对象,并且可以在调用真实对象前后添加一些额外的逻辑
值得注意的是,静态代理需要手动编写代理类,代码量较大,但是运行效率较高
如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:
- 抽象主题(Subject)类:
public interface SellTickets {
void sell();
}
- 真实主题(Real Subject)类:
public class TrainStation implements SellTickets{
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
- 代理(Proxy)类 :
public class ProxyPoint implements SellTickets {
private TrainStation station = new TrainStation();
@Override
public void sell() {
System.out.println("代理点收取一些服务费用");
station.sell();
}
}
- 测试类:
public class Client {
public static void main(String[] args) {
// 1. 创建代售点类对象
ProxyPoint point = new ProxyPoint();
// 2. 调用方法进行卖票
point.sell();
}
}
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强。运行结果如下:
1.4 JDK动态代理
JDK动态代理是一种在运行时生成代理对象的技术。它允许我们在不修改源代码的情况下,通过代理对象来调用目标对象的方法
其通常用于实现 AOP(面向切面编程) 和 RPC(远程过程调用协议) 等功能。在AOP中,代理对象可以在执行目标对象的方法前后进行一些额外的操作,如日志记录、事务管理等。而在远程方法调用中,代理对象可以隐藏底层的网络通信细节,使得远程调用看起来就像本地调用一样
JDK动态代理的原理是基于反射机制和接口实现的。通过获取目标对象的接口信息和实现类,然后创建一个新的代理类并实现相同的接口,并在代理类中处理特定的逻辑操作
public class JDKProxyFactory {
private Object target;
public JDKProxyFactory(Object target) {
this.target = target;
}
public Object getProxy() {
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
/**
* proxy: 代理对象
* method: 代理对象接口方法实例
* args: 代理对象接口方法时传递的实际参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(target, args);
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
- 测试类:
public class Client {
public static void main(String[] args) {
// 1. 创建代理工厂对象
JDKProxyFactory proxyFactory = new JDKProxyFactory(new TrainStation());
// 2. 获取代理对象
SellTickets proxy = (SellTickets) proxyFactory.getProxy();
// 3. 调用方法进行卖票
proxy.sell();
}
}
JDKProxyFactory代理工厂是我们代理模式中的代理类吗?
答案为并不是。JDKProxyFactory只是一个动态生成代理类的一个工厂,而代理类是程序在运行过程中动态的在内存中生成的类
在动态代理中,底层通过反射获取到目标调用的方法,然后通过自定义的 InvocationHandler
中的 invoke
方法实现对目标方法的增强。通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)查看生成代理类的结构(精简版):
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("com.enndfp.pattern.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
}
public final void sell() {
this.h.invoke(this, m3, null);
}
}
Arthas 生成的完整的代码如下,感兴趣的小伙伴可以自行查看:
package com.sun.proxy;
import com.enndfp.pattern.proxy.jdk_proxy.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0
extends Proxy
implements SellTickets {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.enndfp.pattern.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException noSuchMethodException) {
throw new NoSuchMethodError(noSuchMethodException.getMessage());
}
catch (ClassNotFoundException classNotFoundException) {
throw new NoClassDefFoundError(classNotFoundException.getMessage());
}
}
public final boolean equals(Object object) {
try {
return (Boolean)this.h.invoke(this, m1, new Object[]{object});
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString() {
try {
return (String)this.h.invoke(this, m2, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final int hashCode() {
try {
return (Integer)this.h.invoke(this, m0, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final void sell() {
try {
this.h.invoke(this, m3, null);
return;
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
}
1.5 CGLIB动态代理
CGLIB动态代理是一种Java动态代理技术,它可以在运行时动态地生成一个子类来作为被代理对象的代理。相比于JDK自带的动态代理,CGLIB动态代理使用更加灵活,它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充
CGLIB动态代理和JDK动态代理最大的区别就是前者使用的是第三方包,不需要有抽象主题的接口,后者是JDK自带的,必须要有抽象主题接口
CGLIB动态代理的原理是通过继承被代理类,然后重写其中的方法实现代理功能。当调用被代理类的方法时,实际上是调用了代理类中重写的方法。这样就可以对被代理类的方法进行增强或拦截, 从而实现AOP(面向切面编程)的功能
CGLIB是第三方提供的资源包,所以在使用之前需要引入jar包依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
具体实现步骤如下:
- 创建 Enhancer 实例:
Enhancer
是 CGLIB 中的主要类,用于生成代理对象。需要 使用Enhancer
创建一个新的代理对象 - 设置父类:CGLIB生成的代理对象是目标类的子类,因此需要设置父类。这可以通过调用
Enhancer.setSuperclass()
方法来完成 - 设置回调:回调是代理对象将要执行的操作。可以使用
MethodInterceptor
或CallbackFilter
等类来设置回调 - 创建代理对象:最后一步是使用
Enhancer.create()
方法创建代理对象
public class CglibProxyFactory implements MethodInterceptor {
private Object target;
public CglibProxyFactory(Object target) {
this.target = target;
}
public Object getProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
/**
* @param o 代理对象
* @param method 被代理的方法
* @param objects 方法参数
* @param methodProxy cglib方法
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("代理点收取一些服务费用");
Object result = method.invoke(target, objects);
return result;
}
}
- 测试类:
public class Client {
public static void main(String[] args) {
// 1. 创建代理工厂对象
CglibProxyFactory proxyFactory = new CglibProxyFactory(new TrainStation());
// 2. 获取代理对象
TrainStation proxy = (TrainStation) proxyFactory.getProxy();
// 3. 调用方法进行卖票
proxy.sell();
}
}
1.6 对比
(1)静态代理和动态代理
静态代理和动态代理是代理模式中两种不同的实现方式,它们之间的区别主要体现在以下几个方面:
- 代理类生成时期不同。静态代理在编译期就已经确定了代理类与目标类的关系,即代理类和目标类是早已确定并且固定的。而动态代理则是在运行时通过反射机制动态地生成代理类,使其具有与目标类相同的接口和方法
- 灵活性不同。由于静态代理在编译期就确定了代理类和目标类的关系,因此它的灵活性较差,无法在运行时改变代理类和目标类的关系。而动态代理则可以根据需要在运行时生成代理类,并动态地指定具体的目标类对象,从而具有更高的灵活性
- 实现原理不同。静态代理在程序编写和编译时,需要开发人员手动编写代理类和目标类的代码,较为繁琐。而动态代理是通过Java反射机制生成代理类的字节码,并加载到JVM 中,然后动态创建代理实例。这种方式大大简化了代理类的开发工作
(2)JDK 和 CGLIB
JDK动态代理和CGLIB动态代理都是Java中的动态代理技术,它们的主要区别在于实现方式和适用场景:
- JDK动态代理是通过反射机制来实现的,在运行时动态地创建一个实现指定接口的代理类,代理类中的方法调用会被转发到
InvocationHandler
进行处理。因此,JDK动态代理只能代理实现了接口的类,并且生成的代理类只能代理接口中声明的方法,对于其他方法则无法代理 - CGLIB动态代理则是通过继承目标类来实现的,它创建的代理类是目标类的子类,重写了目标类中的非final方法,并将它们分派到
Callback
中定义的拦截器中去处理。因此,CGLIB动态代理可以代理没有实现接口的类,并且可以代理目标类中所有非final方法
简而言之,JDK动态代理适用于代理有接口的类,而CGLIB动态代理则适用于代理没有接口或者需要代理目标类中所有非final方法的类。因此大部分情况下有接口用JDK,无接口用CGLIB
除了上述区别之外,JDK动态代理和CGLIB动态代理还有一些其他的差异:
- 性能:一般情况下,后者性能比前者要好。JDK动态代理使用反射机制动态创建代理类,生成代理对象的效率相对较低;而CGLIB动态代理则是直接生成目标类的子类,因此生成代理对象的效率较高
- 内存占用:由于CGLIB动态代理创建的代理类是目标类的子类,所以代理类会继承目标类的所有非私有属性和方法,导致代理类的内存占用比较大;而JDK动态代理生成的代理类只包含需要代理的接口方法,因此内存占用相对较小
- 依赖性:JDK动态代理是Java原生的API,不需要引入第三方库,而CGLIB动态代理需要引入cglib库进行支持
- 版本兼容性:JDK动态代理是Java原生API,因此具有很好的版本兼容性;而CGLIB动态代理在不同版本的Java环境下可能存在兼容性问题,推荐使用的是 2.2.2 及以下版本
这里补充一个性能相关的小知识,在Java中,JDK在5、6、7、8等版本中都对动态代理进行了优化,使得在JDK8及之后,JDK动态代理的性能与CGLIB动态代理性能持平甚至反超
- 在JDK5中,Java引入了新的虚拟机指令——"
invokedynamic
",该指令的出现为动态语言的实现提供了更广泛的支持。这项技术的引入也为Java的动态代理提供了更好的性能和灵活性- 在JDK6和7中,Java对反射机制进行了一系列优化,使得动态代理的创建和调用效率得到了显著提升
- 在JDK8中,Java引入了默认方法和Lambda表达式等新特性,这些特性进一步提升了动态代理的性能和效率。同时,JDK8还引入了
MethodHandle
类,可以更高效地调用方法
1.7 优缺点
优点:
- 可以在客户端毫无察觉的情况下控制服务对象
- 如果客户端对服务对象的生命周期没有特殊要求, 可以对生命周期进行管理
- 即使服务对象还未准备好或不存在, 代理也可以正常工作
- 符合开闭原则。 可以在不对服务或客户端做出修改的情况下创建新代理
缺点:
- 代码可能会变得复杂, 因为需要新建许多类
- 服务响应可能会延迟
1.8 使用场景
代理模式是一种结构型设计模式,它通过增加一个代理对象来控制对原始对象的访问。代理对象可以在不改变原始对象的前提下,实现额外的功能或者控制访问级别
下面是一些代理模式中常见的使用场景:
- AOP(面向切面编程):通过动态代理,在方法前后自动添加日志记录、权限控制、性能统计等通用功能,避免了代码冗余,提高了代码的复用性和可维护性
- RPC(远程过程调用):在分布式系统中,动态代理可以将远程方法调用封装成本地方法调用,使得远程调用像本地调用一样简单,同时也支持负载均衡、容错等功能
- 数据库连接池:数据库连接池可以通过动态代理来实现,每次获取连接时,动态代理会检查当前连接是否可用,如果已经关闭或者超时,则重新创建连接返回给用户
- 缓存框架:缓存框架可以通过动态代理来实现,当一个对象需要缓存时,动态代理可以根据缓存配置来判断是否需要从缓存中获取数据,还是直接从数据库中获取数据并更新缓存
- 日志框架:动态代理可以用于实现日志框架,例如Spring AOP中的日志切面,可以动态地在方法前后添加日志记录代码,以此来监控系统运行情况,方便问题排查
- 虚拟代理:虚拟代理是一种延迟加载技术,它允许对象在真正需要时才被创建。例如,在需要显示大量图片的应用程序中,可以使用虚拟代理来延迟加载图片,只有当用户需要查看图片时才会加载
2. 适配器模式
2.1 介绍
适配器模式是一种常用的结构型设计模式,核心思想是将现有的接口转换为客户端所期望的接口。它允许通过将一个接口转换为另一个接口,将不兼容的类或对象组合在一起。这种模式通常用于集成现有系统或库中不兼容的组件
在软件开发中,我们经常会遇到由不同的团队或不同的供应商编写的代码、服务或库,这些组件可能使用不同的协议、数据格式或接口定义,因此无法直接集成在一起。为了解决这个问题,我们可以使用适配器模式来创建一个适配器,它可以将这些不兼容的组件转换为一个统一的接口,从而实现它们之间的互操作性
2.2 结构
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:该角色是所需的客户端接口,也就是客户端希望使用的接口。在适配器模式中,我们需要设计一个新的目标接口来满足客户端的需求
- 适配者(Adaptee)接口:该角色是需要被适配的现有接口,它与目标接口不兼容,无法直接使用。在适配器模式中,我们需要将适配者接口适配成目标接口,以便客户端能够使用
- 适配器(Adapter)类:该角色是适配器模式的核心,其作用是将不兼容的接口转换为目标接口。适配器可以通过继承或组合等方式实现
2.3 类适配器模式
一个适配器类来实现适配者接口,同时又继承目标接口
现有一台电脑只能读取SD卡,而要读取TF卡中的内容就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。类图如下:
- 目标(Target)接口:
public interface TFCard {
String readTF();
void writeTF(String msg);
}
public class TFCardImpl implements TFCard {
@Override
public String readTF() {
String msg = "tf card read msg : hello word tf card";
return msg;
}
@Override
public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}
- 适配者(Adaptee)接口:
public interface SDCard {
String readSD();
void writeSD(String msg);
}
public class SDCardImpl implements SDCard {
@Override
public String readSD() {
String msg = "sd card read a msg :hello word SD";
return msg;
}
@Override
public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}
- 适配器(Adapter)类:
public class SDAdapterTF extends TFCardImpl implements SDCard {
@Override
public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}
@Override
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}
- 测试类:
public class Computer {
public String readSD(SDCard sdCard) {
if (sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}
public class Client {
public static void main(String[] args) {
// 1. 创建计算机对象
Computer computer = new Computer();
// 2. 读取SD卡中的数据
System.out.println(computer.readSD(new SDCardImpl()));
System.out.println("=============");
// 3. 使用该电脑读取TF卡中的内容
// 定义适配器类
String msg = computer.readSD(new SDAdapterTF());
System.out.println(msg);
}
}
2.4 对象适配器模式
将目标接口通过聚合的方式引入适配器类中,该类同时实现适配者接口
- 适配器(Adapter)类:
public class SDAdapterTF implements SDCard {
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
@Override
public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}
@Override
public void writeSD(String msg) {
System.out.println("adapter write tf card");
tfCard.writeTF(msg);
}
}
- 测试类:
public class Client {
public static void main(String[] args) {
// 1. 创建计算机对象
Computer computer = new Computer();
// 2. 读取SD卡中的数据
System.out.println(computer.readSD(new SDCardImpl()));
System.out.println("=============");
// 3. 使用该电脑读取TF卡中的内容
// 定义适配器类
String msg = computer.readSD(new SDAdapterTF(new TFCardImpl()));
System.out.println(msg);
}
}
2.5 接口适配器
接口适配器模式是一种结构型设计模式,用于解决一个接口具有多个方法,但某些类只需要使用其中的一部分方法的情况。接口适配器模式通过创建一个抽象类来实现适配者接口,并提供这个接口的所有方法的默认实现。然后,具体的类可以选择性地继承这个抽象类,并只实现它们需要的方法。这允许类按需实现接口中的方法,而无需实现所有方法
- 抽象适配器类:
public abstract class SDCardAdapter implements SDCard {
@Override
public String readSD() {
return null;
}
@Override
public void writeSD(String msg) {
}
}
- 适配器类:
public class SDAdapterTF extends SDCardAdapter {
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
@Override
public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}
@Override
public void writeSD(String msg) {
System.out.println("adapter write tf card");
tfCard.writeTF(msg);
}
}
2.6 对比
类适配器、对象适配器、接口适配器都是适配器模式的实现方式,它们的目的是将一个类或接口转换成另一个类或接口,以满足不同的业务需求。它们之间的联系和区别如下:
- 类适配器:类适配器通过继承目标接口和实现适配者接口的方式,来实现对适配类的适配。具体来说,在适配器中包含了目标接口的实例,并实现了适配者接口的方法,以便客户端调用。这种方式可以在不改变已有代码的情况下进行适配,但只能适配单个目标接口
- 对象适配器:对象适配器通过聚合目标接口和实现适配者接口的方式,来实现对适配类的适配。具体来说,在适配器中包含了目标接口的实例,并通过实现适配者接口的方式,将目标接口的方法委托给适配器来实现。这种方式可以适配多个目标接口, 而且更加灵活,因为可以在运行时动态设置待适配类的实例
- 接口适配器:接口适配器通过定义一个抽象适配器类,实现适配者接口的所有方法,并将它们设置成空方法。适配器类只需要通过继承抽象类实现需要的方法即可,避免了实现不必要的方法,也使得适配器更加灵活
2.7 优缺点
优点:
- 提高代码复用性:适配器模式可以重用已有的代码,减少代码量
- 提高系统的灵活性:适配器模式可以使得系统更加灵活,易于扩展和维护
- 降低耦合度:适配器模式可以将不同的模块之间解耦,使得各个模块之间的依赖关系更加简单明了
- 可以适配多个类或接口:不同的适配器实现方式可以适配多个类或接口,提高代码的可复用性
缺点:
- 增加代码复杂性:适配器模式需要增加新的适配器类或方法,会增加代码的复杂性
- 可能会造成性能损失:适配器模式可能会引入额外的开销,例如对象适配器需要组合待适配类的实例对象
- 不易理解:适配器模式可能会使代码结构变得复杂,不易于阅读和理解
2.8 使用场景
适配器模式是一种常用的设计模式,主要应用于以下场景:
- 处理旧接口与新接口的兼容性问题:当系统中的某个组件需要调用另一个组件的接口时, 如果这两个组件的接口不兼容,可以使用适配器模式将旧接口转换成新接口
- 重用已有的代码:适配器模式可以重用现有的代码,减少代码量,提高代码的可复用性
- 构建抽象接口:适配器模式可以将多个类或接口适配成一个抽象接口,使得客户端只需要针对抽象接口编程,而不需要关注具体的实现细节
- 隐藏不必要的接口:适配器模式可以隐藏一些不必要的接口,避免客户端直接访问实现类的方法,提高代码的安全和稳定性
- 适配不同的数据格式:适配器模式可以适配不同的数据格式,例如将 XML 数据转换成 JSON 格式
3. 装饰者模式
3.1. 介绍
装饰者模式(Decorator)是一种结构型设计模式,它允许你在不改变对象自身的基础上,动态地给一个对象添加额外的功能。该模式是通过创建一个包装对象来实现的,也就是用一个新的对象来包装真实的对象。这个装饰对象与原始对象拥有相同的接口,因此客户端无需更改代码即可使用装饰后的对象
3.2. 结构
在装饰者模式中,一般会涉及到下面四种角色:
- 抽象构件(Component)角色:它是具体构件类和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作
- 具体构件(Concrete Component)角色:它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)
- 抽象装饰(Decorator)角色:它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的
- 具体装饰(ConcreteDecorator)角色:它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为
3.3 实现方式
(1)案例引入
在生活中或多或少大家应该都点过奶茶,我们就以奶茶为例,假设“007奶茶店”中有原味奶茶和茉莉奶绿两种奶茶,而配料则是有红糖珍珠和芝士奶盖两种,每种奶茶都能添加不同的配料,且价格不同
现在要求的是计算用户下单不同奶茶的价格,我们很直观的能够想象到把每种情况都列举出来即可,通过继承实现多种不同的搭配:
但是有个问题不知道大家有没有看出来,通过这种继承的方式,很容易产生类爆炸,种类少还好,一旦组合多起来那将是不可描述的一场类灾难,这时我们就可以使用上这里说到的装饰者模式来进行优化
(2)实现步骤
实现装饰者模式的步骤如下:
- 定义一个基础接口或抽象类,作为所有具体组件和装饰者的公共接口
- 创建具体的组件类,实现基础接口或抽象类,并提供基础功能
- 创建一个抽象的装饰者类,它包含一个基础接口或抽象类类型的成员变量,并实现基础接口或抽象类。这个类通常是一个抽象类,因为它的目的是让子类来扩展装饰行为
- 创建具体的装饰者类,继承自抽象的装饰者类,重写基础方法并在方法执行前后添加自己的逻辑,还可以增加新的方法
- 在客户端代码中,使用具体的组件对象来声明一个基础接口或抽象类类型的变量,然后将装饰者对象赋值给该变量。由于装饰者对象也实现了基础接口或抽象类,所以可以通过该变量对被装饰对象进行操作
(3)案例实现
我们先来分析一下上面的角色担任:
- 奶茶:对应装饰者模式中的抽象构件,是具体构件和抽象装饰类的共同父类
- 原味奶茶和茉莉奶绿:对应装饰者模式中的具体构件,装饰器可给它增加额外的职责
- 配料:对应装饰者模式中的抽象装饰类,为抽象构件奶茶的子类,用于给具体构件增加职责
- 红糖珍珠和芝士奶盖:对应装置者模式中的具体装饰类,负责给构件添加新的职责
使用代码通过装饰者模式实现上述场景如下:
- 抽象构件(Component)角色:
public interface MilkTea {
String getDescription();
double getPrice();
}
- 具体构件(Concrete Component)角色:
public class OriginalMilkTea implements MilkTea {
private final String description = "原味奶茶";
private final double price = 10.0;
@Override
public String getDescription() {
return description;
}
@Override
public double getPrice() {
return price;
}
}
public class JasmineMilkTea implements MilkTea {
private final String description = "茉莉奶绿";
private final double price = 12.0;
@Override
public String getDescription() {
return description;
}
@Override
public double getPrice() {
return price;
}
}
- 抽象装饰(Decorator)角色:
public class CondimentDecorator implements MilkTea {
protected MilkTea milkTea;
public CondimentDecorator(MilkTea milkTea) {
this.milkTea = milkTea;
}
@Override
public String getDescription() {
return milkTea.getDescription();
}
@Override
public double getPrice() {
return milkTea.getPrice();
}
}
- 具体装饰(ConcreteDecorator)角色:
public class BrownSugarPearl extends CondimentDecorator {
private final String description = "红糖珍珠";
private final double price = 3.0;
public BrownSugarPearl(MilkTea milkTea) {
super(milkTea);
}
@Override
public String getDescription() {
return milkTea.getDescription() + ",加" + description;
}
@Override
public double getPrice() {
return milkTea.getPrice() + price;
}
}
public class CheeseCream extends CondimentDecorator {
private final String description = "芝士奶盖";
private final double price = 5.0;
public CheeseCream(MilkTea milkTea) {
super(milkTea);
}
@Override
public String getDescription() {
return milkTea.getDescription() + ",加" + description;
}
@Override
public double getPrice() {
return milkTea.getPrice() + price;
}
}
- 测试类:
public class Client {
public static void main(String[] args) {
// 原味奶茶不加任何配料
MilkTea originalMilkTea = new OriginalMilkTea();
System.out.println(originalMilkTea.getDescription() + "价格:" + originalMilkTea.getPrice());
// 原味奶茶加芝士奶盖
MilkTea originalMilkTeaWithCheese = new OriginalMilkTea();
originalMilkTeaWithCheese = new CheeseCream(originalMilkTeaWithCheese);
System.out.println(originalMilkTeaWithCheese.getDescription() + "价格:" + originalMilkTeaWithCheese.getPrice());
// 茉莉奶绿搭配红糖珍珠
MilkTea jasmineMilkTeaWithBrownSugar = new JasmineMilkTea();
jasmineMilkTeaWithBrownSugar = new BrownSugarPearl(jasmineMilkTeaWithBrownSugar);
System.out.println(jasmineMilkTeaWithBrownSugar.getDescription() + "价格:" + jasmineMilkTeaWithBrownSugar.getPrice());
// 茉莉奶绿满配
MilkTea jasmineMilkTeaAll = new JasmineMilkTea();
jasmineMilkTeaAll = new BrownSugarPearl(jasmineMilkTeaAll);
jasmineMilkTeaAll = new CheeseCream(jasmineMilkTeaAll);
System.out.println(jasmineMilkTeaAll.getDescription() + "价格:" + jasmineMilkTeaAll.getPrice());
}
}
运行结果如下:
3.4 对比
(1)装饰者模式和代理模式
联系:
-
装饰者模式和代理模式都委托被包装对象进行操作。在代理模式中,代理对象控制着实际对象的访问,并根据需要对其进行更改或增强。而在装饰者模式中,装饰器对象对被装饰的对象进行了装饰,以增强它的功能
-
装饰者模式和代理模式都可以在运行时动态地增强和修改对象的行为
区别:
-
装饰者模式侧重于在不改变已经存在的对象结构的情况下,动态地将责任附加到对象上,以增强其功能;而代理模式则是控制对对象的访问
-
装饰者模式所实现的功能一般都是增强性质的,而代理模式则是控制性质的
(2)装饰者模式和适配器模式
-
适配器模式旨在将一个接口转换成另一个接口,以便于不兼容的对象之间进行交互。而装饰者模式和代理模式并不涉及接口转换
-
适配器模式和装饰者模式都是结构型模式。适配器模式主要用于解决接口不兼容的问题,而装饰者模式则主要用于为对象增加新的功能
-
适配器模式和代理模式都能够控制对对象的访问,但是它们的目的不同。适配器模式关注接口的转换,代理模式关注控制对对象的访问
3.5 优缺点
优点:
- 动态扩展功能:装饰者模式可以在运行时动态地添加、删除和修改对象的功能,从而实现对对象的动态扩展,避免了使用继承带来的静态局限性
- 单一职责原则:装饰者模式将一个大类分为多个小类,每个小类只关注自己的功能实现,符合单一职责原则,使得代码更加清晰简洁
- 开放封闭原则:通过装饰者模式,可以在不改变原有代码的情况下,增强、扩展对象的功能,符合开放封闭原则
- 可组合性:装饰者模式中的装饰者可以任意组合,以增强对象的功能,形成不同的组合结果,具有很好的灵活性和可复用性
缺点:
- 多层嵌套:如果使用不当,装饰者模式会导致大量的嵌套和复杂度,使得代码难以维护和理解
- 具体组件与装饰者的耦合:装饰者模式需要每个具体装饰者都依赖于一个具体组件,这种依赖关系可能会导致系统中出现大量的具体类,增加了系统的复杂度
3.6 使用场景
装饰者模式主要用于在不改变原有对象的结构和功能的情况下,动态地增加对象的功能。以下是一些使用装饰者模式的常见使用场景:
- 动态地添加对象的职责:通过装饰者模式,可以在运行时动态地为一个对象添加新的职责,而不需要修改它的代码或继承它
- 多个小对象进行组合:使用装饰者模式可以将多个小对象组合成一个大对象,并且可以根据需要随意组合这些小对象,以形成不同的组合结果
- 需要扩展现有类的功能而又不能修改其源代码:在一些开源库或第三方库中,由于源代码无法修改,但是又需要对其功能进行扩展,此时装饰者模式可以非常方便地实现这一需求
- 给已有的对象添加新的行为,而且这些行为还能够互相组合:使用装饰者模式,可以很容易地给一个已有的对象添加新的行为,并且这些行为还能够互相组合,以形成更复杂的行为
- 避免继承带来的子类爆炸问题:通过装饰者模式,可以避免使用继承带来的子类爆炸问题,从而使得系统更加灵活、可扩展