设计模式-3章(工场模式)


第三章 工场模式

工厂模式(Factory Pattern)是一种创建型设计模式,用于对象的创建和实例化。它提供了一种将对象的实例化过程封装在一个单独的方法或类中的方式,以便在应用程序中更灵活地创建对象,而不必直接调用构造函数。工厂模式有多种变体,其中最常见的包括简单工厂模式、工厂方法模式和抽象工厂模式。

以下是关于工厂模式的详细解释:

  1. 简单工厂模式(Simple Factory Pattern)
    简单工厂模式是最基本的工厂模式,它由一个工厂类负责创建其他类的对象。在这种模式中,客户端代码不需要直接实例化对象,而是通过调用工厂类的静态方法来创建对象。这个静态方法根据传入的参数或条件,决定创建哪个具体的对象。

    示例:一个简单工厂可以根据用户的输入创建不同类型的汽车对象,如小轿车、卡车或SUV。

    注意:简单工场模式并不属于23种设计模式之一。

  2. 工厂方法模式(Factory Method Pattern)
    工厂方法模式定义了一个接口或抽象类,其中包含一个工厂方法,该方法由具体的子类实现。每个具体的子类负责创建一种特定类型的对象。这样,客户端可以通过调用工厂方法来创建对象,而无需关心具体的实现细节。

    示例:一个图形绘制应用程序可以定义一个抽象图形工厂,具体子类可以是矩形工厂、圆形工厂等,每个工厂都负责创建相应的图形对象。

    工场方法让类的实例化推迟到了子类中进行。

  3. 抽象工厂模式(Abstract Factory Pattern)
    抽象工厂模式提供了一种创建一组相关或相互依赖对象的方式,而不需要指定其具体类。它包括多个工厂方法,每个工厂方法用于创建一组相关的对象。这种模式通常用于创建产品族,其中不同工厂可以创建不同变体的产品。

    示例:一个UI界面库可以使用抽象工厂模式来创建不同风格的按钮、文本框和窗口,如Windows风格、Mac风格或Linux风格。

工厂模式的优点包括:

  • 将对象的创建和使用分离,使代码更灵活、可维护和可扩展。
  • 可以隐藏具体类的实现细节,使客户端代码更加独立于具体类。
  • 可以根据需要轻松添加新的具体类,而无需修改客户端代码。

然而,工厂模式也有一些缺点:

  • 增加了额外的类和层次结构,可能会导致代码更加复杂。
  • 如果工厂方法不合理地设计,可能会导致代码重复或难以维护。

在选择使用工厂模式时,需要考虑应用程序的具体需求和设计目标,以确定哪种工厂模式最适合。

1. 简单工场模式

首先,我们来看,什么是简单工厂模式。我们通过一个例子来解释一下。

在下面这段代码中,我们根据配置文件的后缀(json、xml、yaml、properties),选择不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),将存储在文件中的配置解析成内存对象 RuleConfig。

public class RuleConfigSource {
    public RuleConfig load(String ruleConfigFilePath) {
        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = null;
        if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new JsonRuleConfigParser();
        } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new XmlRuleConfigParser();
        } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new YamlRuleConfigParser();
        } else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
            parser = new PropertiesRuleConfigParser();
        } else {
            throw new InvalidRuleConfigException(
                    "Rule config file format is not supported: " + ruleConfigFilePath)
        }
        String configText = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }

    private String getFileExtension(String filePath) {
        //...解析文件名获取扩展名,比如rule.json,返回json
        return "json";
    }
}

为了让代码逻辑更加清晰,可读性更好,我们要善于将功能独立的代码块封装成函数。按照这个设计思路,我们可以将代码中涉及parser 创建的部分逻辑剥离出来,抽象成 createParser() 函数。重构之后的代码如下所示:

public class RuleConfigSource {
    public RuleConfig load(String ruleConfigFilePath) {
        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = createParser(ruleConfigFileExtension); //方法抽离
        if (parser == null) {
            throw new InvalidRuleConfigException(
                    "Rule config file format is not supported: " + ruleConfigFilePath);
        }
        String configText = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }

    private String getFileExtension(String filePath) {
        //...解析文件名获取扩展名,比如rule.json,返回json
        return "json";
    }

