从工厂模式到依赖注入

常说的“依赖注入”是如何发展过来的呢?

工厂方法模式

工厂模式是常见的设计模式。

静态工厂

介绍

静态工厂的工厂类集中了所有实例的创建逻辑,你可以告诉工厂“我要个 A 类型的产品”,工厂就乖乖给了你个 A,是一种 valueOf 类型的指令。

代码

class SimpleFactoryImpl {
    public static Product getInstance(ProductType productType) {
        switch (productType) {
            case TYPE_1:
                return () -> System.out.println("Product Type1");
            case TYPE_2:
                return () -> System.out.println("Product Type2");
            case TYPE_3:
                return () -> System.out.println("Product Type3");
            default:
                return null;
        }
    }
}

interface Product {
    void use();
}

enum ProductType {
    TYPE_1, TYPE_2, TYPE_3
}

缺点

这样的“工厂”不利于扩展,如果我现在有个 TYPE_4 的 Product,还得修改工厂本身的代码,破坏了 OCP(开-闭原则,Open-Closed Principle)。

工厂方法

介绍

对于简单工厂存在的缺陷,工厂方法作了改进,将创建对象的逻辑抽象到一个接口内,使不同的实现类都实现工厂方法。

工厂方法常常用在“不想让调用者决定使用哪个实现”或者“调用者自己不知道要调用哪个实现”的情况。

当需要扩展工厂方法时,不需要修改原有的逻辑分支,只需要在新实现类中实现获取产品的方法即可。

工厂方法

代码

interface Product {
    void use();
}

class Product1 implements Product {

    @Override
    public void use() {
        System.out.println("Product Type1");
    }
}

class Product2 implements Product {

    @Override
    public void use() {
        System.out.println("Product Type2");
    }
}

class Product3 implements Product {

    @Override
    public void use() {
        System.out.println("Product Type3");
    }
}

interface FactoryMethod {
    Product getInstance();
}

class FactoryMethodImpl1 implements FactoryMethod {

    @Override
    public Product getInstance() {
        return new Product1();
    }
}

class FactoryMethodImpl2 implements FactoryMethod {

    @Override
    public Product getInstance() {
        return new Product2();
    }
}

class FactoryMethodImpl3 implements FactoryMethod {

    @Override
    public Product getInstance() {
        return new Product3();
    }
}

这样一来,当我利用多态去使用 FactoryMethod 类型的实例时,我不用关心实例具体的类型,我知道这些实例都可以 get 一个 Product 类型的 Instance,到时候 use 就完事儿了。

典型举例

集合框架 java.util.Collection 接口继承了 java.lang.Iterable 接口,后者的 iterator 方法即 Collection 接口的工厂方法,产品就是迭代器 java.util.Iterator。

我们在调用 Collection 类型的各种集合时不需要关心 iterator 方法具体返回的是什么类型的迭代器对象(比如 ArrayList 的 iterator 方法返回的是其内部类 java.util.ArrayList.Itr),只需要知道 Collection 都有一个 iterator 方法可以返回具有遍历 Collection 功能的对象就行了。

缺点

  1. 工厂方法的调用者不能决定调用的是哪个类型。
  2. 如果工厂需要生产多类产品,则工厂方法不能很好地满足。

针对第一点,与其说是缺点,不如说是初衷,工厂方法的初衷就是使调用者对工厂返回的类型无感知。

针对第二点,抽象工厂模式进行了改进,下一篇我们会介绍。

抽象工厂模式

抽象工厂用来处理一般工厂模式带来的扩展性问题。

首先解释“为什么在多种产品的情况下不新建一个 interface FactoryMethod2”

代码

为了解释这种情景,我们添加了一类产品 TrademarkSticker。

/**
 * 产品
 */
interface Product {
    void use();
}

/**
 * 可被贴标
 */
interface CanBeStuck {
    void setTrademark(String trademark);
}

/**
 * 鼠标既是产品又可以被贴商标
 */
interface Mouse extends Product, CanBeStuck {
}

/**
 * 贴标器
 */
interface TrademarkSticker {
    void stick(CanBeStuck canBeStuck);
}

/**
 * 既生产鼠标又生产贴标器的工厂
 */
interface Factory {
    Mouse produceMouse();

    TrademarkSticker produceTs();
}

class DellFactory implements Factory {
    @Override
    public Mouse produceMouse() {
        DellMouse dellMouse = new DellMouse();
        // 鼠标需要贴商标,生产鼠标需要贴标器的支持
        produceTs().stick(dellMouse);
        return dellMouse;
    }

    @Override
    public TrademarkSticker produceTs() {
        return new DellTrademarkSticker();
    }
}

class HPFactory implements Factory {
    @Override
    public Mouse produceMouse() {
        HPMouse hpMouse = new HPMouse();
        // 鼠标需要贴商标,生产鼠标需要贴标器的支持
        produceTs().stick(hpMouse);
        return hpMouse;
    }

