一、代理模式
代理设计模式(Proxy Design Pattern)是一种结构型设计模式,它为其他对象提供了一个代理,以控制对这个对象的访问。 代理模式可以用于实现懒加载、安全访问控制、日志记录等功能。简单来说,代理模式 就是通过代理对象来控制对实际对象的访问,代理对象在客户端和目标对象之间起到了中介的作用。
在设计模式中,代理模式可以分为静态代理和动态代理。静态代理是指代理类在编译时就已经确定,而动态代理是指代理类在运行时动态生成。
1、静态代理
静态代理是在代码编译阶段就已经生成了代理类,代理类需要实现目标对象相同的接口。
优点:可以在不修改目标对象的前提下对目标对象进行增强。
缺点:需要为每个目标创建一个代理类,导致系统中类的数量增加,维护成本较高。
2、静态代理的使用场景
2.1 缓存代理
缓存代理是一种特殊类型的的代理模式,它可以为耗时的操作或者重复的请求提供缓存功能,从而提高程序的执行效率。缓存代理通常会在内部维护一个缓存的数据结构。如HashMap 或者 LinkHashMap,用来 存储已经处理过的请求及其结果。
以下是一个示例:
假设有一个数据查询接口,它从数据库或者其他数据源中检索数据,在没有缓存代理的情况下,每次查询都需要访问数据库,这可能会导致较高的资源消耗和延迟,通过引入缓存代理,我们可以将查询结果存储在内存中,从而避免重复查询数据库。
首先定义一个数据查询接口:
public interface DataQuery {
String query(String queryKey);
}
然后,实现一个真实的数据查询类:
public class DatabaseDataQuery implements DataQuery {
@Override
public String query(String queryKey) {
// 查询数据库并返回结果
return "Result from database: " + queryKey;
}
}
然后创建一个缓存代理类,它实现DataQuery接口,并在内部使用HashMap作为缓存:
public class CachingDataQueryProxy implements DataQuery {
private final DataQuery realDataQuery;
private final Map<String, String> cache;
public CachingDataQueryProxy(DataQuery realDataQuery) {
this.realDataQuery = new DatabaseDataQuery();
cache = new HashMap<>();
}
@Override
public String query(String queryKey) {
String result = cache.get(queryKey);
if (result == null) {
result = realDataQuery.query(queryKey);
cache.put(queryKey, result);
System.out.println("Result retrieved from database and added to cache.");
} else {
System.out.println("Result retrieved from cache.");
}
return result;
}
}
最后,我们在客户端就可以使用缓存代理:
public class Client {
public static void main(String[] args) {
DataQuery realDataQuery = new DatabaseDataQuery();
DataQuery cachingDataQueryProxy = new
CachingDataQueryProxy(realDataQuery);
String queryKey = "example_key";
// 第一次查询,从数据库中获取数据并将其缓存
System.out.println(cachingDataQueryProxy.query(queryKey));
// 第二次查询相同的数据,从缓存中获取
System.out.println(cachingDataQueryProxy.query(queryKey));
}
}
其实也就是说我们单独实现了一个方法,我们在这个方法中取调用原本的查询操作,也就是在这个新的方法中完成了过滤的操作。
2.2 安全代理
(Security Proxy)是一种代理模式用的应用,它用于控制对真实主题对象的访问。通过安全代理,可以实现访问控制、权限控制等安全相关功能。
以下是一个简单的安全代理示例:
假设我们有一个敏感数据查询接口,只有具有特定权限的用户才能访问:
首先,我们定义一个数据查询接口:
public interface SensitiveDataQuery {
String queryData(String userId);
}
接着实现一个真的敏感数据查询类:
public class SensitiveDataQueryImpl implements SensitiveDataQuery {
@Override
public String queryData(String userId) {
// 查询敏感数据并返回结果
return "Sensitive data for user: " + userId;
}
}
然后,我们创建一个安全代理类,它实现了 SensitiveDataQuery接口,并在内部进行权限验证:
public class SecurityProxy implements SensitiveDataQuery {
private final SensitiveDataQuery sensitiveDataQuery;
private final UserAuthenticator userAuthenticator;
public SecurityProxy(SensitiveDataQuery sensitiveDataQuery,
UserAuthenticator userAuthenticator) {
this.sensitiveDataQuery = sensitiveDataQuery;
this.userAuthenticator = userAuthenticator;
}
@Override
public String queryData(String userId) {
if (userAuthenticator.hasPermission(userId)) {
return sensitiveDataQuery.queryData(userId);
} else {
return "Access Denied: Insufficient permission for user" + userId;
}
}
}
我们使用一个UserAuthenticator类来模拟用户权限验证:
public class UserAuthenticator {
private final List<String> authorizedUserIds;
public UserAuthenticator() {
// 模拟从数据库或配置文件中获取已授权的用户列表
authorizedUserIds = Arrays.asList("user1", "user2", "user3");
}
public boolean hasPermission(String userId) {
return authorizedUserIds.contains(userId);
}
}
在客户端中调用:
public class Client {
public static void main(String[] args) {
SensitiveDataQuery sensitiveDataQuery = new SensitiveDataQueryImpl();
UserAuthenticator userAuthenticator = new UserAuthenticator();
SensitiveDataQuery securityProxy = new SecurityProxy(sensitiveDataQuery,
userAuthenticator);
String userId1 = "user1";
String userId2 = "user4";
// 用户1具有访问权限
System.out.println(securityProxy.queryData(userId1));
// 用户4没有访问权限
System.out.println(securityProxy.queryData(userId2));
}
}
2.3 虚拟代理
(Virtual Proxy)是一种代理模式,用于在需要时延迟创建耗时或资源密集型对象。虚拟代理在初始访问时才创建实际对象,之后将直接使用该对象。这可以避免在实际对象尚未使用的情况下就创建它,从而节省资源。
例如:我们有一个大型图片类,他从网络加载图像。由于图像可能非常大。我们希望在需要显示时才加载他,为了实现这一点,我们可以创建一个虚拟代理来代表大型图片类,首先定义一个图片接口:
public interface Image {
void display();
}
然后实现了一个大型图片类,他从网络加载图像并实现display() 方法:
public class LargeImage implements Image {
private final String imageUrl;
public LargeImage(String imageUrl) {
this.imageUrl = imageUrl;
loadImageFromNetwork();
}
private void loadImageFromNetwork() {
System.out.println("Loading image from network: " + imageUrl);
// 真实的图像加载逻辑...
}
@Override
public void display() {
System.out.println("Displaying image: " + imageUrl);
}
}
然后创建一个虚拟代理类,它实现了Image接口,并在内部使用LargeImage:
public class VirtualImageProxy implements Image {
private final String imageUrl;
private LargeImage largeImage;
public VirtualImageProxy(String imageUrl) {
this.imageUrl = imageUrl;
}
@Override
public void display() {
if (largeImage == null) {
largeImage = new LargeImage(imageUrl);
}
largeImage.display();
}
}
最后我们在客户端中使用虚拟代理:
public class Client {
public static void main(String[] args) {
Image virtualImageProxy = new
VirtualImageProxy("https://example.com/large-image.jpg");
System.out.println("Image will not be loaded until it is displayed.");
// 调用 display() 方法时,才会创建并加载大型图片
virtualImageProxy.display();
}
}
这个例子就是通过虚拟代理实现懒加载,以减少资源消耗和提高程序性能,当实际对象的创建和初始化非常耗时或占用大量资源时,虚拟代理是一个很好的选择。
2.4 远程代理
(Remote Proxy)是一种代理模式,用于访问位于不用的地址空间的对象。远程代理可以为本地对象提供与远程对象相同的接口,使得客户端可以透明的访问远程对象。通常,远程代理需要处理网络通信、序列化和反序列化等细节。以后做rpc时也会使用。
简单的示例:首先定义一个服务接口:
public interface RemoteService {
String fetchData(String dataId);
}
然后实现一个远程服务类,在服务端运行并实现fetchData()方法:
public class RemoteServiceImpl implements RemoteService {
@Override
public String fetchData(String dataId) {
// 实际操作,例如从数据库获取数据
return "Data from remote service: " + dataId;
}
}
· 接下来,我们创建了一个远程代理类,它实现了RemoteService接口,并在内部处理网络通信等细节:
public class RemoteServiceProxy implements RemoteService {
private final String remoteServiceUrl;
private RemoteService remoteService;
public RemoteServiceProxy(String remoteServiceUrl) {
this.remoteServiceUrl = remoteServiceUrl;
this.remoteService = new RemoteService();
}
@Override
public String fetchData(String dataId) {
// 网络通信、序列化和反序列化等逻辑
System.out.println("Connecting to remote service at: " + remoteServiceUrl);
// 假设我们已经获取到远程服务的数据
String result = remoteService.fetchData(dataId);
System.out.println("Received data from remote service.");
return result;
}
}
2.5 静态代理步骤总结
通过前面几个案例,大致了解了静态代理的使用方式,其大致流程如下:
1.创建一个接口,定义代理类和被代理类共同实现的方法。
2.创建被代理类,实现这个接口,并且在其中定义实现方法。
3.创建代理类,也要实现这个接口,同时在其中定义一个被代理类的对象作为成员变量。
4.在代理类中实现接口中的方法,方法中调用被代理类中的对应方法。
5.通过创建代理对象,并调用其方法,方法增强。
这样,被代理类的方法就会被代理类所覆盖,实现了对被代理类的增强或修改。
在静态代理中,也可以使用继承来实现代理,具体步骤如下:
1.创建被代理类,定于需要被代理的方法。
2.创建代理类,继承被代理类,重写被代理类中的方法,对方法进行增强。
3.再重写的方法中添加代理逻辑,例如在调用被代理类中的方法前后添加日志记录、安全检查等功能。
4.在使用代理类时,创建代理对象,调用重写方法。
这样,被代理类的方法就会被代理类所覆盖,实现了对被代理类的增强或修改。使用继承来实现代理类的好处就是简单易懂,不需要创建接口,同时继承可以继承被代理类的属性和方法,可以更方便的访问类中的成员。但是这个方法也有一些缺点,类如代理类与被代理类的耦合度较高,不够灵活。
3、动态代理
java中动态代理的实现方式主要有两种:基于JDK的动态代理和基于CGLB的动态代理。
动态代理是指在程序运行时动态生成代理类,无需手动编写代理类,大大降低了代码的复杂度,动态代理一般使用Java提供的反射机制实现,可以对任意实现了接口的类进行代理。动态代理的优点是灵活性高,可以根据需要动态生成代理类,缺点是性能相对较低,由于使用反射机制,在运行时会产生额外的开销。
静态代理 和 动态代理都是代理模式的实现方式,其主要区别在于代理类的生成时机和方式。静态代理需要手动编写代理类,适用于代理类数量较少,不需要频繁修改的场景。而动态代理不需要手动编写代理类,可以动态的生成代理类,适用于代理类数量较多、需要频繁修改的场景。
4.1 基于JDK的动态代理实现步骤
基于JDK的动态代理需要使用 java.lang.reflect.Proxy 类 和 Java.lang.reflect.InvocationHandler 接口。我们依旧使用上述的缓存代理的案例来实现,具体步骤如下:
1)定义一个接口,声明需要代理的方法:
public interface DataQuery {
String query(String queryKey);
String queryAll(String queryKey);
}
2)创建一个被代理类,实现这个接口,并在其中定义实现方法:
public class DatabaseDataQuery implements DataQuery {
@Override
public String query(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "result";
}
@Override
public String queryAll(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "all result";
}
}
3)创建一个代理类,实现了InvocationHandler接口,并在其中定义一个被代理类的对象作为属性。
public class CacheInvocationHandler implements InvocationHandler {
private HashMap<String,String> cache = new LinkedHashMap<>(256);
private DataQuery databaseDataQuery;
public CacheInvocationHandler(DatabaseDataQuery databaseDataQuery) {
this.databaseDataQuery = databaseDataQuery;
}
public CacheInvocationHandler() {
this.databaseDataQuery = new DatabaseDataQuery();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1、判断是哪一个方法
String result = null;
if("query".equals(method.getName())){
// 2、查询缓存,命中直接返回
result = cache.get(args[0].toString());
if(result != null){
System.out.println("数据从缓存重获取。");
return result;
}
// 3、未命中,查数据库(需要代理实例)
result = (String) method.invoke(databaseDataQuery, args);
// 4、如果查询到了,进行呢缓存
cache.put(args[0].toString(),result);
return result;
}
// 当其他的方法被调用,不希望被干预,直接调用原生的方法
return method.invoke(databaseDataQuery,args);
}
}
在代理类中,我们实现了InvocationHandler接口,并在其中定义了一个被代理类的对象作为属性。在invoke方法中,我们可以对被代理对象的方法进行增强,并在方法调用前后输出日志。
4)使用代理类,创建被代理类的对象和代理类的对象,并使用Proxy.newProxyInstance方法生成代理对象。
public class Main {
public static void main(String[] args) {
// jdk提供的代理实现,主要是使用Proxy类来完成
// 1、classLoader:被代理类的类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 2、代理类需要实现的接口数组
Class[] interfaces = new Class[]{DataQuery.class};
// 3、InvocationHandler
InvocationHandler invocationHandler = new CacheInvocationHandler();
DataQuery dataQuery = (DataQuery)Proxy.newProxyInstance(
classLoader, interfaces, invocationHandler
);
// 事实上调用query方法的使用,他是调用了invoke
String result = dataQuery.query("key1");
System.out.println(result);
System.out.println("--------------------");
result = dataQuery.query("key1");
System.out.println(result);
System.out.println("--------------------");
result = dataQuery.query("key2");
System.out.println(result);
System.out.println("++++++++++++++++++++++++++++++++++++");
// 事实上调用queryAll方法的使用,他是调用了invoke
result = dataQuery.queryAll("key1");
System.out.println(result);
System.out.println("--------------------");
result = dataQuery.queryAll("key1");
System.out.println(result);
System.out.println("--------------------");
result = dataQuery.queryAll("key2");
System.out.println(result);
System.out.println("--------------------");
}
}
再看一下生成日志的例子:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface UserService {
void saveUser(String username);
}
class UserServiceImpl implements UserService {
public void saveUser(String username) {
System.out.println("Saving user: " + username);
}
}
class LogProxy implements InvocationHandler {
private Object target;
public LogProxy(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After method: " + method.getName());
return result;
}
}
public class DynamicProxyExample {
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new LogProxy(userService)
);
proxy.saveUser("Alice");
}
}
其实主要的步骤就是要编写我们的动态代理类,并在invoke 方法中书写自己需要添加的逻辑,Proxy.newProxyInstance 方法生成代理对象,这一步很多人看不懂,其实不需要担心,因为这个其实就是一个固定的api知道怎么调用即可!
4.2 基于CGLIB的动态代理实现步骤
基于CGLB的动态代理需要使用 net.sf.cglib.proxy.Enhancer类 和 net.sf.cglib.proxy.MethodInterceptor 接口 ,具体步骤如下:
1)创建一个被代理类,定义需要被代理的方法:
public class DatabaseDataQuery {
public String query(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "result";
}
public String queryAll(String queryKey) {
// 他会使用数据源从数据库查询数据很慢
System.out.println("正在从数据库查询数据");
return "all result";
}
}
2)创建一个方法拦截器类,实现MethodInterceptor接口,并在其中定义一个被代理类的对象作为属性。
public class CacheMethodInterceptor implements MethodInterceptor {
private HashMap<String,String> cache = new LinkedHashMap<>(256);
private DatabaseDataQuery databaseDataQuery;
public CacheMethodInterceptor() {
this.databaseDataQuery = new DatabaseDataQuery();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
// 1、判断是哪一个方法
String result = null;
if("query".equals(method.getName())){
// 2、查询缓存,命中直接返回
result = cache.get(args[0].toString());
if(result != null){
System.out.println("数据从缓存重获取。");
return result;
}
// 3、未命中,查数据库(需要代理实例)
result = (String) method.invoke(databaseDataQuery, args);
// 4、如果查询到了,进行呢缓存
cache.put(args[0].toString(),result);
return result;
}
return method.invoke(databaseDataQuery,args);
}
}
在这个代理类中,我们实现了MethodInterceptor 接口,并在其中定义了一个被代理类的对象作为属性,在intercept方法中,我们可以对被代理对象的方法进行增强,并在方法调用前后输出日志。
3)在使用代理类时,创建被代理类的对象和代理类的对象,并使用Enhancer.create方法生成代理对象。
public class Main {
public static void main(String[] args) {
// cglib通过Enhancer
Enhancer enhancer = new Enhancer();
// 设置他的父类
enhancer.setSuperclass(DatabaseDataQuery.class);
// 设置一个方法拦截器,用来拦截方法
enhancer.setCallback(new CacheMethodInterceptor());
// 创建代理类
DatabaseDataQuery databaseDataQuery =
(DatabaseDataQuery)enhancer.create();
databaseDataQuery.query("key1");
databaseDataQuery.query("key1");
databaseDataQuery.query("key2");
}
}
实现打印日志的例子:
class UserServiceImpl {
public void saveUser(String username) {
System.out.println("Saving user: " + username);
}
}
class LogInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method: " + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method: " + method.getName());
return result;
}
}
public class CglibProxyExample {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new LogInterceptor());
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
proxy.saveUser("Bob");
}
}
在这个示例中,我们使用 Enhancer.create 方法生成代理对象,并将代理对象转成RealSubject类型,以便调用request方法,在代理对象调用request方法时,会调用DynamicProxy类中的intercept方法,实现对被代理对象的增强。
在实际应用中,基于CGLIB的动态代理可以代理任意类,但是生成的代理类比较重量级。如果被代理类是一个接口,建议使用基于JDK的动态代理来实现,这也是spring的做法;如果被代理类没有实现接口或者需要代理的方法时final方法,建议使用基于CGLIB的动态代理来实现。
4.3 spring aop 与 动态代理之间的关系
spring aop 即面向切面编程,他对aop进行了封装,使用面向对象的思想来实现,所以aop的底层使用动态代理来实现的。
4、动态代理的应用场景
4.1 日志记录:使用动态代理可以在方法调用前后自动添加日志记录,从而跟踪方法的执行过程。这样可以方便的监控系统运行情况,诊断问题,而无需修改实际类的代码。
4.2 性能监控:动态代理可用于测量方法执行的时间,以评估性能。在方法调用前后记录时间戳,然后计算时间差,就可以得到方法执行所需要的时间。
4.3 事务管理:在数据库操作中,动态代理可用于自动管理事务。在方法调用前开始一个事务,在方法成功执行后提交事务,如果发生异常,则回滚事务,这样可以确保数据的一致性和完整性。
4.4 权限验证:使用动态代理可以在方法调用前进行权限验证,确保具有适当权限的用户才能访问受保护的资源。这可以提高系统的安全性。
4.5缓存:动态代理可用于实现方法结果的的缓存。在方法调用前检查缓存,如果缓存中有结果,则直接返回,否则执行方法并将结果存入缓存。这样可以提高程序的执行效率。
4.6 负载均衡与故障转移:在分布式系统中,动态代理可以用于实现负载均衡和故障转移。代理对象根据某种策(入轮询、随机等)选择一个可用的服务实例。并将请求转发给它。如果服务实例发生故障,代理对象可以自动选择另一个可用的实例。
4.7 API速率限制:使用动态代理,可以在方法调用前检查aoi请求速率是否超过预设的限制。如果超过限制,可以拒绝请求获奖请求延迟一段时间后再执行。
4.8 数据验证:在方法调用前,动态代理可以用于验证传入的参数是否符合预期的规则和约束。这有助于确保数据的有效性和一致性。
4.9 重试机制:当方法调用失败时,比如网络问题等等,动态代理可以实现自动重试的机制,代理对象可以在一定的时间间隔内尝试重新执行方法,直到成功达到最大重试次数。
4.10 懒加载与资源管理:动态代理可以用于实现资源的懒加载和管理。例如,代理对象可以在第一次访问资源时才创建和初始化它。此外,代理对象还可以在资源不再需要时自动释放它,以减少内存占用和提高性能。
4.11 跨语言和跨平台调用:动态代理可以实现跨语言和跨平台的对象调用。例如,一个java客户端可以使用动态代理调用一个基于python的服务,在这种情况下,代理对象会负责跨语言通信的细节,如序列化、反序列化和网络传输。
4.12 AOP(面向切面编程):动态代理是实现AOP的一种方式。AOP允许在程序运行时动态的插入和修改横切关注点(如日志记录、性能监控等),而无需修改实际代码。动态代理可以轻松地实现AOP,以提高代码的可维护性和可重用性。
具体例子省略,可自行查询相关例子。
二、装饰器模式
1、实现原理
装饰器设计模式是一种结构型设计模式,它允许动态的为对象添加新的行为,它通过创建一个包装器来实现。即将对象放入一个装饰器中,再将装饰器类放入另一个装饰器类中,以此类推,形成一个包装链。这样我们可以在不改变原有对象的情况下 ,动态的添加新的行为或修改原有行为。
实现步如下:
1)定义一个 接口或者抽象类,作为被装饰对象的基类
例如创建一个component的接口,包含一个名为operation 的抽象方法,用于定义被装饰对象的基本行为。
public interface Component {
void operation();
}
2)定义一个具体的被装饰对象,实现基类中的方法
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("ConcreteComponent is doing something...");
}
}
3)定义一个抽象装饰类,继承基类,并将被装饰对象作为属性
public abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
4)定义具体的装饰器类,继承抽象装饰器类,并实现增强逻辑
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
System.out.println("ConcreteDecoratorA is adding new behavior...");
}
}
5)使用装饰器增强被装饰对象
public class Main {
public static void main(String[] args) {
Component component = new ConcreteComponent();
component = new ConcreteDecoratorA(component);
component.operation();
}
}
上面的过程跟静态代理有一些相似之处,但他们之间还是有区别的:
代理模式的目的是为了控制对象的访问,他在对象的外部提供了一个代理对象来控制对原始对象的访问。代理对象和原始对象通常实现同一个接口或继承同一个类,以保证二者可以互相替代。
装饰器模式的目的是为了动态地增强对象的功能,他在对象地内部通过一种包装起的方式来实现。装饰器模式中,装饰器类和被装饰对象通常实现同一个接口或继承同一个类,以保证二者可以互相替代。装饰器模式也称为包装器模式。
虽然这两个模式它们实现方式上都是通过继承统一地接口,然后在代理类和装饰者类中通过构造函数引入被代理类和被装饰者类,但是装饰着模式和静态代理模式在设计理念上有着本质的区别。装饰者模式注重于动态地为对象添加新功能,而静态代理模式则注重于对对象访问地控制。
2、使用场景
在Java中,装饰器模式的应用非常广泛,特别是在IO操作中。Java的IO类库就是使用装饰器模式来实现不同的数据流之间的转换和增强的。
2.1 从IO库的设计理解装饰器
例如,我们要打开文件 test.txt 从中读取数据,其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率,具体代码如下:
InputStream in = new FileInputStream("D:/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
初看上面的代码可能会觉得麻烦,需要先创建一个FileInputStream对象,然后传递给BufferedInputStream对象来使用。之前会觉得为什么不设计一个 继承FileInputStream对象并且支持缓存的BufferedFileStream类呢?
其实如果我们使用继承的方式来实现的话,就需要继续派生出DataFileInputStream、DataPipedInputStream 等类。如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多类,这还只是附加了两个增强功能,如果还需要附加更多功能,那就会导致爆炸,类继承结构变得无比复杂,代码即不好扩展,也不好维护。这就是之前再讲设计原则的时候,不推荐使用继承的原因。
所以使用装饰器模式来设计IO类会比较好,时间上IO类也确实是这么设计的,但是并不是简单的套用了装饰器的设计模式。
使用装饰器模式第一个比较特殊的地方:装饰器类和原始类继承同样的父类,这样我们可以对原始类 嵌套 多个装饰器类。
第二个比较特殊的地方:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。
2.2 mybatis的缓存设计
mybatis中一级缓存 二级缓存,都是使用装饰器模式实现的,具体省略,可以自行查询以下源码。
3、总结
装饰器模式主要解决继承关系过于复杂的问题,通常是通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
对于大多数 添加缓存 的业务场景,核心目的主要就是相增强对象的功能(即增加缓存功能),而不是控制对象的访问,所以装饰器模式可能会更合适。但是比如相对持久层增加一个本地缓存,代理设计模式也是很好的选择。
三、桥接模式
桥接模式的代码实现非常简单,但是理解起来稍微有点难度,并且应用场景也比较局限,相对于代理模式来说,桥接模式在实际的项目中并没有那么常用,只需要简单了解,见到能认识就行。
1、桥接模式示例
JDBC驱动是桥接模式的经典应用,复习一下JDBC驱动来查询数据库方法,具体代码如下所示:
// 1.数据库连接的4个基本要素:
String url = "jdbc:mysql://127.0.0.1:3306/ydlclass?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
String user = "root";
String password = "root";
// 8.0之后名字改了 com.mysql.cj.jdbc.Driver
String driverName = "com.mysql.cj.jdbc.Driver";
// 2.实例化Driver,可省略
// 3.注册驱动,可省略
// 4.获取连接
conn = DriverManager.getConnection(url, user, password);
如果要把mysql数据库换成Oracle数据库,只需要把驱动com.mysql.cj.jdbc.Driver 换成 oracle.jdbc.driver.OracleDriver,同时将url进行替换就可以了。
在工作中上述配置一般在配饰文件中,一般直接修改配置文件即可。
不管是改代码还是改配置,在项目中,从一个数据库切换到另一个数据库,都只需要改动很少的代码。或者完全不需要改动代码。这个切换模式的设计要看 com.mysql.cj.jdbc.driver这个类的相关代码。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
* @throws SQLException if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
结合 com.mysql.jdbc.Driver 的代码实现,可以发现当执行 Class.forName("com.mysql.jdbc.Driver")这条语句的时候,实际上做了两件事情。
第一件事情是要求JVM查找并加载指定的Driver类。
第二件事情是执行该类的静态代码,也就是将mysql Driver 注册到DriverManager类中。
当我们把具体的Driver实现类(比如com.mysql.cj.jdbc.Driver)注册到DriverManager之后,后续所有对JDBC接口的调用,都会委派到对具体的Driver实现类来执行。而Driver实现类都实现了相同的接口(java.sql.Drvier),这也是可以灵活切换Driver的原因。
以下是具体代码:
public class DriverManager {
// List of registered JDBC drivers
private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static void registerDriver(java.sql.Driver driver,
DriverAction da) throws SQLException {
/* Register the driver if it has not already been added to our list */
if (driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
callerCL = Thread.currentThread().getContextClassLoader();
}
if (url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
ensureDriversInitialized();
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for (DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.driver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
}
桥接模式的定义是“将抽象和实现解耦,让它们独立变化”。在jdbc这个例子中,JDBC本身相当于抽象,注意这里所说的抽象,指的并非是 抽象类 或 接口,而是跟具体的数据库无关的,被抽象出来的一套类库。
具体的Driver(比如,com.mysql.cj.jdbc.Driver)就相当于 实现。注意 ,这里所说的实现,也并非指 接口的实现类 。而是跟具体数据库相关的一套 类库。JDBC 跟 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC的所有逻辑操作,最终都委托给Driver来执行。
这就是JDBC使用桥接模式的一个例子。通过将抽象的JDBC接口与具体的数据库厂商的实现相分离,我们可以实现对抽象和实现的独立扩展,这种设计模式使得在不修改客户端代码的情况下更换数据库驱动程序成为可能。从而提高了代码的可维护性和可扩展性。
2、桥接模式原理解析
桥接模式(Bridge Pattern)是一种结构型设计模式,用于将抽象与器实现相分离,以便两者可以独立地进行变化。它通过将抽象的类与实现的类分离,实现了抽象和实现的解耦。
桥接模式的核心概念:
1)抽象化:在抽象类中定义抽象业务的接口和一个对实现层次结构的引用。抽象化的主要目的是隐藏实现的细节,以便可以在不影响客户端的情况下更改实现。
2)实现化:这是一个接口,定了实现抽象化的方法,不同的具体实现类可以有不同的实现方式。
3)扩展抽象化:这是抽象化的一个具体实现,它定义抽象业务的具体操作。
4)具体实现化:实现化接口的具体实现类,这些类为抽象业务提供具体的实现
优点:
1)抽线与实现分离,可以独立的进行变化
2)提高了可扩展性
3)符合单一职责原则,抽象部分专注于抽象化,实现部分专注于具体实现。
缺点:
1)增加了系统复杂性
2.1 实现步骤
首先定义具体实现化类的相关api,这里我们可以理解为具体的mysql的实现:
// 可以当做具体的jdbc实现
interface Implementor {
void operationImpl();
}
// 具体实现化类A,具体实现可以多样化,独立变化
class ConcreteImplementorA implements Implementor {
@Override
public void operationImpl() {
System.out.println("具体实现A");
}
}
// 具体实现化类B
class ConcreteImplementorB implements Implementor {
@Override
public void operationImpl() {
System.out.println("具体实现B");
}
}
接下来编写抽象化类的相关内容,可以理解为jdbc提供的api接口:
abstract class Abstraction {
protected Implementor implementor;
// 抽象依赖实现的接口,而不依赖实现本身
public Abstraction(Implementor implementor) {
this.implementor = implementor;
}
abstract void operation();
}
// 抽象化类
class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor implementor) {
super(implementor);
}
// 请记住这里的方法名字不同,他们不需要实现相同的接口,他可以独立变化
@Override
void operation() {
implementor.operationImpl();
}
}
这里展示桥接的过程:
public class BridgePatternDemo {
public static void main(String[] args) {
Implementor implementorA = new ConcreteImplementorA();
Abstraction abstractionA = new RefinedAbstraction(implementorA);
abstractionA.operation();
Implementor implementorB = new ConcreteImplementorB();
Abstraction abstractionB = new RefinedAbstraction(implementorB);
abstractionB.operation();
}
}
这个例子展示了桥接模式的核心概念,即将抽象与实现分离,这种分离使得我们可以独立的改变抽象类和实现类,提高了代码的可扩展性和可维护性。同时,这种设计也符合了单一职责原则,使得抽象部分和实现部分各自关注自己的核心职责。
3、桥接模式的应用举例
3.1 消息通知
来看一个消息通知系统的例子,这系统需要支持多种通知方式(例如邮件、短信、即时消息等)以及多种通知紧急程度(普通、紧急、非常紧急等)。
我们可以使用桥接模式将通知方式和通知紧急程度分离,使得它们可以独立的进行变化和扩展。
// 通知方式接口(实现化角色)
interface MessageSender {
void send(String message);
}
// 邮件通知实现类
class EmailSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("发送邮件通知: " + message);
}
}
// 短信通知实现类
class SmsSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("发送短信通知: " + message);
}
}
// 即时消息通知实现类
class InstantMessageSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("发送即时消息通知: " + message);
}
}
// 抽象通知类(抽象化角色)
abstract class Notification {
protected MessageSender messageSender;
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}
abstract void notify(String message);
}
// 普通通知子类(扩展抽象化角色)
class NormalNotification extends Notification {
public NormalNotification(MessageSender messageSender) {
super(messageSender);
}
@Override
void notify(String message) {
messageSender.send("普通:" + message);
}
}
// 紧急通知子类(扩展抽象化角色)
class UrgentNotification extends Notification {
public UrgentNotification(MessageSender messageSender) {
super(messageSender);
}
@Override
void notify(String message) {
messageSender.send("紧急:" + message);
}
}
// 非常紧急通知子类(扩展抽象化角色)
class CriticalNotification extends Notification {
public CriticalNotification(MessageSender messageSender) {
super(messageSender);
}
@Override
void notify(String message) {
messageSender.send("非常紧急:" + message);
}
}
public class BridgePatternExample {
public static void main(String[] args) {
MessageSender emailSender = new EmailSender();
MessageSender smsSender = new SmsSender();
MessageSender instantMessageSender = new InstantMessageSender();
Notification normalEmailNotification = new
NormalNotification(emailSender);
normalEmailNotification.notify("有一个新的任务待处理。");
Notification urgentSmsNotification = new UrgentNotification(smsSender);
urgentSmsNotification.notify("系统出现故障,请尽快处理!");
Notification criticalInstantMessageNotification = new
CriticalNotification(instantMessageSender);
criticalInstantMessageNotification.notify("系统崩溃,请立即处理!");
}
}
这个示例中,我们使用桥接模式将通知方式(MessageSender接口以及其实现类)和通知紧急程度(Notification类及其子类)分离,这使得我们可以独立地添加更多的通知方法和通知紧急程度,而不会导致类的数量爆炸性增长。
以下是如何运行这个示例的步骤:
1)我们定义了一个MessageSender接口,用于表示通知方式,然后,我们创建了几个实现了MewssageSender接口的具体实现类:EmailSender、SmsSender和InstantMessageSender,它们分别表示通过邮件、短信和及时消息发送通知。
2)然后,定义了一个抽象类Notification,它持有一个MessageSender对象,Notification类有一个抽象方法notify,用于发送通知。然后,我们创建了几个扩展Notification类的子类:NormalNotification、UrgentNotification 和 CriticalNotification ,它们分别表示普通、紧急 和 非常紧急的通知。
3)在BridgePatternExample的main方法中,我们创建了 EmailSender 、 SmsSender 和 InstantMessageSender 对象,并将它们与不同紧急程度的通知对象组合,然后,我们调用这些通知对象的norify方法来发送通知。
通过使用桥接模式,我们可以轻松的为消息通知系统添加新的通知方式和通知紧急程度,而无需修改现有的类结构,这种设计增强了代码的可扩展性和可维护性。
3.2 支付方式
看一电商项目中的例子,在这个例子中,需要处理多种支付方式(如信用卡、PayPal、支付宝等)和多种折扣策略(如VIP折扣,新用户折扣、优惠卷等)。
interface PaymentMethod {
void pay(double amount);
}
// 信用卡支付实现类
class CreditCardPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("使用信用卡支付: " + amount);
}
}
// PayPal支付实现类
class PayPalPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("使用PayPal支付: " + amount);
}
}
// 支付宝支付实现类
class AlipayPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("使用支付宝支付: " + amount);
}
}
// 折扣策略接口(抽象化角色)
abstract class DiscountStrategy {
protected PaymentMethod paymentMethod;
public DiscountStrategy(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
abstract double getDiscountedAmount(double originalAmount);
public void payWithDiscount(double originalAmount) {
double discountedAmount = getDiscountedAmount(originalAmount);
paymentMethod.pay(discountedAmount);
}
}
// VIP折扣策略子类(扩展抽象化角色)
class VipDiscountStrategy extends DiscountStrategy {
public VipDiscountStrategy(PaymentMethod paymentMethod) {
super(paymentMethod);
}
@Override
double getDiscountedAmount(double originalAmount) {
return originalAmount * 0.9; // VIP用户享有9折优惠
}
}
// 新用户折扣策略子类(扩展抽象化角色)
class NewUserDiscountStrategy extends DiscountStrategy {
public NewUserDiscountStrategy(PaymentMethod paymentMethod) {
super(paymentMethod);
}
@Override
double getDiscountedAmount(double originalAmount) {
return originalAmount * 0.95; // 新用户享有95折优惠
}
}
public class ECommerceExample {
public static void main(String[] args) {
PaymentMethod creditCardPayment = new CreditCardPayment();
PaymentMethod payPalPayment = new PayPalPayment();
PaymentMethod alipayPayment = new AlipayPayment();
DiscountStrategy vipCreditCardStrategy = new
VipDiscountStrategy(creditCardPayment);
vipCreditCardStrategy.payWithDiscount(100);
DiscountStrategy newUserPayPalStrategy = new
NewUserDiscountStrategy(payPalPayment);
newUserPayPalStrategy.payWithDiscount(100);
}
}
四、适配器模式
1、原理与实现
适配器设计模式(Adapter Design Pattern)是一种结构设计模式,用于解决两个不兼容接口之间的问题。适配器允许将一个类的接口转换为客户端期望的另一个接口,使得原本由于接口不兼容而不能在一起工作的类可以一起工作。
在适配器模式中,主要包含以下四个角色:
1)目标接口(Target):这是客户端期望使用地接口,它定义了特定领域地操作和方法。
2)需要适配的类(Adaptee):这是一个已经存在地类,它具有客户端需要的功能,但其接口与目标接口不兼容,适配器的目标是使这个类的功能能够通过目标接口使用。
3)适配器(Adapter):这是适配器模式的核心角色,它实现了目标接口并持有需要适配的类的一个实例。适配器通过封装Adaptee的功能,使其能够满足Target接口的要求。
4)客户端(Client):这是使用目标接口的类。客户端可以间接的使用需要适配的类的功能。
适配器模式的主要目的是不修改现有代码的情况下,是不兼容的接口能协同工作,通过引入适配器角色,客户端可以使用目标接口与需要适配的类进行通信,从而实现解耦和扩展性。
适配器模式有两种实现方式:类适配器和对象适配器
1.1 类适配器
类适配器使用继承来实现适配器功能。适配器类继承了原有的类(Adaptee)并实现了目标接口(Target)。
// 目标接口
interface Target {
void request();
}
// 需要适配的类(Adaptee)
class Adaptee {
void specificRequest() {
System.out.println("Adaptee's specific request");
}
}
// 类适配器
class ClassAdapter extends Adaptee implements Target {
@Override
public void request() {
specificRequest();
}
}
public class ClassAdapterExample {
public static void main(String[] args) {
Target target = new ClassAdapter();
target.request();
}
}
1.2 对象适配器
对象适配器使用组合来实现适配器功能。适配器类包含了一个原有类的实例(Adaptee)并实现了目标接口(Target)。
// 目标接口
interface Target {
void request();
}
// 需要适配的类(Adaptee)
class Adaptee {
void specificRequest() {
System.out.println("Adaptee's specific request");
}
}
// 对象适配器
class ObjectAdapter implements Target {
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specificRequest();
}
}
public class ObjectAdapterExample {
public static void main(String[] args) {
Adaptee adaptee = new Adaptee();
Target target = new ObjectAdapter(adaptee);
target.request();
}
}
适配器模式可以用于解决不同系统、库或API之间的接口不兼容的问题,使得它们可以协同工作。在实际开发,应根据具体需求选择使用类适配器还是对象适配器。
2、应用场景总结
2.1 封装有缺陷的接口设计
假设我们依赖的外部系统在接口设计方面有缺陷(比如含有大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候可是使用适配器模式了。
例如以下代码:
//这个类来自外部sdk,我们无权修改它的代码
public class Outer {
//...
public static void staticFunction1() { //...
}
public void uglyNamingFunction2() { //...
}
public void tooManyParamsFunction3(int paramA, int paramB, ...) { //...
}
public void lowPerformanceFunction4() { //...
}
}
// 使用适配器模式进行重构
public class ITarget {
void function1();
void function2();
void fucntion3(ParamsWrapperDefinition paramsWrapper);
void function4();
//...
}
// 注意:适配器类的命名不一定非得末尾带Adaptor
public class OuterAdaptor extends Outer implements ITarget {
//...
public void function1() {
super.staticFunction1();
}
public void function2() {
super.uglyNamingFucntion2();
}
public void function3(ParamsWrapperDefinition paramsWrapper) {
super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
}
public void function4() {
//...reimplement it...
}
}
2.2 统一多个类的接口设计
比如某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将他们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
例如:假设我们的系统要对用户输入的文本内容做敏感词过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是每个系统提供的过滤接口都是不同的。这意味指我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们可以复用调用敏感词过滤的代码。
例如下面的代码:
public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口
//text是原始文本,函数输出用***替换敏感词之后的文本
public String filterSexyWords(String text) {
// ...
}
public String filterPoliticalWords(String text) {
// ...
}
}
public class BSensitiveWordsFilter { // B敏感词过滤系统提供的接口
public String filter(String text) {
//...
}
}
public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口
public String filter(String text, String mask) {
//...
}
}
// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
public String filterSensitiveWords(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
maskedText = bFilter.filter(maskedText);
maskedText = cFilter.filter(maskedText, "***");
return maskedText;
}
}
// 使用适配器模式进行改造
public interface ISensitiveWordsFilter { // 统一接口定义
String filter(String text);
}
public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
private ASensitiveWordsFilter aFilter;
public String filter(String text) {
String maskedText = aFilter.filterSexyWords(text);
maskedText = aFilter.filterPoliticalWords(maskedText);
return maskedText;
}
}
//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...
// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement {
private List<ISensitiveWordsFilter> filters = new ArrayList<>();
public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
filters.add(filter);
}
public String filterSensitiveWords(String text) {
String maskedText = text;
for (ISensitiveWordsFilter filter : filters) {
maskedText = filter.filter(maskedText);
}
return maskedText;
}
}
2.3 替代依赖的外部接口
当我们把项目中依赖的一个外部系统替换为另一个系统时,利用适配器模式,可以减少对代码的改动。例如以下代码:
// 外部系统A
public interface IA {
//...
void fa();
}
public class A implements IA {
//...
public void fa() { //...
}
}
// 在我们的项目中,外部系统A的使用示例
public class Demo {
private IA a;
public Demo(IA a) {
this.a = a;
}
//...
}
Demo d = new Demo(new A());
// 将外部系统A替换成外部系统B
public class BAdaptor implemnts IA {
private B b;
public BAdaptor(B b) {
this.b= b;
}
public void fa() {
//...
b.fb();
}
}
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
Demo d = new Demo(new BAdaptor(new B()));
2.4 兼容老版本
兼容老版本一般有两个思路:
1)兼容老版本接口,新版本接口要在老版本接口上做扩展,两个版本均可用。
2)老版本接口计划废弃,标注deprecated,但是不想改动已有代码,让两个版本兼容并行,但新功能不使用老版本。
首先第一种场景,这是老版本的支付接口:
// 老版本支付接口
public interface OldPayment {
void pay(double amount);
}
然后,新版本的支付接口:
// 新版本支付接口
public interface NewPayment {
void makePayment(double amount, String currency);
}
接下来我们创建一个适配器类,实现新版本的支付接口,并在内部使用老版本的支付接口:
// 适配器类,实现新版本支付接口
public class PaymentAdapter implements NewPayment {
private OldPayment oldPayment;
public PaymentAdapter(OldPayment oldPayment) {
this.oldPayment = oldPayment;
}
@Override
public void makePayment(double amount, String currency) {
// 假设老版本支付接口只接受人民币,我们需要将其他货币转换为人民币
if ("CNY".equals(currency)) {
oldPayment.pay(amount);
} else {
double convertedAmount = convertToCNY(amount, currency);
oldPayment.pay(convertedAmount);
}
}
private double convertToCNY(double amount, String currency) {
// 在这里进行货币转换的逻辑
// 为了简化示例,我们假设所有其他货币都是1:1兑换人民币
return amount;
}
}
最后我们在客户端代码中使用适配器类,使其可以兼容新旧两种支付接口:
public class Client {
public static void main(String[] args) {
// 创建一个老版本支付实例
OldPayment oldPaymentInstance = new OldPaymentImpl();
// 创建适配器实例
NewPayment paymentAdapter = new PaymentAdapter(oldPaymentInstance);
// 通过适配器使用新版本支付接口
paymentAdapter.makePayment(100, "CNY");
paymentAdapter.makePayment(200, "USD");
}
}
这样我们就可以在不修改 oldpayment的接口的情况下,实现新旧接口的兼容。
第二种情景,老版本接口废弃不使用:
老版本接口在很多地方使用到了,我们想在不影响新接老接口的使用的情况下,完成升级,为了完成该需求,我们可以将适配器类修改为实现 老版本接口,然后在内部使用新版本接口。
老版本支付接口:
// 老版本支付接口
public interface OldPayment {
void pay(double amount);
}
新版本支付接口:
// 新版本支付接口
public interface NewPayment {
void makePayment(double amount, String currency);
}
创建一个适配器类,实现老版本的支付接口,并在内部使用新版本的支付接口:
// 适配器类,实现老版本支付接口
public class PaymentAdapter implements OldPayment {
private NewPayment newPayment;
public PaymentAdapter(NewPayment newPayment) {
this.newPayment = newPayment;
}
@Override
public void pay(double amount) {
// 假设新版本支付接口使用人民币,我们直接调用新接口
newPayment.makePayment(amount, "CNY");
}
}
最后,在客户端代码中,我们将原来使用老版本接口的实例替换为适配器实例:
public class Client {
public static void main(String[] args) {
// 创建一个新版本支付实例
NewPayment newPaymentInstance = new NewPaymentImpl();
// 创建适配器实例(我们只需要将这个新的适配器实例注入容器即可)
OldPayment paymentAdapter = new
PaymentAdapter(newPaymentInstance);
// 通过适配器使用老版本支付接口
paymentAdapter.pay(100);
}
}
3、源码中的使用
3.1 日志中的使用
Slf4j 这个日志框架你肯定不陌生,它相当于 JDBC 规范,是一套门面日志,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback……)来使用。
不仅如此,Slf4j 的出现晚于 JUL、JCL、log4j 等日志框架,所以,这些日志框架也不可能牺牲掉版本兼容性,将接口改造成符合 Slf4j 接口规范。Slf4j 也事先考虑到了这 个问题,所以,它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框架的接口进行二次封装,适配成统一的 Slf4j 接口定义。
具体代码省略,可自行查看相关源码。
3.2 SpringMVC框架
在SpringMVC中,为了适配各种类型的处理器(Handler),使用了适配器设计模 式。例如, org.springframework.web.servlet.HandlerAdapter 接口为各种处理器提供了统一的适配。具体实现类有 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 等。
相关源码可行查看。
4、代理、桥接、装饰器、适配器 4 种设计模式的区别
代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的 代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过Wrapper 类二次封装原始类。
尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、 应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。
五、门面模式
1、门面模式的原理与实现
门面模式,也叫外观模式,英文全称是Facade Design Pattern。在GoF的 设计模式一书中,门面模式是这样定义的:
Provide a unified interface to a set of interfaces in a subsystem. Facade Pattern defines a higher-level interface that makes the subsystem easier to use
翻译成中文就是:门面模式为子系统提供了一组统一的接口,定义一组高层接口让子系统更易用。
门面模式(Facade Pattern)是一种结构型设计模式,它为一组复杂的子系统提供了一个简单的接口,使得子系统更容易使用和理解。在Java中,门面模式通常使用一个门面类(Facade Class)来包装一个或多个复杂的子系统,使得客户端只需要和门面类交互,而不需要直接与子系统交互。
通常在以下情况下使用:
1)将复杂的子系统进行抽象和封装:当一个系统变得复杂时,往往会由多个子系统组成。门面模式可以将这些子系统进行抽象和封装,提供一个简单的接口供客户端使用。
2)隐藏子系统的复杂性:门面模式可以将子系统的复杂性隐藏起来,使得客户端不需要了解子系统的内部实现细节。
3)提供一个简单的接口:门面模式可以为客户端提供一个简单的接口,使得客户端可以更容易地使用子系统。
4)减低客户端与子系统的耦合:门面模式可以将客户端和子系统解耦,使得客户端不需要了解子系统的内部实现细节,也不需要直接和子系统交互。
例子一:
当一个系统需要与多个第三方服务进行交互时,可以使用门面模式来对这些服务进行封装,使得客户端只需要与一个门面类交互就可以完成对多个服务的调用,示例代码:
// 外部服务接口
interface ExternalService {
void doSomething();
}
// 外部服务实现类1
class ExternalServiceImpl1 implements ExternalService {
@Override
public void doSomething() {
System.out.println("ExternalServiceImpl1.doSomething");
}
}
// 外部服务实现类2
class ExternalServiceImpl2 implements ExternalService {
@Override
public void doSomething() {
System.out.println("ExternalServiceImpl2.doSomething");
}
}
// 门面类
class Facade {
private ExternalService service1;
private ExternalService service2;
public Facade() {
service1 = new ExternalServiceImpl1();
service2 = new ExternalServiceImpl2();
}
public void doSomething1() {
service1.doSomething();
}
public void doSomething2() {
service2.doSomething();
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Facade facade = new Facade();
facade.doSomething1();
facade.doSomething2();
}
}
当一个系统需要访问多个不同的数据库 时,可以使用门面模式来对这些数据库进行封装,使得客户端只需要与一个门面类交互就可以完成对多个数据库的访问。示例:
// 数据库接口
interface Database {
void execute(String sql);
}
// MySQL数据库实现类
class MySQLDatabase implements Database {
@Override
public void execute(String sql) {
System.out.println("Executing " + sql + " in MySQL database");
}
}
// Oracle数据库实现类
class OracleDatabase implements Database {
@Override
public void execute(String sql) {
System.out.println("Executing " + sql + " in Oracle database");
}
}
// 门面类
class DatabaseFacade {
private Database mysqlDatabase;
private Database oracleDatabase;
public DatabaseFacade() {
mysqlDatabase = new MySQLDatabase();
oracleDatabase = new OracleDatabase();
}
public void executeSQL(String sql, String databaseType) {
if (databaseType.equals("MySQL")) {
mysqlDatabase.execute(sql);
} else if (databaseType.equals("Oracle")) {
oracleDatabase.execute(sql);
} else {
throw new IllegalArgumentException("Unknown database type: " +
databaseType);
}
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
DatabaseFacade facade = new DatabaseFacade();
facade.executeSQL("SELECT * FROM users", "MySQL");
facade.executeSQL("SELECT * FROM customers", "Oracle");
}
}
在这个示例中,Database 是一个数据库接口,MySQLDatabase 和 OracleDatabase 是这个接口的两个具体实现。DatabaseFacade 是一个门面类,它将这两个数据库进行封装,并提供了一个 executeSQL 方法,用于执行 SQL 语句。
客户端只需要与 DatabaseFacade 类交互,并指定要访问的数据库类型,就可以完成对这两个数据库的访问。
以下是一些策略和指导方针:
1. 单一职责原则(SRP):每个接口或方法应该只有一个单一的职责。这可以保持接口的简单性,并使其更易于理解和使用。
2. 抽象程度:接口的设计应该足够抽象,能够容纳多种具体的实现,这可以提高接口的通用性。但是,过度的抽象可能会使接口难以理解和使用,因此需要找到一个合适的平衡点。
3. 尽量减少接口的依赖性:接口的方法不应该有过多的参数,尽量减少对其他对象或方法的依赖。这样可以使接口更简单,更易于使用。
4. 易于使用的API:设计接口时,应考虑使用者的需求和使用场景。应提供易于使用的API,例如,提供默认参数,使用有意义的方法和变量名,以及充分的文档和示例。
5. 灵活性和扩展性:接口设计应考虑未来可能的改变和扩展。接口的方法和参数应该有足够的灵活性,可以适应新的需求和变化。
6. 封装:接口应该尽可能地隐藏其实现的细节。这可以使接口更易于使用,同时也可以防止使用者直接访问和修改内部状态。
2、门面模式的应用场景举例
2.1 源码使用
门面模式在 JDK 源码中有很多应用。以下是一些常见的使用场景:
(1)JDBC:在 Java 中使用 JDBC 连接数据库时,通常会使用 DriverManager 来获取连接。DriverManager 就是一个门面类,它将多个数据库驱动进行封装,使得客户端只需要使用一个简单的接口就可以访问不同的数据库。
(2)Spring 框架:在 Spring 框架中,ApplicationContext 就是一个门面类,它将Spring 中的各个组件进行封装,使得客户端可以更容易地使用 Spring 中的功能。
(3)Servlet API:在 Servlet API 中,HttpServletRequest 和 HttpServletResponse 接口就是门面类,它们将底层的网络通信进行封装,使得开发者可以更容易地编写 Web 应用程序。
2.2 使用场景
在生产环境中,门面模式常常用于封装复杂的第三方API或系统,以提供简单、易用的接口给客户端使用。一个具体的例子是,假设我们的系统需要与多个支付系统进行 交互,而每个支付系统的接口和参数都不一样,这时候就可以使用门面模式来对这些 支付系统进行封装,使得客户端只需要使用一个简单的接口就可以完成对多个支付系 统的调用
示例:
// 支付系统接口
interface PaymentSystem {
void pay(double amount);
}
// 支付宝接口实现类
class AliPay implements PaymentSystem {
@Override
public void pay(double amount) {
System.out.println("支付宝支付:" + amount + "元");
}
}
// 微信支付接口实现类
class WeChatPay implements PaymentSystem {
@Override
public void pay(double amount) {
System.out.println("微信支付:" + amount + "元");
}
}
// 门面类
class PaymentFacade {
private PaymentSystem aliPay;
private PaymentSystem weChatPay;
public PaymentFacade() {
aliPay = new AliPay();
weChatPay = new WeChatPay();
}
public void payByAliPay(double amount) {
aliPay.pay(amount);
}
public void payByWeChatPay(double amount) {
weChatPay.pay(amount);
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
PaymentFacade paymentFacade = new PaymentFacade();
paymentFacade.payByAliPay(100.0);
paymentFacade.payByWeChatPay(200.0);
}
}
在这个示例中,PaymentSystem 是支付系统的接口,AliPay 和 WeChatPay 是这个接口的两个具体实现。PaymentFacade 是一个门面类,它将这两个支付系统进行封装,并提供了两个简单的方法 payByAliPay 和 payByWeChatPay。客户端只需要与 PaymentFacade 类交互,就可以完成对这两个支付系统的调用。这种方式可以方便地支持新的支付系统的加入,同时也可以提高客户端的调用效率和代码可读性。