抽象工厂模式
|字数总计:2.3k|阅读时长:11分钟|阅读量:|
抽象工厂模式案例解析
场景模拟
原本我的服务使用的是单机Redis,现在想要升级到Redis集群。
服务需要同时兼容不同种类的Redis集群,便于后期的灾备。
而不同Redis服务提供的接口各有不同,需要手动做适配抽象出来。
不能影响到目前正常运行的系统。
模拟单机Redis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package cn.zzq.redis; import cn.zzq.util.Logger;
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
public class RedisUtils { private final Logger logger = new Logger(RedisUtils.class); private final Map<String,String> dataMap = new ConcurrentHashMap<>(); public String get(String key){ logger.info("Redis 获取数据 key: %s",key); return dataMap.get(key); }
public void set(String key,String value){ logger.info("Redis 写入数据 key: %s val: %s",key,value); dataMap.put(key,value); }
public void set(String key, String value, long timeout, TimeUnit timeUnit){ logger.info("Redis 写入数据 key: %s val: %s timeout: %s timeUnit: %s",key,value,timeout,timeUnit); dataMap.put(key,value); }
public void del(String key){ logger.info("Redis 删除数据 key: %s",key); dataMap.remove(key); } }
|
模拟Redis集群EGM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package cn.zzq.redis.cluster;
import cn.zzq.util.Logger;
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
public class EGM { private final Logger logger = new Logger(EGM.class); private final Map<String,String> dataMap = new ConcurrentHashMap<>(); public String gain(String key){ logger.info("cn.zzq.redis.cluster.EGM 获取数据 key: %s",key); return dataMap.get(key); }
public void set(String key,String value){ logger.info("cn.zzq.redis.cluster.EGM 写入数据 key: %s val: %s",key,value); dataMap.put(key,value); }
public void setEx(String key, String value, long timeout, TimeUnit timeUnit){ logger.info("cn.zzq.redis.cluster.EGM 写入数据 key: %s val: %s timeout: %s timeUnit: %s",key,value,timeout,timeUnit); dataMap.put(key,value); }
public void delete(String key){ logger.info("cn.zzq.redis.cluster.EGM 删除数据 key: %s",key); dataMap.remove(key); } }
|
模拟Redis集群IIR
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package cn.zzq.redis.cluster;
import cn.zzq.util.Logger;
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
public class IIR { private final Logger logger = new Logger(IIR.class); private final Map<String,String> dataMap = new ConcurrentHashMap<>(); public String get(String key){ logger.info("cn.zzq.redis.cluster.IIR 获取数据 key: %s",key); return dataMap.get(key); }
public void set(String key,String value){ logger.info("cn.zzq.redis.cluster.IIR 写入数据 key: %s val: %s",key,value); dataMap.put(key,value); }
public void setExpire(String key, String value, long timeout, TimeUnit timeUnit){ logger.info("cn.zzq.redis.cluster.IIR 写入数据 key: %s val: %s timeout: %s timeUnit: %s",key,value,timeout,timeUnit); dataMap.put(key,value); }
public void del(String key){ logger.info("cn.zzq.redis.cluster.IIR 删除数据 key: %s",key); dataMap.remove(key); } }
|
一般的实现方案
抽象统一适配接口
从上述三套Redis服务来看,不同Redis服务提供的接口不同,没有提供统一的抽象接口,而又要确保两套系统能够相互兼容使用,同时不影响业务使用。
一种方式就是先抽象出接口
1 2 3 4 5 6 7 8 9 10 11
| package cn.zzq.application;
import java.util.concurrent.TimeUnit;
public interface CacheService { String get(final String key); void set(String key,String value); void set(String key, String value, long timeout, TimeUnit timeUnit); void del(String key); }
|
则原单机Redis服务便可改造成如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package cn.zzq.application;
import cn.zzq.redis.RedisUtils;
import java.util.concurrent.TimeUnit;
public class CacheServiceImpl implements CacheService{ private RedisUtils redisUtils = new RedisUtils(); @Override public String get(String key) { return redisUtils.get(key); }
@Override public void set(String key, String value) { redisUtils.set(key,value); }
@Override public void set(String key, String value, long timeout, TimeUnit timeUnit) { redisUtils.set(key, value, timeout, timeUnit); }
@Override public void del(String key) { redisUtils.del(key); } }
|
if…else…实现需求
通过if … else …手动判断redis类型来选择不同的redis服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| package cn.zzq.application;
import cn.zzq.redis.RedisUtils; import cn.zzq.redis.cluster.EGM; import cn.zzq.redis.cluster.IIR;
import java.util.concurrent.TimeUnit;
public class CacheClusterServiceImpl implements CacheService{ private final RedisUtils redisUtils = new RedisUtils(); private final EGM egm = new EGM(); private final IIR iir = new IIR();
private int redisType = 0;
@Override public String get(String key) { if(1 == redisType){ return egm.gain(key); } if(2 == redisType){ return iir.get(key); }
return redisUtils.get(key); }
@Override public void set(String key, String value) { if(1 == redisType){ egm.set(key,value); } if(2 == redisType){ iir.set(key,value); }
redisUtils.set(key,value); }
@Override public void set(String key, String value, long timeout, TimeUnit timeUnit) { if(1 == redisType){ egm.setEx(key, value, timeout, timeUnit); } if(2 == redisType){ iir.setExpire(key, value, timeout, timeUnit); }
redisUtils.set(key, value, timeout, timeUnit); }
@Override public void del(String key) { if(1 == redisType){ egm.delete(key); } if(2 == redisType){ iir.del(key); }
redisUtils.del(key); }
public int getRedisType() { return redisType; }
public void setRedisType(int redisType) { this.redisType = redisType; } }
|
这样,我们的单机Redis服务的使用如果遵循依赖倒置(DIP)原则,只依赖于CacheService抽象接口,那么原来的CacheServiceImpl便可无缝直接替换为CacheClusterServiceImpl,并且如果不设置redisType属性,那么还可自动兼容单机Redis服务。
但是,这种实现方式后期将会很难扩展与维护,并且接口方法的实现需要为每个兼容类做不同的判断和调用,极其容易出错,并且代码会变得相当丑。
编写测试代码
1 2 3 4 5 6 7 8 9
| @Test public void test_CacheClusterServiceImpl(){ Logger logger = new Logger(CacheClusterServiceImplTest.class); CacheClusterServiceImpl cacheService = new CacheClusterServiceImpl(); cacheService.setRedisType(1); cacheService.set("user_name","用户名"); String val = cacheService.get("user_name"); logger.info("缓存集群升级,测试结果:%s",val); }
|
测试结果:
1 2 3 4
| EGM: cn.zzq.redis.cluster.EGM 写入数据 key: user_name val: 用户名 RedisUtils: Redis 写入数据 key: user_name val: 用户名 EGM: cn.zzq.redis.cluster.EGM 获取数据 key: user_name CacheClusterServiceImplTest: 缓存集群升级,测试结果:用户名
|
从结果上看,程序能跑,但是以后维护变得很麻烦。
使用抽象工厂模式重构代码
编写适配器接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package cn.zzq.workshop;
import java.util.concurrent.TimeUnit;
public interface ICacheAdapter { String get(final String key); void set(String key,String value); void set(String key, String value, long timeout, TimeUnit timeUnit); void del(String key); }
|
对不同的Redis服务编写适配器
将两种不同的Redis服务适配到同一个接口,包装两个Redis集群中差异化的接口名,便于后面通过反射获得到适配后的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package cn.zzq.workshop.impl;
import cn.zzq.redis.cluster.EGM; import cn.zzq.workshop.ICacheAdapter;
import java.util.concurrent.TimeUnit;
public class EGMCacheAdapter implements ICacheAdapter { private final EGM egm = new EGM(); @Override public String get(String key) { return egm.gain(key); }
@Override public void set(String key, String value) { egm.set(key,value); }
@Override public void set(String key, String value, long timeout, TimeUnit timeUnit) { egm.setEx(key, value, timeout, timeUnit); }
@Override public void del(String key) { egm.delete(key); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package cn.zzq.workshop.impl;
import cn.zzq.redis.cluster.IIR; import cn.zzq.workshop.ICacheAdapter;
import java.util.concurrent.TimeUnit;
public class IIRCacheAdapter implements ICacheAdapter { private final IIR iir = new IIR(); @Override public String get(String key) { return iir.get(key); }
@Override public void set(String key, String value) { iir.set(key,value); }
@Override public void set(String key, String value, long timeout, TimeUnit timeUnit) { iir.setExpire(key, value, timeout, timeUnit); }
@Override public void del(String key) { iir.del(key); } }
|
通过JDK动态代理机制实现动态分发
编写JDK动态代理接口,可动态生成实现了ICacheAdapter接口的代理类,实现方法适配方法的动态拦截,并自动路由到对应的实现了适配器接口的Redis服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package cn.zzq.factory;
import cn.zzq.workshop.ICacheAdapter;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method;
public class JDKInvocationHandler implements InvocationHandler {
private final ICacheAdapter cacheAdapter;
public JDKInvocationHandler(ICacheAdapter cacheAdapter) { this.cacheAdapter = cacheAdapter; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Class<?>[] argTypes = new Class[args.length]; for (int i = 0; i < args.length; i++) { argTypes[i] = args[i].getClass(); } return ICacheAdapter.class.getMethod( method.getName(), argTypes ).invoke(cacheAdapter, args); } }
|
编写JDK动态代理工厂类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package cn.zzq.factory;
import cn.zzq.workshop.ICacheAdapter;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy;
public class JDKProxyFactory { public static <T> T getProxy(Class<T> cacheClazz, Class<? extends ICacheAdapter> cacheAdapter) throws Exception{ ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); InvocationHandler handler = new JDKInvocationHandler(cacheAdapter.newInstance()); return (T) Proxy.newProxyInstance(classLoader, new Class[]{cacheClazz}, handler); } }
|
编写测试代码
测试通过抽象工厂模式重构后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Test public void test_CacheService() throws Exception { Logger logger = new Logger(CacheClusterServiceImplTest.class); CacheService proxy_egm = JDKProxyFactory.getProxy(CacheService.class, EGMCacheAdapter.class); proxy_egm.set("user_name","用户名"); String val = proxy_egm.get("user_name"); logger.info("缓存集群升级,测试结果:%s",val);
CacheService proxy_iir = JDKProxyFactory.getProxy(CacheService.class, IIRCacheAdapter.class); proxy_iir.set("user_name","用户名"); String val1 = proxy_iir.get("user_name"); logger.info("缓存集群升级,测试结果:%s",val1); }
|
从上述代码中,可以看出,通过抽象工厂模式重构后的代码,如果后期加入新的Redis服务,仅需编写一个单独的适配器去适配新的Redis服务,而不是添加一坨坨if … else …语句。实现了开闭原则,对修改封闭,对扩展开放。实现了单一职责原则,一个类仅存在单一职责,而if else的实现使得一个类具有多种职责。实现了依赖倒置原则,所有依赖Redis服务的地方均依赖其适配器接口。
总结
抽象工厂模式,所要解决的问题就是在一个产品族,存在多个不同类型的产品(Redis集群、操作系统)情况下,接口选择的问题。而这种场景在业务开发中也是非常多见的,只不过可能有时候没有将它们抽象化出来。