    private IRuleConfigParser createParser(String configFormat) {
        IRuleConfigParser parser = null;
        if ("json".equalsIgnoreCase(configFormat)) {
            parser = new JsonRuleConfigParser();
        } else if ("xml".equalsIgnoreCase(configFormat)) {
            parser = new XmlRuleConfigParser();
        } else if ("yaml".equalsIgnoreCase(configFormat)) {
            parser = new YamlRuleConfigParser();
        } else if ("properties".equalsIgnoreCase(configFormat)) {
            parser = new PropertiesRuleConfigParser();
        }
        return parser;
    }
}

为了让类的职责更加单一、代码更加清晰,我们还可以进一步将 createParser() 函数剥离到一个独立的类中,让这个类只负责对象的创建。而这个类就是我们现在要讲的简单工厂模式类。具体的代码如下所示:

public class RuleConfigSource {
    public RuleConfig load(String ruleConfigFilePath) {
        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfig); //使用简单工场来创建
        if (parser == null) {
            throw new InvalidRuleConfigException(
                    "Rule config file format is not supported: " + ruleConfigFilePath);
        }
        String configText = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }

    private String getFileExtension(String filePath) {
        //...解析文件名获取扩展名,比如rule.json,返回json
        return "json";
    }
}

public class RuleConfigParserFactory {
    public static IRuleConfigParser createParser(String configFormat) {
        IRuleConfigParser parser = null;
        if ("json".equalsIgnoreCase(configFormat)) {
            parser = new JsonRuleConfigParser();
        } else if ("xml".equalsIgnoreCase(configFormat)) {
            parser = new XmlRuleConfigParser();
        } else if ("yaml".equalsIgnoreCase(configFormat)) {
            parser = new YamlRuleConfigParser();
        } else if ("properties".equalsIgnoreCase(configFormat)) {
            parser = new PropertiesRuleConfigParser();
        }
        return parser;
    }
}

大部分工厂类都是以“Factory”这个单词结尾的,但也不是必须的,比如 Java 中的DateFormatCalender。除此之外,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()createInstance()newInstance(),有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等,这个我们根据具体的场景和习惯来命名就好。

在上面的代码实现中,我们每次调用 RuleConfigParserFactory createParser() 的时候,都要创建一个新的 parser。实际上,如果 parser 可以复用,为了节省内存和对象创建的时间,我们可以将 parser 事先创建好缓存起来。当调用 createParser() 函数的时候,我们从缓存中取出 parser 对象直接使用。

这有点类似单例模式和简单工厂模式的结合,具体的代码实现如下所示。我们把上一种实现方法叫作简单工厂模式的第一种实现方法,把下面这种实现方法叫作简单工厂模式的第二种实现方法

public class RuleConfigParserFactory {
    private static final Map<String, RuleConfigParser> cachedParsers = new HashMa

    static {
        cachedParsers.put("json", new JsonRuleConfigParser());
        cachedParsers.put("xml", new XmlRuleConfigParser());
        cachedParsers.put("yaml", new YamlRuleConfigParser());
        cachedParsers.put("properties", new PropertiesRuleConfigParser());
    }

    public static IRuleConfigParser createParser(String configFormat) {
        if (configFormat == null || configFormat.isEmpty()) {
            return null;//返回null还是IllegalArgumentException全凭你自己说了算
        }
        IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
        return parser;
    }
}

对于上面两种简单工厂模式的实现方法,如果我们要添加新的 parser,那势必要改动到RuleConfigParserFactory 的代码,那这是不是违反开闭原则呢?实际上,如果不是需要频繁地添加新的 parser,只是偶尔修改一下 RuleConfigParserFactory 代码,稍微不符合开闭原则,也是完全可以接受的。

除此之外,在 RuleConfigParserFactory 的第一种代码实现中,有一组 if 分支判断逻辑,是不是应该用多态或其他设计模式来替代呢?实际上,如果 if 分支并不是很多,代码中有if 分支也是完全可以接受的。应用多态或设计模式来替代 if 分支判断逻辑,也并不是没有任何缺点的,它虽然提高了代码的扩展性,更加符合开闭原则,但也增加了类的个数,牺牲了代码的可读性。

总结一下:尽管简单工厂模式的代码实现中,有多处 if 分支判断逻辑,违背开闭原则,但权衡扩展性和可读性,这样的代码实现在大多数情况下(比如,不需要频繁地添加parser,也没有太多的 parser)是没有问题的。

