【设计模式】抽象工厂模式
参考资料:
Java设计模式 - 抽象工厂模式
重学 Java 设计模式:实战抽象工厂模式
文章目录
- 【设计模式】抽象工厂模式
- 一、抽象工厂模式介绍
- 1.1、什么是工厂方法模式
- 1.2、角色概述
- 二、案例场景模拟
- 2.1、背景一:Redis的集群扩容
- 2.1.1、模拟单机服务 RedisUtils
- 2.1.2、模拟集群 EGM
- 2.1.3、模拟集群 IIR
- 2.1.4、if/else实现需求
- 2.1.5、定义适配接口
- 2.1.6、实现集群使用服务
- 2.1.7、定义抽象工程代理类和实现
- 2.1.8、测试验证
- 2.2、背景二:咖啡店点餐系统
- 2.2.1、抽象产品及具体产品
- 2.2.2、抽象工厂 及具体工厂
- 2.2.3、测试
- 三、总结
一、抽象工厂模式介绍
1.1、什么是工厂方法模式
抽象工厂模式与工厂方法模式虽然主要意图都是为了解决,接口选择问题。但在实现上,抽象工厂是一个中心工厂,创建其他工厂的模式。
可能在平常的业务开发中很少关注这样的设计模式或者类似的代码结构,但是这种场景确一直在我们身边,例如;
-
不同系统内的回车换行
- Unix系统里,每行结尾只有 <换行>,即
\n
; - Windows系统里面,每行结尾是 <换行><回车>,即
\n\r
; - Mac系统里,每行结尾是 <回车>
- Unix系统里,每行结尾只有 <换行>,即
-
IDEA 开发工具的差异展示(Win\Mac)
除了这样显而易见的例子外,我们的业务开发中时常也会遇到类似的问题,需要兼容做处理。但大部分经验不足的开发人员,常常直接通过添加ifelse
方式进行处理了。
1.2、角色概述
抽象工厂模式中存在四种角色,分别是抽象工厂角色,具体工厂角色,抽象产品角色,具体产品角色。
- 抽象工厂:提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂:主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品:定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
二、案例场景模拟
2.1、背景一:Redis的集群扩容
随着业务超过预期的快速发展,系统的负载能力也要随着跟上。原有的单机 Redis
已经满足不了系统需求。这时候就需要更换为更为健壮的Redis集群服务,虽然需要修改但是不能影响目前系统的运行,还要平滑过渡过去。
随着这次的升级,可以预见的问题会有;
- 很多服务用到了Redis需要一起升级到集群。
- 需要兼容集群A和集群B,便于后续的灾备。
- 两套集群提供的接口和方法各有差异,需要做适配。
- 不能影响到目前正常运行的系统。
2.1.1、模拟单机服务 RedisUtils
- 模拟Redis功能,也就是假定目前所有的系统都在使用的服务
- 类和方法名次都固定写死到各个业务系统中,改动略微麻烦
2.1.2、模拟集群 EGM
- 模拟一个集群服务,但是方法名与各业务系统中使用的方法名不同。有点像你mac,我用win。做一样的事,但有不同的操作。
2.1.3、模拟集群 IIR
- 这是另外一套集群服务,有时候在企业开发中就很有可能出现两套服务,这里我们也是为了做模拟案例,所以添加两套实现同样功能的不同服务,来学习抽象工厂模式。
综上可以看到,我们目前的系统中已经在大量的使用redis服务,但是因为系统不能满足业务的快速发展,因此需要迁移到集群服务中。而这时有两套集群服务需要兼容使用,又要满足所有的业务系统改造的同时不影响线上使用。
2.1.4、if/else实现需求
public class CacheServiceImpl implements CacheService {
private RedisUtils redisUtils = new RedisUtils();
private EGM egm = new EGM();
private IIR iir = new IIR();
public String get(String key, int redisType) {
if (1 == redisType) {
return egm.gain(key);
}
if (2 == redisType) {
return iir.get(key);
}
return redisUtils.get(key);
}
public void set(String key, String value, int redisType) {
if (1 == redisType) {
egm.set(key, value);
return;
}
if (2 == redisType) {
iir.set(key, value);
return;
}
redisUtils.set(key, value);
}
}
抽象工厂模型结构
- 工程中涉及的部分核心功能代码,如下;
ICacheAdapter
,定义了适配接口,分别包装两个集群中差异化的接口名称。EGMCacheAdapter
、IIRCacheAdapter
JDKProxy
、JDKInvocationHandler
,是代理类的定义和实现,这部分也就是抽象工厂的另外一种实现方式。通过这样的方式可以很好的把原有操作Redis的方法进行代理操作,通过控制不同的入参对象,控制缓存的使用。
2.1.5、定义适配接口
public interface ICacheAdapter {
String get(String key);
void set(String key, String value);
void set(String key, String value, long timeout, TimeUnit timeUnit);
void del(String key);
}
- 这个类的主要作用是让所有集群的提供方,能在统一的方法名称下进行操作。也方面后续的拓展。
2.1.6、实现集群使用服务
EGMCacheAdapter
public class EGMCacheAdapter implements ICacheAdapter {
private EGM egm = new EGM();
public String get(String key) {
return egm.gain(key);
}
public void set(String key, String value) {
egm.set(key, value);
}
public void set(String key, String value, long timeout, TimeUnit timeUnit) {
egm.setEx(key, value, timeout, timeUnit);
}
public void del(String key) {
egm.delete(key);
}
}
IIRCacheAdapter
public class IIRCacheAdapter implements ICacheAdapter {
private IIR iir = new IIR();
public String get(String key) {
return iir.get(key);
}
public void set(String key, String value) {
iir.set(key, value);
}
public void set(String key, String value, long timeout, TimeUnit timeUnit) {
iir.setExpire(key, value, timeout, timeUnit);
}
public void del(String key) {
iir.del(key);
}
}
- 以上两个实现都非常容易,在统一方法名下进行包装。
2.1.7、定义抽象工程代理类和实现
JDKProxy
public static <T> T getProxy(Class<T> interfaceClass, ICacheAdapter cacheAdapter) throws Exception {
InvocationHandler handler = new JDKInvocationHandler(cacheAdapter);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = interfaceClass.getInterfaces();
return (T) Proxy.newProxyInstance(classLoader, new Class[]{classes[0]}, handler);
}
- 这里主要的作用就是完成代理类,同时对于使用哪个集群有外部通过入参进行传递。
JDKInvocationHandler
public class JDKInvocationHandler implements InvocationHandler {
private ICacheAdapter cacheAdapter;
public JDKInvocationHandler(ICacheAdapter cacheAdapter) {
this.cacheAdapter = cacheAdapter;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return ICacheAdapter.class.getMethod(method.getName(), ClassLoaderUtils.getClazzByArgs(args)).invoke(cacheAdapter, args);
}
}
- 在代理类的实现中其实也非常简单,通过穿透进来的集群服务进行方法操作。
- 另外在
invoke
中通过使用获取方法名称反射方式,调用对应的方法功能,也就简化了整体的使用。 - 到这我们就已经将整体的功能实现完成了,关于抽象工厂这部分也可以使用非代理的方式进行实现。
2.1.8、测试验证
编写测试类:
@Test
public void test_CacheService() throws Exception {
CacheService proxy_EGM = JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
proxy_EGM.set("user_name_01","yby");
String val01 = proxy_EGM.get("user_name_01");
System.out.println(val01);
CacheService proxy_IIR = JDKProxy.getProxy(CacheServiceImpl.class, new IIRCacheAdapter());
proxy_IIR.set("user_name_01","yby");
String val02 = proxy_IIR.get("user_name_01");
System.out.println(val02);
}
- 在测试的代码中通过传入不同的集群类型,就可以调用不同的集群下的方法。
JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
- 如果后续有扩展的需求,也可以按照这样的类型方式进行补充,同时对于改造上来说并没有改动原来的方法,降低了修改成本。
结果:
23:07:06.953 [main] INFO org.itstack.demo.design.matter.EGM - EGM写入数据 key:user_name_01 val:yby
23:07:06.956 [main] INFO org.itstack.demo.design.matter.EGM - EGM获取数据 key:user_name_01
测试结果:yby
23:07:06.957 [main] INFO org.itstack.demo.design.matter.IIR - IIR写入数据 key:user_name_01 val:yby
23:07:06.957 [main] INFO org.itstack.demo.design.matter.IIR - IIR获取数据 key:user_name_01
测试结果:yby
Process finished with exit code 0
- 运行结果正常,这样的代码满足了这次拓展的需求,同时你的技术能力也给老板留下了深刻的印象。
- 研发自我能力的提升远不是外接的压力就是编写一坨坨代码的接口,如果你已经熟练了很多技能,那么可以在即使紧急的情况下,也能做出完善的方案。
2.2、背景二:咖啡店点餐系统
设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。
现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。
2.2.1、抽象产品及具体产品
第一种产品:
Coffee(第一种抽象产品类)、AmericanCoffee和LatteCoffee (具体产品类)
public abstract class Coffee {
public abstract void addMilk();
public abstract void addSugar();
public abstract String getName();
}
public class AmericanCoffee extends Coffee {
@Override
public void addMilk() { System.out.println("给咖啡加奶"); }
@Override
public void addSugar() { System.out.println("给咖啡加糖"); }
@Override
public String getName() { return "美式咖啡"; }
}
public class LatteCoffee extends Coffee {
@Override
public void addMilk() { System.out.println("给咖啡加奶"); }
@Override
public void addSugar() { System.out.println("给咖啡加糖"); }
@Override
public String getName() { return "拿铁咖啡"; }
}
第二种产品:
Dessert (第二种抽象产品 甜点) MatchaMousse、Tiramisu(具体产品类)
public abstract class Dessert {
public abstract void show();
}
public class MatchaMousse extends Dessert{
@Override
public void show() { System.out.println("抹茶慕斯"); }
}
public class Tiramisu extends Dessert{
@Override
public void show() { System.out.println("提拉米苏"); }
}
2.2.2、抽象工厂 及具体工厂
DessertFactory (抽象工厂) AmericanDessertFactory 和(具体工厂)
public interface DessertFactory {
Coffee createCoffee();
Dessert createDessert();
}
public class AmericanDessertFactory implements DessertFactory {
@Override
public Coffee createCoffee() { return new AmericanCoffee(); }
@Override
public Dessert createDessert() { return new MatchaMousse(); }
}
public class ItalyDessertFactory implements DessertFactory {
@Override
public Coffee createCoffee() { return new LatteCoffee(); }
@Override
public Dessert createDessert() { return new Tiramisu(); }
}
2.2.3、测试
public class Client {
public static void main(String[] args) {
// 想次美式东西
// AmericanDessertFactory factory = new AmericanDessertFactory();
// 想换成意大利风味,仅仅只需要换一个工厂类 其他的代码无需改变
ItalyDessertFactory factory= new ItalyDessertFactory();
Coffee coffee = factory.createCoffee();
Dessert dessert = factory.createDessert();
System.out.println(coffee.getName());
dessert.show();
}
}
如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。
三、总结
- 抽象工厂模式,所要解决的问题就是在一个产品族,存在多个不同类型的产品(Redis集群、操作系统)情况下,接口选择的问题。而这种场景在业务开发中也是非常多见的,只不过可能有时候没有将它们抽象化出来。
你的代码只是被ifelse埋上了!
当你知道什么场景下何时可以被抽象工程优化代码,那么你的代码层级结构以及满足业务需求上,都可以得到很好的完成功能实现并提升扩展性和优雅度。- 那么这个设计模式满足了;单一职责、开闭原则、解耦等优点,但如果说随着业务的不断拓展,可能会造成类实现上的复杂度。但也可以说算不上缺点,因为可以随着其他设计方式的引入和代理类以及自动生成加载的方式降低此项缺点。