硬核解读工厂模式,结合实际源码架构把工厂模式玩出花儿来!

举报
徐同学呀 发表于 2022/02/22 23:48:05 2022/02/22
【摘要】 首发CSDN:徐同学呀,原创不易,转载请注明源链接。我是徐同学,用心输出高质量文章,希望对你有所帮助。 文章目录 一、前言二、简单工厂玩出花来1、通用写法2、Map缓存产品对象3、反射...

首发CSDN:徐同学呀,原创不易,转载请注明源链接。我是徐同学,用心输出高质量文章,希望对你有所帮助。

一、前言

万物皆对象,理论上new一个对象的动作都可以交给一个工厂去做。工厂模式的出现,是为了将对象创建与客户端解耦隔离,客户端无需关心对象创建的细节,即使后期创建对象的方式有变化,只要对外交互的工厂不变,就影响不到客户端的使用。

一般情况工厂模式分为三种:简单工厂、工厂方法、抽象工厂,GOF中将简单工厂归到了工厂方法。也没必要去较真这个分类,本来就是经验之谈,为了方便理解,本篇倾向于前一种分类:

  1. 简单工厂,用的最多,也最好理解,可以生产同一类产品对象(实现于同一个接口),且每个对象的生产方式简单或者相似。一般只需要一个工厂,且为了使用方便,获取对象的方法是静态方法,无需new一个工厂类。
  2. 工厂方法,可以生产同一类产品对象(实现于同一个接口),但是每个对象的生产方式不同且比较复杂,如果这时还在一个工厂里生产,势必变得臃肿不好维护。所以,拆,为每个对象的创建分配专属工厂。这样工厂类就不止一个了,且后期增加一种新产品,就要新增一种工厂,扩展度挺高,但是类越来越多,代码结构也会变得越来越庞杂。
  3. 抽象工厂,生产不同类(实现于不同接口),但对象间存在一定联系或者属于同一系列的产品对象。这就在一定程度上缓解了工厂方法日益增多的问题。

三种工厂在复杂度、抽象和使用上是越来越复杂,越来越不好理解,越来越不常用。把简单工厂玩出花来,也能应付工作中大部分场景。

二、简单工厂玩出花来

简单工厂同它名字一样,就是简单。先来看下类图,看看一个简单工厂有哪些元素:

简单工厂

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();
            }
        }
    }
}

这里用到了hutoolClassUtil.scanPackageByAnnotation扫描类的工具,方法使用很简单,但是过程复杂一些,包扫面首先会获取当前类加载器并调用getResources获取指定目录下的所有类资源,如果是目录,扫描目录下的类文件或者jar文件,如果是jar包,则直接从jar包中获取类名(比较考验类加载机制和反射基本功)。

6、简单工厂在JDK中的体现

JDK中用到工厂模式的地方老多了,下面简单举几个例子:

(1)Calendar.getInstance(),可以根据不同时区TimeZone、地区Locale获取不同Calendar实例(不是单例),最终会调用到CalendarProvidergetInstance

Calendar.getInstance()

(2)DateFormat的创建,最终会调用到DateFormatProvider创建对象的方法:

DateFormat

DateFormat.get

(3)几个包装类型,如StringIntergerLongDouble等的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

可以看出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();

    IProductB createProductB();
}
public class Product1Factory implements IFactory {
    @Override
    public IProductA createProductA() {
        return new ProductA1();
    }

    @Override
    public IProductB createProductB() {
        return new ProductB1();
    }
}
public class Product2Factory implements IFactory {
    @Override
    public IProductA createProductA() {
        return new ProductA2();
    }

    @Override
    public IProductB createProductB() {
        return new ProductB2();
    }
}
public class Test {
    public static void main(String[] args) {
        IFactory factory1 = new Product1Factory();
        IProductA productA1 = factory1.createProductA();
        IProductB productB1 = factory1.createProductB();
    }
}

类好多啊,好复杂,好抽象。

2、抽象工厂在JMX中的体现

抽象工厂的例子挺不好找的,有些文章说Spring IOCBeanFactory及其子类是抽象工厂的体现,个人觉得不是那么典型,IOC是一个bean容器,是一个超级工厂,万物皆可对象,万物皆可生产。

还看到网上有说java.sql.Connection体现了抽象工厂,真的是生靠啊,明明生成的对象都是实现了Statement接口,算是同类产品,而且Connection没有抽象意义上工厂的概念。