简单工场模式从形式上看就是很多if分支,然后根据不同的类型创建不同的对象

2. 工厂方法模式

如果我们非得要将 if 分支逻辑去掉,那该怎么办呢?比较经典处理方法就是利用多态。按照多态的实现思路,对上面的代码进行重构。重构之后的代码如下所示:

抽象规则为接口:

public interface IRuleConfigParserFactory {
    IRuleConfigParser createParser();
}

为每一个具体的规则来实现这个接口:

public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
    @Override
    public IRuleConfigParser createParser() {
        return new JsonRuleConfigParser();
    }
}

public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory{
        @Override
        public IRuleConfigParser createParser() {
            return new PropertiesRuleConfigParser();
        }
}

public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
    @Override
    public IRuleConfigParser createParser() {
        return new YamlRuleConfigParser();
    }
}

public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
    @Override
    public IRuleConfigParser createParser() {
        return new XmlRuleConfigParser();
    }
}

实际上,这就是工厂方法模式的典型代码实现。这样当我们新增一种 parser 的时候,只需要新增一个实现了 IRuleConfigParserFactory 接口的 Factory 类即可。所以,工厂方法模式比起简单工厂模式更加符合开闭原则

从上面的工厂方法的实现来看,一切都很完美,但是实际上存在挺大的问题。问题存在于这些工厂类的使用上。接下来,我们看一下,如何用这些工厂类来实现 RuleConfigSource的 load() 函数。具体的代码如下所示:

public class RuleConfigSource {
    //load方法
    public RuleConfig load(String ruleConfigFilePath) {
        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        
        IRuleConfigParserFactory parserFactory = null;
        if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
            parserFactory = new JsonRuleConfigParserFactory(); //只是把原来的具体的类改为了工场,没有实质改变
        } else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
            parserFactory = new XmlRuleConfigParserFactory();//只是把原来的具体的类改为了工场,没有实质改变
        } else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
            parserFactory = new YamlRuleConfigParserFactory();//只是把原来的具体的类改为了工场,没有实质改变
        } else if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
            parserFactory = new PropertiesRuleConfigParserFactory();//只是把原来的具体的类改为了工场,没有实质改变
        } else {
            throw new InvalidRuleConfigException("Rule config file format is not supported");
        }
        
        IRuleConfigParser parser = parserFactory.createParser();
        String configText = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }

    private String getFileExtension(String filePath) {
        //...解析文件名获取扩展名,比如rule.json,返回json
        return "json";
    }
}

从上面的代码实现来看,工厂类对象的创建逻辑又耦合进了 load() 函数中,跟我们最初的代码版本非常相似,引入工厂方法非但没有解决问题,反倒让设计变得更加复杂了。那怎么来解决这个问题呢?

我们可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。这段话听起来有点绕,我把代码实现出来了,你一看就能明白了。其中,
RuleConfigParserFactoryMap 类是创建工厂对象的工厂类,getParserFactory() 返回的是缓存好的单例工厂对象

public class RuleConfigSource {
    public RuleConfig load(String ruleConfigFilePath) {
        String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
        IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
        if (parserFactory == null) {
            throw new InvalidRuleConfigException("Rule config file format is not supported");
        }
        IRuleConfigParser parser = parserFactory.createParser();
        String configText = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中
        RuleConfig ruleConfig = parser.parse(configText);
        return ruleConfig;
    }

    private String getFileExtension(String filePath) {
        //...解析文件名获取扩展名,比如rule.json,返回json
        return "json";
    }
}
//因为工厂类只包含方法,不包含成员变量,完全可以复用,
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
public class RuleConfigParserFactoryMap { //工厂的工厂
    private static final HashMap<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();

    static {
        cachedFactories.put("json", new JsonRuleConfigParserFactory());
        cachedFactories.put("xml", new XmlRuleConfigParserFactory());
        cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
        cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
    }

    public static IRuleConfigParserFactory getParserFactory(String type) {
        if (type == null || type.isEmpty()) {
            return null;
        }
        IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
        return parserFactory;
    }
}

当我们需要添加新的规则配置解析器的时候,我们只需要创建新的 parser 类和 parserfactory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。