    @Override
    public TrademarkSticker produceTs() {
        return new HPTrademarkSticker();
    }
}

class DellMouse implements Mouse {
    @Override
    public void use() {
        System.out.println("using DELL mouse");
    }

    @Override
    public void setTrademark(String trademark) {
        System.out.println("trademark is set to " + trademark);
    }
}

class HPMouse implements Mouse {
    @Override
    public void use() {
        System.out.println("using HP mouse");
    }

    @Override
    public void setTrademark(String trademark) {
        System.out.println("trademark is set to " + trademark);
    }
}

class DellTrademarkSticker implements TrademarkSticker {
    @Override
    public void stick(CanBeStuck canBeStuck) {
        canBeStuck.setTrademark("DELL");
    }
}

class HPTrademarkSticker implements TrademarkSticker {
    @Override
    public void stick(CanBeStuck canBeStuck) {
        canBeStuck.setTrademark("HP");
    }
}

原本可以通过两个 FactoryMethod 抽象出两种产品的工厂,但是由于产品间有相互依赖的关系,某个产品的生产需要依赖其它产品才能完成,这种情况如果使用 FactoryMethod 的话,会在产品中调工厂,代码耦合更严重。所以,我们才需要在工厂方法的基础上进行改造,形成上面抽象工厂的逻辑。

结构

抽象工厂

缺点

但是抽象工厂也有缺陷,当我们需要增加新的产品线时(比如增加 Keyboard),在新增实现类后,还要修改所有 Factory 实现类中生产 Keyboard 的逻辑。

从工厂模式到依赖注入

Spring 的 Web 容器等 IOC 容器在 Servlet 容器启动期间对所“管辖”的组件间的依赖进行解析,帮助程序为不同的 Java Bean 注入所需要的依赖,程序员不需要关心他们的来源,只需要将配置文件编写好,这大大方便了 JavaEE 开发者们的开发工作。

Martin Fowler 大神有一篇 Inversion of Control Containers and the Dependency Injection pattern,即“控制反转型容器和依赖注入模式”,其中介绍了多种依赖注入的方式,是 IOC 的经典必读。

抽象工厂的问题

如上文所述,在抽象工厂模式中,如果要多增加一个产品线,会需要修改很多代码。

我们尝试模拟抽象工厂中的客户端调用工厂方法得到产品的场景:

public static void main(String[] args) {
    Mouse mouse = new DellFactory().produceMouse();
    mouse.use();

    mouse = new HPFactory().produceMouse();
    mouse.use();
}

结果如下:

result-1

现在添加一个产品线 Keyboard,我们需要增加以下代码:

interface Keyboard extends Product, CanBeStuck {
    
}

class DellKeyboard implements Keyboard {
    @Override
    public void use() {
        System.out.println("using DELL keyboard");
    }

    @Override
    public void setTrademark(String trademark) {
        System.out.println("trademark is set to " + trademark);
    }
}

class HPKeyboard implements Keyboard {
    @Override
    public void use() {
        System.out.println("using HP keyboard");
    }

    @Override
    public void setTrademark(String trademark) {
        System.out.println("trademark is set to " + trademark);
    }
}

并修改以下代码:

interface Factory {
    ...
    
    Keyboard produceKeyboard();
}

class DellFactory implements Factory {
    ...
    
    @Override
    public Keyboard produceKeyboard() {
        DellKeyboard dellKeyboard = new DellKeyboard();
        produceTs().stick(dellKeyboard);
        return dellKeyboard;
    }
}

class HPFactory implements Factory {
    ...
    
    @Override
    public Keyboard produceKeyboard() {
        HPKeyboard hpKeyboard = new HPKeyboard();
        produceTs().stick(hpKeyboard);
        return hpKeyboard;
    }
}

可以预想,当代码量很大,业务比较复杂的情况下,这将会是件很恶心的事情。其实对于 Java 语言来说,利用反射去生成对象再方便好不过了。

优化抽象工厂

Java 提供反射机制,可以让程序获取代码运行时的信息,我们尝试结合 反射和配置文件 来优化抽象工厂。

为了方便展示重点,我们把原抽象工厂代码中体现产品联系的 Trademark 去掉,保留单纯的产品侧抽象:

interface Product {
    void use();
}

interface Mouse extends Product {
}

interface Keyboard extends Product {
}

class DellMouse implements Mouse {
    @Override
    public void use() {
        System.out.println("using DELL mouse");
    }
}

class HPMouse implements Mouse {
    @Override
    public void use() {
        System.out.println("using HP mouse");
    }
}

class DellKeyboard implements Keyboard {
    @Override
    public void use() {
        System.out.println("using DELL keyboard");
    }
}

class HPKeyboard implements Keyboard {
    @Override
    public void use() {
        System.out.println("using HP keyboard");
    }
}