public interface Connection  extends Wrapper, AutoCloseable {
    Statement createStatement() throws SQLException;
    PreparedStatement prepareStatement(String sql)throws SQLException;
    CallableStatement prepareCall(String sql) throws SQLException;
    ... ... 还有其他方法,如commit、rollback...
}

寻寻觅觅,终于是找到一个比较典型的代表JMX:

MBeanIntrospector提供了多个创建对象的抽象方法,MXBeanIntrospectorStandardMBeanIntrospector分别实现,创建不同的对象。

abstract class MBeanIntrospector<M> {
    
    abstract PerInterfaceMap<M> getPerInterfaceMap();
    abstract MBeanInfoMap getMBeanInfoMap();
    abstract MBeanAnalyzer<M> getAnalyzer(Class<?> mbeanInterface)
    throws NotCompliantMBeanException;
    abstract MBeanOperationInfo getMBeanOperationInfo(String operationName,
            M operation);
    abstract Descriptor getBasicMBeanDescriptor();
    abstract Descriptor getMBeanDescriptor(Class<?> resourceClass);
}
class MXBeanIntrospector extends MBeanIntrospector<ConvertingMethod> {
    private static final MXBeanIntrospector instance = new MXBeanIntrospector();

    static MXBeanIntrospector getInstance() {
        return instance;
    }
    @Override
    Descriptor getBasicMBeanDescriptor() {
        return new ImmutableDescriptor("mxbean=true",
                                       "immutableInfo=true");
    }
    // 其他方法省略
}
class StandardMBeanIntrospector extends MBeanIntrospector<Method> {
    private static final StandardMBeanIntrospector instance =
        new StandardMBeanIntrospector();

    static StandardMBeanIntrospector getInstance() {
        return instance;
    }
    
    @Override
    Descriptor getBasicMBeanDescriptor() {
        return ImmutableDescriptor.EMPTY_DESCRIPTOR;
    }
    // 其他方法省略
}

虽然MXBeanIntrospectorStandardMBeanIntrospector创建的Descriptor是同一个类ImmutableDescriptor,但是Descriptor是接口就说明有扩展的可能。

3、抽象工厂的优缺点

(1)优点:抽象工厂具备工厂方法的所有优点,易扩展,对于新产品族系的增加,新建一个抽象工厂就可以了;隔离变化,隔离复杂,符合开闭原则、迪米特原则、依赖倒置原则。并且可以生产多种产品,且产品间有一定联系,保证高内聚。

(2)缺点:概念更抽象了,同时增加了理解成本;产品族系中的产品扩展困难,比如一个产品族系新增了一个产品,就需要对抽象工厂加方法,就需要改到其工厂接口,实现了工厂接口的抽象工厂就都需要拓展新方法。

五、要点总结

  1. 设计模式,是前人总结出来的经验,有些开发经验再去学习,效果会更好,会发现原来自己平时开发也用到了一些设计模式,验证并加深。
  2. 设计模式的学习,最好是在开源框架中找到实际体现,看看大佬们是怎么用的,然后模仿实践,把它变成自己信手拈来的经验。掌握了常用的几个设计模式,在看其他开源框架时会更容易看懂作者的思路。
  3. 设计模式本来是让代码结构变得清晰、有条理、可扩展,但是如果一味的追求其概念,面面俱到,势必会适得其反,架构变得冗杂,难以理解。经验经验没有正确与否,就是要灵活运用,适当的多种模式结合或者改造。
  4. 工厂模式,一般分为简单工厂、工厂方法、抽象工厂,三种工厂是一个由简到繁的递进过程,越来越抽象,越来越复杂,找准其适用场景,才能化繁为简。
  5. 工厂模式隔离变化,无法完全消除if-else,只能被转移,或者通过牺牲一些空间(缓存)和性能(反射)让if-else隐式存在。

参考:

  • 《设计模式之美》王争。

  • 《设计模式就该这么学》谭勇德,虽然是培训机构咕泡学院出的书,但是讲的确实可以。

  • JDK8源码com.sun.jmx模块。

  • logback源码https://github.com/qos-ch/logback

如若文章有错误理解,欢迎批评指正,同时非常期待你的留言和点赞。如果觉得有用,不妨点个在看,让更多人受益。

文章来源: blog.csdn.net,作者:徐同学呀,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/weixin_36586120/article/details/118659768

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。