实际上,对于规则配置文件解析这个应用场景来说,工厂模式需要额外创建诸多 Factory类,也会增加代码的复杂性,而且,每个 Factory 类只是做简单的 new 操作,功能非常单薄(只有一行代码),也没必要设计成独立的类,所以,在这个应用场景下,简单工厂模式简单好用,比工方法厂模式更加合适。

总结:

工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它解决了简单工厂模式中的一些限制,并提供了更高的灵活性和可扩展性。工厂方法模式的核心思想是将对象的创建延迟到子类中去实现,每个具体子类都负责创建自己特定类型的对象。以下是工厂方法模式的详细解释和特点:

主要组成部分:

  1. 抽象工厂(Abstract Factory): 这是一个抽象类或接口,它定义了一个工厂方法的接口,用于创建产品对象。抽象工厂通常包含多个工厂方法,每个方法对应一个具体的产品类型。

  2. 具体工厂(Concrete Factory): 具体工厂是抽象工厂的子类或实现类,它实现了工厂方法,负责创建具体的产品对象。每个具体工厂通常与一种具体产品相关联。

  3. 抽象产品(Abstract Product): 这是一个抽象类或接口,定义了产品对象的通用接口。具体的产品类必须实现这个接口。

  4. 具体产品(Concrete Product): 具体产品是抽象产品的子类,它实现了抽象产品定义的接口,提供了具体的产品功能。

工作流程:

  1. 客户端需要一个产品对象,但不直接实例化产品类。客户端调用工厂方法来获取产品对象。

  2. 具体工厂类负责实现工厂方法,根据客户端的请求创建相应的具体产品对象。

  3. 客户端只需要知道所需的产品接口,而不需要关心具体的产品类。这样,客户端代码与具体产品的实现解耦。

假设我们有不同类型的汽车和相应的工厂类来创建这些汽车:

// 抽象产品类
interface Car {
    void drive();
}

// 具体产品类 A
class SedanCar implements Car {
    @Override
    public void drive() {
        System.out.println("Driving a Sedan car");
    }
}

// 具体产品类 B
class SUVCar implements Car {
    @Override
    public void drive() {
        System.out.println("Driving an SUV car");
    }
}

// 抽象工厂接口 用户无需知道具体的产品实现,只需要知道汽车工场就可以了
interface CarFactory {
    Car createCar();
}

// 具体工厂类 A
class SedanCarFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new SedanCar();
    }
}

// 具体工厂类 B
class SUVCarFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new SUVCar();
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 创建具体工厂对象
        CarFactory sedanFactory = new SedanCarFactory();
        CarFactory suvFactory = new SUVCarFactory();

        // 使用工厂方法创建不同类型的汽车
        Car sedan = sedanFactory.createCar();
        Car suv = suvFactory.createCar();

        // 驾驶汽车
        sedan.drive(); // 输出:Driving a Sedan car
        suv.drive();   // 输出:Driving an SUV car
    }
}

这个例子的代码就是工场方法的标准实现。

2.1 那什么时候该用工厂方法模式,而非简单工厂模式呢?

我们前面提到,之所以将某个代码块剥离出来,独立为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。但是,如果代码块本身并不复杂,就几行代码而已,我们完全没必要将它拆分成单独的函数或者类。

基于这个设计思想,当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。

除此之外,在某些场景下,如果对象不可复用,那工厂类每次都要返回不同的对象。如果我们使用简单工厂模式来实现,就只能选择第一种包含 if 分支逻辑的实现方式。如果我们还想避免烦人的 if-else 分支逻辑,这个时候,我们就推荐使用工厂方法模式。

3. 抽象工厂模式

讲完了简单工厂、工厂方法,我们再来看抽象工厂模式。抽象工厂模式的应用场景比较特殊,没有前两种常用,所以不是我们本节课学习的重点,你简单了解一下就可以了。

在简单工厂和工厂方法中,类只有一种分类方式。比如,在规则配置解析那个例子中,解析器类只会根据配置文件格式(Json、Xml、Yaml……)来分类。但是,如果类有两种分类方式,比如,我们既可以按照配置文件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,那就会对应下面这 8 个 parser 类。

针对规则配置的解析器:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser

针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser

针对这种特殊的场景,如果还是继续用工厂方法来实现的话,我们要针对每个 parser 都编写一个工厂类,也就是要编写 8 个工厂类。如果我们未来还需要增加针对业务配置的解析器(比如 IBizConfigParser),那就要再对应地增加 4 个工厂类。而我们知道,过多的类也会让系统难维护。这个问题该怎么解决呢?

抽象工厂就是针对这种非常特殊的场景而诞生的。我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser对象。这样就可以有效地减少工厂类的个数。具体的代码实现如下所示:

public interface IConfigParserFactory {
    IRuleConfigParser createRuleParser();

    ISystemConfigParser createSystemParser();
    //此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
    @Override
    public IRuleConfigParser createRuleParser() {
        return new JsonRuleConfigParser();
    }

    @Override
    public ISystemConfigParser createSystemParser() {
        return new JsonSystemConfigParser();
    }
}
public class XmlConfigParserFactory implements IConfigParserFactory {
    @Override
    public IRuleConfigParser createRuleParser() {
        return new XmlRuleConfigParser();
    }

    @Override
    public ISystemConfigParser createSystemParser() {
        return new XmlSystemConfigParser();
    }
}

省略YamlConfigParserFactory和PropertiesConfigParserFactory代码

总结:

抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,它提供了一种创建一系列相关或依赖对象的接口,而无需指定它们的具体类。抽象工厂模式是工厂方法模式的扩展,它通过引入多个相关工厂方法来创建一组对象,这些对象之间有关联,形成一个产品族。以下是抽象工厂模式的详细解释和特点:

主要组成部分:

  1. 抽象工厂(Abstract Factory): 这是一个抽象类或接口,它定义了一组创建产品对象的抽象方法。通常,每个抽象方法对应一个产品族的创建。

  2. 具体工厂(Concrete Factory): 具体工厂是抽象工厂的子类或实现类,它实现了抽象工厂中定义的抽象方法,负责创建一组相关的产品对象。

  3. 抽象产品(Abstract Product): 这是一个抽象类或接口,定义了产品对象的通用接口。每个抽象产品类对应一个产品族中的产品。

  4. 具体产品(Concrete Product): 具体产品是抽象产品的子类,它实现了抽象产品定义的接口,提供了具体的产品功能。

工作流程:

  1. 客户端需要一组相关的产品对象,但不直接实例化产品类。客户端使用抽象工厂接口来创建产品对象。

  2. 每个具体工厂类负责实现抽象工厂接口中的方法,创建一组相关的产品对象,这些产品对象属于同一产品族。

  3. 客户端代码只需要知道所需的抽象工厂和抽象产品,而不需要关心具体工厂和具体产品的创建细节。

假设我们有不同品牌的手机和相应的配件:

// 抽象产品接口 - 手机
interface Phone {
    void makeCall();
}

// 具体产品类 - 苹果手机
class IPhone implements Phone {
    @Override
    public void makeCall() {
        System.out.println("Making a call with iPhone");
    }
}

// 具体产品类 - 三星手机
class SamsungPhone implements Phone {
    @Override
    public void makeCall() {
        System.out.println("Making a call with Samsung phone");
    }
}

// 抽象产品接口 - 配件
interface Accessory {
    void use();
}

// 具体产品类 - 苹果配件
class IPhoneAccessory implements Accessory {
    @Override
    public void use() {
        System.out.println("Using an accessory for iPhone");
    }
}

// 具体产品类 - 三星配件
class SamsungAccessory implements Accessory {
    @Override
    public void use() {
        System.out.println("Using an accessory for Samsung phone");
    }
}

// 抽象工厂接口
interface PhoneFactory {
    Phone createPhone();
    Accessory createAccessory();
}

// 具体工厂类 - 苹果手机工厂
class AppleFactory implements PhoneFactory {
    @Override
    public Phone createPhone() {
        return new IPhone();
    }

    @Override
    public Accessory createAccessory() {
        return new IPhoneAccessory();
    }
}

// 具体工厂类 - 三星手机工厂
class SamsungFactory implements PhoneFactory {
    @Override
    public Phone createPhone() {
        return new SamsungPhone();
    }

