这篇系列文章将按照以下结构逐一介绍不同种类的设计模式:
1. 创建型模式
2. 结构型模式
3. 行为型模式
- 行为型模式涉及类和对象之间的协作,以完成单个对象无法独立完成的任务,并分配职责。这部分将介绍十一种行为型模式:模板方法模式、策略模式、命令模式、责任链模式、状态模式、观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式和解释器模式。
通过这一系列文章,深入了解这些不同类型的设计模式,以及它们如何在软件开发中发挥关键作用。
文章目录
前言
在软件开发中,设计模式是一种被广泛接受的可复用解决方案,用于解决在软件设计中常见的问题。设计模式为开发人员提供了一种通用的指导,帮助他们设计和实施高质量、易维护、可扩展的软件系统。
本文将快速入门结构型模设计模式中的代理模式。
一、代理模式简介
代理模式是一种常见的设计模式,它允许一个对象(代理对象)充当另一个对象(真实对象)的接口,以控制对该对象的访问。代理模式通常用于需要在访问对象之前或之后执行一些额外操作的情况。
1.1 什么是代理模式?
代理模式是一种结构型设计模式,其目的是为其他对象提供一种代理,以控制对这些对象的访问。代理对象通常具有与真实对象相同的接口,客户端代码可以通过代理对象访问真实对象,而代理对象可以在必要时执行一些额外的操作。
1.2 为什么需要代理模式(优点)
代理模式有许多优点,其中包括:
- 控制访问:代理对象可以控制对真实对象的访问,可以限制某些客户端的权限或提供额外的安全性。
- 延迟加载:代理可以延迟加载真实对象,当客户端首次访问时,才创建和初始化真实对象,从而节省资源。
- 缓存:代理可以缓存真实对象的结果,以提高性能。
- 远程代理:代理模式还可用于处理远程对象的访问,使客户端可以透明地访问远程对象。
1.3 代理模式的不足
代理模式的缺点包括:
- 增加复杂性:引入代理对象会增加系统的复杂性,需要额外的类和代码来管理代理。
- 性能开销:在某些情况下,代理模式可能引入性能开销,特别是在远程代理或延迟加载的情况下。
1.4 代理模式的结构
代理模式的结构通常由以下几个要素组成:
抽象主题(Subject):定义了真实主题和代理对象的共同接口,客户端通过这个接口访问真实主题。
真实主题(Real Subject):实现了抽象主题接口,是代理模式中所代表的真正对象。客户端最终想要访问的对象就是真实主题。
代理(Proxy):也实现了抽象主题接口,它包含一个对真实主题的引用,并在必要时控制对真实主题的访问。代理可以承担一些额外的职责,如延迟加载、安全性检查、缓存、日志记录等。
在这个结构中,客户端通过抽象主题接口来访问真实主题,但实际上客户端可能会与代理对象交互。代理对象内部维护了对真实主题的引用,当客户端调用代理对象的方法时,代理对象可以在必要时将请求传递给真实主题,以完成实际的操作。代理对象可以根据需要在请求前后执行一些额外的逻辑。这种结构使代理模式非常有用,因为它允许我们对客户端隐藏真实主题的实现细节,并提供一种更加灵活和可控的访问方式。
1.5 代理模式的应用场景
代理模式适用于多种情况,包括:
- 虚拟代理(Virtual Proxy):用于延迟加载大型对象,以减小启动时间。
- 虚拟代理是一种延迟加载的代理,用于延迟创建和初始化真实对象,以节省资源。
- 虚拟代理通常在客户端首次访问真实对象时才创建和初始化真实对象。
- 虚拟代理常用于加载大型图片、视频等资源,以减小启动时间。
- 远程代理(Remote Proxy):用于访问远程对象,如网络服务。
- 远程代理用于在不同地址空间中代表对象,使客户端可以透明地访问远程对象,如通过网络。
- 远程代理处理对象的序列化、远程通信和数据传输等细节。
- 远程代理常用于分布式系统和远程服务调用。
- 保护代理(Protective Proxy):用于控制对对象的访问权限。
- 保护代理用于控制对对象的访问权限,以确保只有合法用户可以访问对象。
- 保护代理通常包括访问权限检查,以验证客户端是否有权访问对象。
- 保护代理常用于实现安全性和权限控制。
- 缓存代理(Cache Proxy):用于缓存对象的结果,以提高性能。
- 缓存代理用于缓存对象的结果,以提高性能。
- 缓存代理在首次请求对象时执行操作,并将结果缓存,后续请求可以从缓存中获取结果,而无需再次执行操作。
- 缓存代理适用于需要频繁计算或获取的操作。
- 智能引用(Smart Reference):用于在对象被引用时执行额外操作。
- 智能引用通常与引用计数技术结合使用。每当有新引用指向对象时,引用计数增加。
- 智能引用常用于资源管理,例如内存管理。它可以确保在没有引用时释放资源,防止内存泄漏。
- 智能引用可以用于检测循环引用,这是一种常见的资源泄漏问题。
- 智能引用还可以用于自动清理操作,例如自动关闭文件、数据库连接或网络连接。
二、代理模式分类
代理模式根据其实现方式可以分为静态代理和动态代理:
静态代理(Static Proxy):
- 静态代理是在代理类编译时就已经确定的代理关系。
- 代理类和真实类需要实现相同的接口或继承相同的父类,以便代理类可以代理真实类。
- 静态代理的代理类通常需要手动编写,以包装真实类的方法调用,并可以在方法前后添加额外的逻辑。
动态代理(Dynamic Proxy):
- 动态代理是在运行时根据接口或类生成代理对象,无需手动编写代理类。
- Java提供了动态代理的机制,主要通过
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
来实现。- 动态代理适用于需要在运行时代理不同类型对象的情况,例如AOP(面向切面编程)。
2.1 静态代理示例
在奶茶店场景中,假设我们要实现控制客户对奶茶的访问(不将实际的奶茶制作暴露给客户),并在访问前后执行一些额外的操作,比如记录订单时间。以下是一个简单的 Java 实现示例:
首先,定义奶茶接口 MilkTea
:
public interface MilkTea {
void makeMilkTea();
}
然后,创建真实奶茶类 RealMilkTea
实现 MilkTea
接口:
public class RealMilkTea implements MilkTea {
private String flavor;
public RealMilkTea(String flavor) {
this.flavor = flavor;
}
@Override
public void makeMilkTea() {
System.out.println("制作" + flavor + "奶茶");
}
}
接下来,创建代理奶茶类 ProxyMilkTea
,它包装了真实奶茶对象,并在制作奶茶前后记录时间:
public class ProxyMilkTea implements MilkTea {
private RealMilkTea realMilkTea;
public ProxyMilkTea(String flavor) {
this.realMilkTea = new RealMilkTea(flavor);
}
@Override
public void makeMilkTea() {
long startTime = System.currentTimeMillis();
realMilkTea.makeMilkTea();
long endTime = System.currentTimeMillis();
System.out.println("制作奶茶用时: " + (endTime - startTime) + " 毫秒");
}
}
最后,客户端可以使用代理奶茶来制作奶茶,而代理会记录制作时间:
public class Client {
public static void main(String[] args) {
MilkTea milkTea = new ProxyMilkTea("香草");
milkTea.makeMilkTea();
}
}
在这个示例中,ProxyMilkTea
是静态代理,它包装了真实奶茶对象,并在调用 makeMilkTea
方法前后执行额外的操作。这个代理模式允许我们控制奶茶制作的访问,同时可以添加记录时间等额外功能。
2.2 动态代理示例
在奶茶店场景中,使用动态代理模式可以更加灵活,因为它允许在运行时代理不同类型的奶茶对象。以下是一个简单的示例,演示如何使用动态代理来代理奶茶的制作:
首先,定义奶茶接口 MilkTea
:
public interface MilkTea {
void makeMilkTea();
}
然后,创建真实奶茶类 RealMilkTea
实现 MilkTea
接口:
public class RealMilkTea implements MilkTea {
private String flavor;
public RealMilkTea(String flavor) {
this.flavor = flavor;
}
@Override
public void makeMilkTea() {
System.out.println("制作" + flavor + "奶茶");
}
}
接下来,创建一个动态代理处理程序 ProxyMilkTea
,它会在制作奶茶前后记录时间:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyMilkTea implements InvocationHandler {
private Object target;
public ProxyMilkTea(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("makeMilkTea")) {
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
System.out.println("制作奶茶用时: " + (endTime - startTime) + " 毫秒");
return result;
} else {
return method.invoke(target, args);
}
}
}
现在,客户端可以使用动态代理来创建奶茶对象,并在制作奶茶时记录时间:
import java.lang.reflect.Proxy;
public class Client {
public static void main(String[] args) {
MilkTea realMilkTea = new RealMilkTea("草莓");
MilkTea proxyMilkTea = (MilkTea) Proxy.newProxyInstance(
MilkTea.class.getClassLoader(),
new Class[]{MilkTea.class},
new ProxyMilkTea(realMilkTea)
);
proxyMilkTea.makeMilkTea();
}
}
在这个示例中,我们使用动态代理创建了 MilkTea
接口的代理对象,然后在制作奶茶时记录了时间。这种动态代理方式可以用于代理不同类型的奶茶对象,而无需为每种奶茶编写具体的代理类。
三、静态代理和动态代理的比较
以下是静态代理和动态代理之间的比较:
特点 | 静态代理 | 动态代理 |
---|---|---|
代理对象的创建时间 | 编译时,早于运行时 | 运行时 |
代理类的手动编写 | 需要手动编写代理类 | 不需要手动编写代理类 |
接口或父类的实现 | 代理类和真实类需实现相同的接口或继承相同的父类 | 不需要实现相同接口或继承父类 |
代理对象类型 | 固定,每个代理类代理特定的真实类 | 动态生成代理对象,可以代理多个真实类 |
增加代理方法的成本 | 每次新增代理方法需要修改代理类的代码 | 可以在不改变代理类的情况下添加新方法 |
应用于不同类型的对象 | 通常用于代理特定类型的对象 | 适用于代理不同类型的对象 |
AOP(面向切面编程) | 静态代理通常用于手动实现AOP | 动态代理常用于AOP,更灵活 |
代码维护复杂性 | 随着代理方法的增加,代理类的代码可能变得复杂 | 代理类通常较简单 |
编译时错误检测 | 可以在编译时捕获代理类的错误 | 错误通常在运行时才会被检测到 |
总结
代理模式是一种常见的设计模式,用于控制对对象的访问并在访问前后执行额外操作。
静态代理和动态代理是代理模式的两种主要实现方式。
静态代理和动态代理都有各自的优点和适用场景。 静态代理适合在编译时已知代理关系和代理类不经常变化的情况下,但它通常需要手动编写代理类。
动态代理更适合需要在运行时代理不同类型对象或需要更灵活的AOP实现的情况,它不需要手动编写代理类,但代理对象类型可以在运行时动态生成。