修改 Factory 为面向 JavaBean 的 BeanFactory:

interface BeanFactory {
    Object getBean(String beanName) throws Exception;
}

class MyBeanFactory implements BeanFactory {
    private Map<String, Object> beanMap;
    private Document document;

    @SuppressWarnings("unchecked")
    public MyBeanFactory(String configFilePath) {
        try {
            SAXReader saxReader = new SAXReader();
            this.beanMap = new HashMap<>();
            this.document = saxReader.read(new File(configFilePath));
            injectRecursively(this.document.getRootElement().elements("bean"));
        } catch (Exception e) {
            System.err.println("initialize bean factory error: " + e.getCause().getMessage());
        }
    }

    /**
     * initialize <bean>s
     */
    private void injectRecursively(List<? extends Element> elements) throws Exception {
        for (Element element : elements) {
            // <bean>
            String beanId = element.attribute("id").getValue();
            String beanClassName = element.attribute("class").getValue();
            if (beanMap.get(beanId) != null) {
                continue;
            }
            Object bean = Class.forName(beanClassName).newInstance();
            // <property>s
            List properties = element.elements("property");
            if (properties.size() != 0) {
                for (Object prop : properties) {
                    Element subElement = (Element) prop;
                    String subBeanAttr = subElement.attribute("attr").getValue();
                    String subBeanId = subElement.attribute("ref").getValue();
                    Object initialized = this.beanMap.get(subBeanId);
                    if (initialized != null) {
                        // property 已被工厂持有
                        ReflectionUtils.setFieldValue(bean, subBeanAttr, initialized);

                    } else {
                        Element refed = findClassConfig(subBeanId);
                        ArrayList<Element> beanList = new ArrayList<>();
                        beanList.add(refed);
                        injectRecursively(beanList);
                        ReflectionUtils.setFieldValue(bean, subBeanAttr, beanMap.get(subBeanId));
                    }
                }
            }
            beanMap.put(beanId, bean);
        }
    }

    private Element findClassConfig(String beanId) {
        Element element = null;
        try {
            element = (Element) this.document.selectSingleNode("/beans/bean[@id='" + beanId + "']");
        } catch (Exception ignored) {
        }
        return element;
    }

    @Override
    public Object getBean(String id) throws Exception {
        return this.beanMap.get(id);
    }

}

代码中使用了 DOM4j 解析 xml 文件,实现了一个简陋的利用反射技术使用 setter 进行注入的 BeanFactory。

使用工厂产品的类:

class User {
    private Mouse mouse;
    private Keyboard keyboard;

    public void setMouse(Mouse mouse) {
        this.mouse = mouse;
    }

    public void setKeyboard(Keyboard keyboard) {
        this.keyboard = keyboard;
    }

    public void use() {
        mouse.use();
        keyboard.use();
    }
}

配置文件 config.xml:

<beans>
    <bean id="user" class="di.User">
        <property attr="mouse" ref="dellMouse"/>
        <property attr="keyboard" ref="hPKeyboard"/>
    </bean>

    <bean id="dellMouse" class="di.DellMouse"/>

    <bean id="hPKeyboard" class="di.HPKeyboard"/>
</beans>

测试代码:

BeanFactory beanFactory = new MyBeanFactory(configPath);
User user = (User) beanFactory.getBean("user");
user.use();

我们在第二行打断点,查看 BeanFactory 持有的对象情况:

BeanFactory 持有情况

可以看到,工厂持有的 User 被注入了两个属性,且来自工厂本身(@943、@941)。

运行结果:

result2

结构

MyBeanFactory

控制被反转?

我们把创建对象比作炒菜,原先我们需要自己掌勺,亲力亲为,现在厨师(BeanFactory)帮我们把菜做好了,你尽管享用就行。

我们把抽象工厂模式和 DI 做下对比,发现抽象工厂在增加产品线时,需要修改代码,而依赖注入模式中的 BeanFactory 持有的 beans 是来源于配置文件的(还有扫描等非侵入的方式),程序端在使用时是感觉不到 Factories 的存在的(如 User),这样如果要再增加产品线,只需要在增加产品线定义(接口)和相应产品(实现类)后,增加配置文件即可(自动装配则更简单)。也就是说,原先需要我在代码中主动关注的“从哪个工厂拿对象”这件事,现在我不需要关心了,IOC 工厂都帮我都做好了,我要做的只是告诉工厂:“我要个 Mouse,要个 Keyboard,注入的话你用我暴露的 setter 吧”,原先掌控在我手里的“依赖获取方式”(即控制),现在委托给 BeanFactory 啦(即反转)。

Spring IoC 容器:

img

总结

本文实现了一个比较简单的 BeanFactory,还没有对资源等做抽象,依赖解析的过程也过于简单,实际上 Spring 等高级工厂的逻辑还有功能都更复杂强大的多。