    @Override
    public Accessory createAccessory() {
        return new SamsungAccessory();
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 使用具体工厂创建苹果手机及其配件
        PhoneFactory appleFactory = new AppleFactory();
        Phone applePhone = appleFactory.createPhone();
        Accessory appleAccessory = appleFactory.createAccessory();

        // 使用具体工厂创建三星手机及其配件
        PhoneFactory samsungFactory = new SamsungFactory();
        Phone samsungPhone = samsungFactory.createPhone();
        Accessory samsungAccessory = samsungFactory.createAccessory();

        // 使用手机和配件
        applePhone.makeCall();
        appleAccessory.use();
        samsungPhone.makeCall();
        samsungAccessory.use();
    }
}

这个例子的代码就是抽象工场的标准实现。


前三节我们讲到,当创建对象是一个“大工程”的时候,我们一般会选择使用工厂模式,来封装对象复杂的创建过程,将对象的创建和使用分离,让代码更加清晰。那何为“大工程”呢?一种是创建过程涉及复杂的 if-else 分支判断,另一种是对象创建需要组装多个其他类对象或者需要复杂的初始化过程。

今天,我们再来讲一个创建对象的“大工程”,依赖注入框架,或者叫依赖注入容器(Dependency Injection Container),简称 DI 容器。在今天的讲解中,我会带你一块搞清楚这样几个问题:

  • DI 容器跟我们讲的工厂模式又有何区别和联系?
  • DI 容器的核心功能有哪些?
  • 如何实现一个简单的 DI 容器?

4. 工厂模式和 DI 容器有何区别?

实际上,DI 容器底层最基本的设计思路就是基于工厂模式的。DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为“容器”。

DI 容器相对于我们讲的工厂模式的例子来说,它处理的是更大的对象创建工程。工厂模式中,一个工厂类只负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建,而 DI 容器负责的是整个应用中所有类对象的创建。

除此之外,DI 容器负责的事情要比单纯的工厂模式要多。比如,它还包括配置的解析、对象生命周期的管理。接下来,我们就详细讲讲,一个简单的 DI 容器应该包含哪些核心功能。

5. DI 容器的核心功能有哪些?

总结一下,一个简单的 DI 容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。

5.1 配置解析。

首先,我们来看配置解析。

在前几节讲的工厂模式中,工厂类要创建哪个类对象是事先确定好的,并且是写死在工厂类代码中的。作为一个通用的框架来说,框架代码跟应用代码应该是高度解耦的,DI 容器事先并不知道应用会创建哪些对象,不可能把某个应用要创建的对象写死在框架代码中。所以,我们需要通过一种形式,让应用告知 DI 容器要创建哪些对象。这种形式就是我们要讲的配置

我们将需要由 DI 容器来创建的类对象和创建类对象的必要信息(使用哪个构造函数以及对应的构造函数参数都是什么等等),放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。

下面是一个典型的 Spring 容器的配置文件。Spring 容器读取这个配置文件,解析出要创建的两个对象:rateLimiter redisCounter,并且得到两者的依赖关系:rateLimiter 依赖 redisCounter

redisCounter:

public class RedisCounter {
    private String ipAddress;
    private int port;

    public RedisCounter(String ipAddress, int port) {
        this.ipAddress = ipAddress;
        this.port = port;
    }
    //...
}

rateLimiter:

public class RateLimiter {
    private RedisCounter redisCounter;

    public RateLimiter(RedisCounter redisCounter) {
        this.redisCounter = redisCounter;
    }

    public void test() {
        System.out.println("Hello World!");
    }
    //...
}

配置文件beans.xml:

<beans>
    <bean id="rateLimiter" class="com.nxz.RateLimiter">
        <constructor-arg ref="redisCounter"/>
    </bean>
    <bean id="redisCounter" class="com.nxz.redisCounter">
        <constructor-arg type="String" value="127.0.0.1">
        <constructor-arg type="int" value=1234>
    </bean>
</beans>

5.2 对象创建

其次,我们再来看对象创建。

在 DI 容器中,如果我们给每个类都对应创建一个工厂类,那项目中类的个数会成倍增加,这会增加代码的维护成本。要解决这个问题并不难。我们只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如 BeansFactory

你可能会说,如果要创建的类对象非常多,BeansFactory 中的代码会不会线性膨胀(代码量跟创建对象的个数成正比)呢?实际上并不会。待会讲到 DI 容器的具体实现的时候,我们会讲“反射”这种机制,它能在程序运行的过程中,动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象。所以,不管是创建一个对象还是十个对象,BeansFactory 工厂类代码都是一样的。

