本篇Blog继续学习创建型模式,创建型模式的主要关注点是怎样创建对象,它的主要特点是将对象的创建与使用分离,这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。本篇学习的是工厂方法模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:
- 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
- 模式结构:包含模式的结构,包含的角色定义及调用关系
- 模式实现:包含模式的实现方式代码举例或者生活中简单问题映射代码举例
- 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
- 模式对比:如果模式相似或模式有额外的替换方法,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
- 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构;对模式的思考需要进行发散等。
接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构,本文的模式实践案例大多来自极客时间。
模式档案
在日常开发中,凡是需要生成复杂对象的地方,都可以尝试考虑使用工厂模式来代替。复杂对象指的是类的构造函数参数过多等对类的构造有影响的情况,因为类的构造过于复杂,如果直接在其他业务类内使用,则两者的耦合过重,后续业务更改,就需要在任何引用该类的源代码内进行更改,光是查找所有依赖就很消耗时间了,更别说要一个一个修改了
模式定义:工厂方法模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂方法模式中,创建对象不会对调用者暴露创建逻辑,而是通过使用接口来指向新创建的对象。
模式特点:工厂方法模式的主要特点是:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂方法模式使其创建过程延迟到子类进行,在编写工厂类时,不需要知道实际创建的产品类是哪一个,而是选择了哪个具体的工厂类就决定了实际创建的产品类是哪一个。
解决什么问题:在面向对象编程中,最常用的创建对象的方法是通过new操作符构造一个对象实例。但是在一些情况下, new操作符直接生成对象会带来一些问题。举例来说,许多类型对象的创造需要一系列的步骤: 可能需要计算获取对象的初始设置; 可能需要选择创建哪个对象的子对象实例; 可能在创建需要的对象前必须先创建一些辅助功能的对象。 在这些情况对象的创建就是一个 【过程】,不仅是一个【操作】,像一部大机器中的一个个齿轮传动,使调用者能轻松方便地构造对象实例,而不必关心构造对象实例的细节和复杂过程
优点: 一个调用者想创建一个对象,只要知道其名称就可以了,细节不必关注; 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以;屏蔽产品的具体实现,调用者只关心产品的接口,具体来说:
- 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
- 代码复用:创建代码抽离到独立的工厂类之后可以复用。
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
- 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖
使用场景: 当一种产品实现有多种不同的呈现方式或表现形式时。例如:日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口
模式结构
相对于简单的工厂模式,它的特点就是:有一个抽象产品类和多个具体的产品类,有一个抽象的工厂类和多个具体的工厂类,每个具体工厂类只能创建一个具体产品类的实例。为什么需要工厂方法模式呢?静态工厂方法模式不够用么?
我们从开闭原则(对扩展开放,对修改封闭)上来分析下简单工厂模式。当我们想要对产品进行扩展时。对产品部分来说,它是符合开闭原则的,只需再增加一个新的具体产品类即可;但是工厂部分好像不太理想,因为每增加一个产品,都要在工厂类中增加相应的创建业务逻辑并使这个工厂类越来越大,越来越难以维护,同时各个产品类之间也并不能很好的隔离。这显然是违背开闭原则的。所以对于新产品的加入,工厂类是很被动的。所以我们需要进一步解耦
我们需要将工厂类也进行抽象。相比于简单工厂模式,工厂方法模式将对产品的创建方法进行抽象,它可以被子类继承。这样在简单工厂模式里集中在工厂方法上的压力可以由工厂方法模式里不同的工厂子类来分担 ,
- 抽象工厂角色: 这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在Java中它由抽象类或接口来实现。
- 具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。
- 抽象产品角色:它是具体产品继承的父类或者是实现的接口。在Java中一般由抽象类或者接口来实现。
- 具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在Java中由具体的类来实现。
需要注意的是工厂方法本身的重点不是解决if else
而是解决简单工厂的开闭原则,实际上工厂方法本身也会有if else
问题
模式实现
我们来看依据上边的工厂方法结构图各类角色如何配合实现:
抽象产品角色
//抽象产品:提供了产品的接口 interface Product { public void show(); }
具体产品角色
//具体产品1:实现抽象产品中的抽象方法 class ConcreteProduct1 implements Product { public void show() { System.out.println("具体产品1显示..."); } } //具体产品2:实现抽象产品中的抽象方法 class ConcreteProduct2 implements Product { public void show() { System.out.println("具体产品2显示..."); } }
抽象工厂角色
//抽象工厂:提供了厂品的生成方法 interface AbstractFactory { public Product newProduct(); }
具体工厂角色
//具体工厂1:实现了厂品的生成方法 class ConcreteFactory1 implements AbstractFactory { public Product newProduct() { System.out.println("具体工厂1生成-->具体产品1..."); return new ConcreteProduct1(); } } //具体工厂2:实现了厂品的生成方法 class ConcreteFactory2 implements AbstractFactory { public Product newProduct() { System.out.println("具体工厂2生成-->具体产品2..."); return new ConcreteProduct2(); } }
客户端调用如下:
public class AbstractFactoryTest { public static void main(String[] args) { try { AbstractFactory abstractFactory = new ConcreteFactory1(); Product product = abstractFactory .newProduct(); product .show(); } catch (Exception e) { System.out.println(e.getMessage()); } } }
打印结果如下:
具体工厂1生成-->具体产品1... 具体产品1显示...
模式实践
接下来我们看两个工厂模式的具体实践:文件解析工厂和组织处理工厂。
设计一个可扩展文件解析工厂
继续简单工厂的文件解析工厂例子,我们来看看工厂方法如何实现更加符合开闭原则,将其打造为一个可扩展文件解析工厂。划分角色来看:
抽象产品角色
package com.example.designpattern.factory; public interface IRuleConfigParser{ void parse(); }
具体产品角色
public class JsonRuleConfigParser implements IRuleConfigParser { @Override public void parse() { System.out.println("JsonRuleConfigParser parse "); } } public class XmlRuleConfigParser implements IRuleConfigParser { @Override public void parse() { System.out.println("XmlRuleConfigParser parse"); } } public class YamlRuleConfigParser implements IRuleConfigParser { @Override public void parse() { System.out.println("YamlRuleConfigParser parse"); } } public class PropertiesRuleConfigParser implements IRuleConfigParser { @Override public void parse() { System.out.println("PropertiesRuleConfigParser parse"); } }
抽象工厂角色
package com.example.designpattern.factory; public interface IRuleConfigParserFactory { IRuleConfigParser createParser(); }
具体工厂角色
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new JsonRuleConfigParser(); } } public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new XmlRuleConfigParser(); } } public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new YamlRuleConfigParser(); } } public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory { @Override public IRuleConfigParser createParser() { return new PropertiesRuleConfigParser(); } }
通过工厂类的多态保证在工厂类写的时候避免分支逻辑,这样当我们新增一种 parser 的时候,只需要新增一个实现了 IRuleConfigParserFactory
接口的 Factory
类即可。所以,工厂方法模式比起简单工厂模式更加符合开闭原则。从上面的工厂方法的实现来看,一切都很完美,但是实际上存在挺大的问题。问题存在于这些工厂类的使用上。接下来,我们看一下,如何用这些工厂类来实现 RuleConfigSource 的 load() 函数。具体的代码如下所示:
public class RuleConfigSource { 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: " + ruleConfigFilePath); } 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: " + ruleConfigFilePath); } 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 Map<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 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。实际上,对于规则配置文件解析这个应用场景来说,工厂模式需要额外创建诸多 Factory 类,也会增加代码的复杂性,而且,每个 Factory 类只是做简单的 new 操作,功能非常单薄(只有一行代码),也没必要设计成独立的类,所以,在这个应用场景下,简单工厂模式简单好用,比工厂方法模式更加合适
模式对比
结合上述文件解析工厂的示例,我们来比较下哪些情况下简单工厂更合适,哪些场景下工厂方法更合适?
简单工厂模式与工厂方法模式
当创建逻辑比较复杂,是一个大工程的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。何为创建逻辑比较复杂呢?
- 第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。
- 当每个对象的创建逻辑都比较简单的时候,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。
- 当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。注意这里只是为了避免简单工厂类的庞大,并不能避免分支逻辑,工厂方法模式也会存在分支逻辑。
- 第二种情况,尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下,我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。当然也可以考虑使用建造者模式处理
- 对于这种情况:因为单个对象本身的创建逻辑就比较复杂,所以建议使用工厂方法模式
除了刚刚提到的这几种情况之外,如果创建对象的逻辑并不复杂,那我们就直接通过 new 来创建对象就可以了,不需要使用工厂模式。
总结一下
其实工厂模式应用非常广泛,工作中经常会遇到,因为面向接口编程,在很多场景下会有多个类拥有同一种行为的情况,无论这种行为来自实现接口还是继承父类。但是它们各自又有不同的实现内容,也就是多态。在使用这些类进行某种行为时我们希望不关心创建过程直接获取自己需要的类并进行个性化行为,此时工厂方法模式就是最佳选择,工厂方法模式可以让我们的客户端调用代码与被调用对象的创建解耦,无论被调用对象的创建过程如何变化,客户端调用代码都无需改变。其实大多数情况下简单工厂就够用了,只有当类的创建过程复杂、待创建类的种类非常多对工厂扩展不利时才考虑工厂方法模式。