抽象工厂模式案例解析

场景模拟

原本我的服务使用的是单机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;

/**
* 本类模拟了Redis单机服务,仅供模拟Redis使用
*/
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;

/**
* Redis集群服务EGM
*/
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;

/**
* Redis集群服务IIR
*/
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集群、操作系统)情况下,接口选择的问题。而这种场景在业务开发中也是非常多见的,只不过可能有时候没有将它们抽象化出来。