5.3 声明周期管理

最后,我们来看对象的生命周期管理。

我们讲到,简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是所谓的单例对象。在 Spring 框架中,我们可以通过配置 scope 属性,来区分这两种不同类型的对象。scope=prototype 表示返回新创建的对象,scope=singleton 表示返回单例对象。

除此之外,我们还可以配置对象是否支持懒加载。如果 lazy-init=true,对象在真正被使用到的时候(比如:BeansFactory.getBean(“userService”))才被被创建;如果 lazy-init=false,对象在应用启动的时候就事先创建好。

不仅如此,我们还可以配置对象的 init-methoddestroy-method 方法,比如 init-method=loadProperties(),destroy-method=updateConfigFile()。DI 容器在创建好对象之后,会主动调用 init-method 属性指定的方法来初始化对象。在对象被最终销毁之前,DI 容器会主动调用 destroy-method 属性指定的方法来做一些清理工作,比如释放数据库连接池、关闭文件。

6. 如何实现一个简单的 DI 容器?

实际上,用 Java 语言来实现一个简单的 DI 容器,核心逻辑只需要包括这样两个部分:配置文件解析、根据配置文件通过“反射”语法来创建对象。

6.1 最小原型设计

因为我们主要是讲解设计模式,所以,我们只实现一个 DI 容器的最小原型。像 Spring 框架这样的 DI 容器,它支持的配置格式非常灵活和复杂。为了简化代码实现,重点讲解原理,在最小原型中,我们只支持下面配置文件中涉及的配置语法。

配置文件beans.xml:

<beans>
    <bean id="rateLimiter" class="com.nxz.RateLimiter">
        <constructor-arg ref="redisCounter"/>
    </bean>
    <bean id="redisCounter" class="com.nxz.redisCounter" scope="singleton" lazy-init=false>
        <constructor-arg type="String" value="127.0.0.1">
        <constructor-arg type="int" value=1234>
    </bean>
</beans>

最小原型的使用方式跟 Spring 框架非常类似,示例代码如下所示:

public class Demo {
    public static void main(String[] args) {
        //获取XML配置文件信息
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
        //获取RateLimiter对象
        RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
        rateLimiter.test();
        //...
    }
}

6.2 提供执行入口

在这里,执行入口就是一组暴露给外部使用的接口和类。

通过刚刚的最小原型使用示例代码,我们可以看出,执行入口主要包含两部分:ApplicationContextClassPathXmlApplicationContext。其中,ApplicationContext是接口,ClassPathXmlApplicationContext 是接口的实现类。两个类具体实现如下所示:

注意:这是我自己实现的,并不是Spring的源码。

public interface ApplicationContext {
    Object getBean(String beanId);
}
public class ClassPathXmlApplicationContext implements ApplicationContext {
    private BeansFactory beansFactory;
    private BeanConfigParser beanConfigParser;

    public ClassPathXmlApplicationContext(String configLocation) {
        this.beansFactory = new BeansFactory();
        this.beanConfigParser = new XmlBeanConfigParser();//xml转换
        loadBeanDefinitions(configLocation);
    }

    private void loadBeanDefinitions(String configLocation) {
        InputStream in = null;
        try {
            //反射获取
            in = this.getClass().getResourceAsStream("/" + configLocation);
            if (in == null) {
                throw new RuntimeException("Can not find config file: " + configLocation);
            }
            List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
            beansFactory.addBeanDefinitions(beanDefinitions);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // TODO: log error
                }
            }
        }
    }
    @Override
    public Object getBean(String beanId) {
        return beansFactory.getBean(beanId);
    }
}

从上面的代码中,我们可以看出,ClassPathXmlApplicationContext 负责组装BeansFactory BeanConfigParser 两个类,串联执行流程:从 classpath 中加载 XML格式的配置文件,通过 BeanConfigParser 解析为统一的 BeanDefinition 格式,然后,BeansFactory 根据 BeanDefinition 来创建对象。

6.3 配置文件解析

配置文件解析主要包含 BeanConfigParser 接口和 XmlBeanConfigParser 实现类,负责将配置文件解析为 BeanDefinition 结构,以便 BeansFactory 根据这个结构来创建对象。

