硬核解读工厂模式,结合实际源码架构把工厂模式玩出花儿来!
Posted 徐同学呀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核解读工厂模式,结合实际源码架构把工厂模式玩出花儿来!相关的知识,希望对你有一定的参考价值。
首发CSDN:徐同学呀,原创不易,转载请注明源链接。我是徐同学,用心输出高质量文章,希望对你有所帮助。
文章目录
一、前言
万物皆对象,理论上new
一个对象的动作都可以交给一个工厂去做。工厂模式的出现,是为了将对象创建与客户端解耦隔离,客户端无需关心对象创建的细节,即使后期创建对象的方式有变化,只要对外交互的工厂不变,就影响不到客户端的使用。
一般情况工厂模式分为三种:简单工厂、工厂方法、抽象工厂,GOF中将简单工厂归到了工厂方法。也没必要去较真这个分类,本来就是经验之谈,为了方便理解,本篇倾向于前一种分类:
- 简单工厂,用的最多,也最好理解,可以生产同一类产品对象(实现于同一个接口),且每个对象的生产方式简单或者相似。一般只需要一个工厂,且为了使用方便,获取对象的方法是静态方法,无需
new
一个工厂类。 - 工厂方法,可以生产同一类产品对象(实现于同一个接口),但是每个对象的生产方式不同且比较复杂,如果这时还在一个工厂里生产,势必变得臃肿不好维护。所以,拆,为每个对象的创建分配专属工厂。这样工厂类就不止一个了,且后期增加一种新产品,就要新增一种工厂,扩展度挺高,但是类越来越多,代码结构也会变得越来越庞杂。
- 抽象工厂,生产不同类(实现于不同接口),但对象间存在一定联系或者属于同一系列的产品对象。这就在一定程度上缓解了工厂方法日益增多的问题。
三种工厂在复杂度、抽象和使用上是越来越复杂,越来越不好理解,越来越不常用。把简单工厂玩出花来,也能应付工作中大部分场景。
二、简单工厂玩出花来
简单工厂同它名字一样,就是简单。先来看下类图,看看一个简单工厂有哪些元素:
1、通用写法
public interface IProduct {
void doSomeThing();
}
public class ProductA implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductA...doSomeThing");
}
}
public class ProductB implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductB...doSomeThing");
}
}
public class ProductC implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductC...doSomeThing");
}
}
public class SimpleFactory {
public static IProduct create(String name) {
if ("A".equals(name)) {
return new ProductA();
}
if ("B".equals(name)) {
return new ProductB();
}
if ("C".equals(name)) {
return new ProductC();
}
throw new UnsupportedOperationException("not match product, name=" + name);
}
}
public class ClineTest {
public static void main(String[] args) {
SimpleFactory.create("A").doSomeThing();
SimpleFactory.create("B").doSomeThing();
SimpleFactory.create("C").doSomeThing();
}
}
可以看到,SimpleFactory
中依然包含if-else
,如果没有SimpleFactory
,那么这些if-else
就是在客户端里,每次有新产品加入,就需要修改客户端的代码。但是有了SimpleFactory
,修改SimpleFactory
就好了,这就是隔离变化,将对象的创建与客户端解耦。
可以去掉SimpleFactory
中的if-else
吗?非常遗憾的说,无法去掉,个人观点,复杂度是守恒的,只能被转移,无法被消除。如果当前产品比较固定,或者将来的变化不大,少量的if-else
存在也是可以容忍的。
2、Map缓存产品对象
在某些场景下,if-else
可以隐式存在。比如创建一个对象不需要外界传递参数,不需要重复创建,无状态,以单例的形式存在,这时就可以用一个Map
来缓存对象。
public class SimpleFactory {
public static IProduct getInstance(String name) {
IProduct product = INSTANCE_MAP.get(name);
if (product == null) {
throw new UnsupportedOperationException("not match product, name=" + name);
}
return product;
}
private static final Map<String, IProduct> INSTANCE_MAP = new HashMap<>();
static {
INSTANCE_MAP.put("A", new ProductA());
INSTANCE_MAP.put("B", new ProductB());
}
}
看似if-else
没有了,其实是转移到了Map
中,此时如果新增产品,就需要改到Map
。
3、反射创建对象
用反射也可以让代码中没有if-else
。再次强调if-else
没有被消除,而是转移到了class
和实例对象中,且是以性能为代价。这次新增产品也不需要改到SimpleFactory
,但是增加了一点客户端交互成本,客户端必须知道生产的产品对象的class
对象是什么。
public class SimpleFactory {
public static IProduct getInstance2(Class<? extends IProduct> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("class is null");
}
try {
return clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4、反射+Map创建单例对象
单纯用Map缓存事先创建好的对象,无法达到懒加载的效果,而单纯用反射,每次创建的对象都是新的,如果不需要每次创建新的对象,且还想有懒加载的效果,有新产品也不想改SimpleFactory
,那就可以用反射+Map,看下面代码,是不是有些熟悉,单例模式的double check
:
public class SimpleFactory {
public static IProduct getInstance(Class<? extends IProduct> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("class is null");
}
IProduct product = INSTANCE_MAP.get(clazz.getSimpleName());
if (product != null) {
return product;
}
synchronized (SimpleFactory.INSTANCE_MAP) {
// double check
product = INSTANCE_MAP.get(clazz.getSimpleName());
if (product != null) {
return product;
}
try {
product = clazz.newInstance();
INSTANCE_MAP.put(clazz.getSimpleName(), product);
return product;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
private static final Map<String, IProduct> INSTANCE_MAP = new HashMap<>();
}
5、注解+反射+Map创建对象
前面3、4用到反射创建对象,客户端需要知道某个产品的class
是什么,通过class
的名称获取实例对象,且这个名称不能自定义。如果想要对象名称自定义,可以使用注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Product {
public String name() default "";
}
@Product(name = "A")
public class ProductA implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductA...doSomeThing");
}
}
@Product(name = "B")
public class ProductB implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductB...doSomeThing");
}
}
@Product
public class ProductC implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductC...doSomeThing");
}
}
public class SimpleFactory {
public static IProduct getInstance(String name) {
IProduct product = INSTANCE_MAP.get(name);
if (product == null) {
throw new UnsupportedOperationException("not match product, name=" + name);
}
return product;
}
private static final Map<String, IProduct> INSTANCE_MAP = new HashMap<>();
static {
init();
}
private static void init() {
System.out.println("init....");
Set<Class<?>> classSet = ClassUtil.scanPackageByAnnotation("com.stefan.designPattern.factory", Product.class);
for (Class<?> clazz : classSet) {
boolean isProduct = false;
in : for (Class<?> i : clazz.getInterfaces()) {
if (IProduct.class.equals(i)) {
isProduct = true;
break in;
}
}
if (!isProduct) {
continue;
}
Product product = clazz.getAnnotation(Product.class);
String name = product.name();
// 虽然指定name default是“”,但是这里取的话会隐式new String()
// 所以必须用equals比较,不能用==
if ("".equals(name)) {
name = clazz.getSimpleName();
}
try {
INSTANCE_MAP.put(name, (IProduct) clazz.newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
这里用到了hutool
的ClassUtil.scanPackageByAnnotation
扫描类的工具,方法使用很简单,但是过程复杂一些,包扫面首先会获取当前类加载器并调用getResources
获取指定目录下的所有类资源,如果是目录,扫描目录下的类文件或者jar文件,如果是jar包,则直接从jar包中获取类名(比较考验类加载机制和反射基本功)。
6、简单工厂在JDK中的体现
JDK中用到工厂模式的地方老多了,下面简单举几个例子:
(1)Calendar.getInstance()
,可以根据不同时区TimeZone
、地区Locale
获取不同Calendar
实例(不是单例),最终会调用到CalendarProvider
的getInstance
:
(2)DateFormat
的创建,最终会调用到DateFormatProvider
创建对象的方法:
(3)几个包装类型,如String
、Interger
、Long
、Double
等的valueOf
方法都是会创建对象或者从缓存中获取。
(4)线程工厂java.util.concurrent.ThreadFactory#newThread
。
(其他开源框架如Tomcat中也会大量用到简单工厂,暂时不再举例。)
7、简单工厂优缺点
(1)优点:简单工厂的优点就是简单,容易理解,还有就是解耦,隔离变化。
(2)缺点:简单工厂一般以静态方法为主,工厂类单一,难以扩展;当产品基数增多时,工厂类代码可能会比较臃肿,虽然可以通过缓存、反射、自定义注解等方式缓解if-else
,但是需要牺牲一些内存和性能,且有一定的使用场景和开发成本。
三、工厂方法
工厂方法,外国人起的破名字,和抽象工厂傻傻分不清楚,且看定义和类图:
工厂方法就是为了解决简单工厂难以扩展问题的,依然是生产同类产品,但是每个产品生产较为复杂且细节不尽相同,所以给每个产品分配一个专属工厂:
1、通用写法
产品类和简单工厂一样,假装创建对象的过程复杂且不同;每个产品一个工厂,为了面向接口编程,可扩展性,所以建立一个工厂接口,所有工厂实现这个接口:
public interface IProduct {
void doSomeThing();
}
public class ProductA implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductA...doSomeThing");
}
}
public class ProductB implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductB...doSomeThing");
}
}
public class ProductC implements IProduct {
@Override
public void doSomeThing() {
System.out.println("ProductC...doSomeThing");
}
}
public interface IFactory {
IProduct create();
}
public class AFactory implements IFactory {
@Override
public IProduct create() {
return new ProductA();
}
}
public class BFactory implements IFactory {
@Override
public IProduct create() {
return new ProductB();
}
}
public class CFactory implements IFactory {
@Override
public IProduct create() {
return new ProductC();
}
}
public class Test {
public static void main(String[] args) {
IFactory factoryA = new AFactory();
IProduct productA = factoryA.create();
productA.doSomeThing();
IFactory factoryB = new BFactory();
IProduct productB = factoryB.create();
productB.doSomeThing();
}
}
很明显类增加了不少,也使得整体变复杂了,再看客户端怎么使用工厂生产产品,需要先实例化对应工厂类,然后创建对应产品。如果客户端不知道使用哪个工厂那还得if-else
选择工厂,咋还回去了,越来越复杂了。。。那就再给工厂加个简单工厂,把if-else
移到一个简单工厂里,生产不同的工厂方法,真的是复杂了,达咩!注意工厂方法的使用场景!
工厂方法的出现是因为每个对象的创建过程较复杂且有自己的创建细节,所以才给每个产品对象分配了一个工厂;且客户端调用、是有不同的场景对应不同的工厂方法,不需要再在工厂方法上面加简单工厂,这是最佳适配。
如果后期新增产品,同时新增工厂,不需要改到旧代码。
2、工厂方法在slf4j+logback中的体现
在开源日志框架门面slf4j
中提供了Logger
接口和ILoggerFactory
供第三方框架扩展(如logback
)。不同的工厂负责生产不同的日志框架,基本类图如下:
可以看出slf4j+logback
既使用了工厂方法,又使用了简单工厂去管理工厂方法。
3、工厂方法优缺点
(1)优点:易扩展,新增产品同时新增工厂,隔离变化,隔离复杂,理想状态完全符合开闭原则、迪米特原则(最少知道原则)、依赖倒置原则。
(2)缺点:类太多了,增加了代码复杂度;更加抽象,不易理解;依然只能生产一种产品,种类单一。
四、抽象工厂
抽象工厂,可以生产不同类型的产品(实现于不同抽象接口),这些产品之间有一定的联系,可以归纳为一个族系的产品。
抽象工厂因为可以生产多种产品,所以不会像工厂方法那样有那么多的工厂类。不过也是因为产品的族系越来越复杂,才使得抽象工厂有了用武之地。
如有两条产品系列1和2,每个系列有一个抽象工厂,每个工厂可以生产A和B。
1、通用写法
public interface IProductA {
void doSomeThing();
}
public interface IProductB {
void doSomeThing();
}
public class ProductA1 implements IProductA {
public void doSomeThing() {
System.out.println("ProductA1...doSomeThing");
}
}
public class ProductA2 implements IProductA {
public void doSomeThing() {
System.out.println("ProductA2...doSomeThing");
}
}
public class ProductB1 implements IProductB {
public void doSomeThing() {
System.out.println("ProductB1...doSomeThing");
}
}
public class ProductB2 implements IProductB {
public void doSomeThing() {
System.out.println("ProductB2...doSomeThing");
}
}
public interface IFactory {
IProductA createProductA()以上是关于硬核解读工厂模式,结合实际源码架构把工厂模式玩出花儿来!的主要内容,如果未能解决你的问题,请参考以下文章