配置文件的解析比较繁琐,不是我们讲解的重点,所以这里我只给出两个类的大致设计思路,并未给出具体的实现代码。如果感兴趣的话,你可以自行补充完整。具体的代码框架如下所示:

public interface BeanConfigParser {
    List<BeanDefinition> parse(InputStream inputStream);

    List<BeanDefinition> parse(String configContent);
}
public class XmlBeanConfigParser implements BeanConfigParser {
    @Override
    public List<BeanDefinition> parse(InputStream inputStream) {
        String content = null;
        // TODO:...
        return parse(content);
    }

    @Override
    public List<BeanDefinition> parse(String configContent) {
        List<BeanDefinition> beanDefinitions = new ArrayList<>();
        // TODO:...
        return beanDefinitions;
    }
}
public class BeanDefinition {
    private String id;
    private String className;
    private List<ConstructorArg> constructorArgs = new ArrayList<>();
    private Scope scope = Scope.SINGLETON;
    private boolean lazyInit = false;

    // 省略必要的getter/setter/constructors
    public boolean isSingleton() {
        return scope.equals(Scope.SINGLETON);
    }

    public static enum Scope {
        SINGLETON,
        PROTOTYPE
    }

    public static class ConstructorArg {
        private boolean isRef;
        private Class type;
        private Object arg;
        // 省略必要的getter/setter/constructors
    }
}

6.4 核心工场设计

最后,我们来看,BeansFactory 是如何设计和实现的。这也是我们这个 DI 容器最核心的一个类了。它负责根据从配置文件解析得到的 BeanDefinition 来创建对象。

如果对象的 scope 属性是 singleton,那对象创建之后会缓存在 singletonObjects 这样一个 map 中,下次再请求此对象的时候,直接从 map 中取出返回,不需要重新创建。如果对象的 scope 属性是 prototype,那每次请求对象,BeansFactory 都会创建一个新的对象返回。

实际上,BeansFactory 创建对象用到的主要技术点就是 Java 中的反射语法:一种动态加载类和创建对象的机制。我们知道,JVM 在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象,这些都是在代码中写死的,或者说提前写好的。

但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象,那这部分工作就没法让 JVM 帮我们自动完成了,我们需要利用 Java 提供的反射语法自己去编写代码。搞清楚了反射的原理,BeansFactory 的代码就不难看懂了。具体代码实现如下所示:

package com.nxz.designpattern.factory.simplefactory;

public class BeansFactory {
    private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
    private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new new ConcurrentHashMap<>();

    public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
        for (BeanDefinition beanDefinition : beanDefinitionList) {
            this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);
        }
        for (BeanDefinition beanDefinition : beanDefinitionList) {
            if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton())
                createBean(beanDefinition);
        }
    }

    public Object getBean(String beanId) {
        BeanDefinition beanDefinition = beanDefinitions.get(beanId);
        if (beanDefinition == null) {
            throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
        }
        return createBean(beanDefinition);
    }

    @VisibleForTesting
    protected Object createBean(BeanDefinition beanDefinition) {
        if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefinitions)) {
            return singletonObjects.get(beanDefinition.getId());
        }

        Object bean = null;
        try {
            Class beanClass = Class.forName(beanDefinition.getClassName());
            List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArg();
            if (args.isEmpty()) {
                bean = beanClass.newInstance();
            } else {
                Class[] argClasses = new Class[args.size()];
                Object[] argObjects = new Object[args.size()];
                for (int i = 0; i < args.size(); ++i) {
                    BeanDefinition.ConstructorArg arg = args.get(i);
                    if (!arg.getIsRef()) {
                        argClasses[i] = arg.getType();
                        argObjects[i] = arg.getArg();
                    } else {
                        BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
                        if (refBeanDefinition == null) {
                            throw new NoSuchBeanDefinitionException("Bean is not defined: " + refBeanDefinition);
                        }
                        argClasses[i] = Class.forName(refBeanDefinition.getClassName());
                        argObjects[i] = createBean(refBeanDefinition);
                    }
                }
                bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
            }
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTarget) {
            throw new BeanCreationFailureException("", e);
        }
        if (bean != null && beanDefinition.isSingleton()) {
            singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
            return singletonObjects.get(beanDefinition.getId());
        }
        return bean;
    }
}

文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录