本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
在之前单例模式的学习中,愈发的感觉到设计模式的精妙,设计模式就像是一种思想,能够很好的嵌入到自己的代码当中,写出更优雅更高效对的代码,本文讲述学习Java设计模式过程中的第二个设计模式——工厂方法模式
提示:以下是本篇文章正文内容,下面案例可供参考
一、举例说明工厂方法模式
东汉《风俗通》中记录了一则神话故事:“开天辟地,未有人民,女娲搏黄土做人”。讲述的是女娲造人的故事。女娲造人的过程是这样的:首先,女娲采集黄土捏成人的形状,然后方法八卦炉中烧制,最后放置到大地上生长,但是在制作的过程中意外随时都有可能发生:
- 第一次烤制泥人,感觉应该烤熟了,结果放到大地上时发现没有烤熟,于是一个白人就诞生了!
- 第二次烤制泥人,因为第一次没有烤熟,于是这一次多烤一会,结果放到大地上发现熟过头了,于是乎黑人就诞生了!
- 第三次烤制泥人,一边烧制一边查看,直到泥人表皮微黄,于是放到大地上,这一次烤制刚刚好,于是黄种人就出现了!
如何通过Java程序来实现女娲造人的过程呢?
在面向对象的思维中,万物皆对象,是对象我们就可以通过软件的设计来实现
1、程序分析
- 首先对造人的过程进行分析,该过程涉及到三个对象:女娲、八卦炉、三种不同肤色的人。
- 女娲可以使用场景类Client来表示。
- 八卦炉类似一个工厂,负责生产产品(即负责生产人类)。
- 对于三种不同肤色的人,他们都是==同一接口下的不同实现类==(对于八卦炉来说三种不同肤色的人都是它生产出来的产品)。
2、程序类图
类图解析:
- AbstractHumanFactory类是一个抽象类,定义了一个八卦炉具有的整体功能。
- HumanFactory类为实现类,用于完成具体的任务——创建人类
- Human接口是人类的总称,其三个实现类分别定义三类不同肤色的人种
- NvWa类是一个场景类,负责模拟这个场景,执行相关的任务
在这里我们定义的每个人种都有两个方法:getColor(获得人的肤色)和talk(交谈)
3、程序代码
Human接口:
public interface Human {
// 每个人种的皮肤都有相应的颜色
public void getColor();
// 只要是人类就都是会说话的
public void talk();
}
该类对应上面类图中的Human接口,接口中定义了每个人种都具备的两个方法:获得人的肤色(getColor方法)和交谈(talk方法)
BlackHuman类(黑色人种):
public class BlackHuman implements Human {
@Override
public void getColor() {
System.out.println("黑色人种的肤色当然是黑色的");
}
@Override
public void talk() {
System.out.println("黑种人:我们为黑人牙膏代言");
}
}
YellowHuman类(黄色人种):
public class YellowHuman implements Human {
@Override
public void getColor() {
System.out.println("黄种人的肤色是黄色的");
}
@Override
public void talk() {
System.out.println("黄种人:古老的东方有一条龙");
}
}
WhiteHuman类(白色人种):
public class WhiteHuman implements Human {
@Override
public void getColor() {
System.out.println("白种人的肤色肯定是白色的");
}
@Override
public void talk() {
System.out.println("白种人:有汰渍,没污渍!汰渍,专业漂白100年");
}
}
到这里所有的人种已经定义完毕,有了所有人种的样本,下一步就是定义一个用于烧制人类的八卦炉。在这里可以畅想一下女娲给八卦炉会下什么样的命令呢 ?无非就以下两种命令:
- 给我生产出一个黄色人种
- 给我生产出一个会跑、会跳、会说话的黄色人种
==第二条命令增加了很多交流成本==,作为一个生产的管理者,只需要知道我们生产的是什么事物就可以了,而不需要事物的具体信息。因此,由于只需要知道生产出的是什么,不需要知道生产出的产品的具体信息,八卦炉生产人类的方法输入参数类型应该是Human接口的实现类。(这也是类图中AbstractHumanFactory抽象类中createHuman方法中的参数类型为Class类型的原因)
AbstractHumanFactory类(抽象人类创建工厂):
public abstract class AbstractHumanFactory {
public abstract <T extends Human> T createHuman(Class<T> c);
}
在这里我们通过定义泛型来对createHuman方法的输入参数产生两层限制:
- 输入的参数必须是Class类型
- 输入的参数必须是Human的实现类
在该类中“T”表示的是只要实现了Human接口的类都可以作为参数
HumanFactory类(人类创建工厂):
public class HumanFactory extends AbstractHumanFactory {
@Override
public <T extends Human> T createHuman(Class<T> c) {
// 定义一个要生产的人种
Human human = null;
// 产生一个人种
try {
human = (T)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
System.out.println("人种生产出错了");
}
return (T)human;
}
}
到目前为止,人种信息、八卦炉都有了,就等女娲采集黄土然后命令八卦炉开始烧制各种肤色的人类了
NvWa类(女娲):
public class NvWa {
public static void main(String[] args) {
// 先声明一个八卦阴阳炉
AbstractHumanFactory factory = new HumanFactory();
// 女娲开始造人,但是第一次火候不足
System.out.println("<---- 造出的第一批人是白种人 ---->");
Human whiteHuman = factory.createHuman(WhiteHuman.class);
System.out.println("-- 获取白色人种的肤色 --");
whiteHuman.getColor();
System.out.println("-- 让我们听听白色人种想说什么 --");
whiteHuman.talk();
// 女娲第二次开始造人,烧制时间过长出现了黑人
System.out.println("<---- 造出的第二批人是黑种人 ---->");
Human blackHuman = factory.createHuman(BlackHuman.class);
System.out.println("-- 获取黑色人种的肤色 --");
blackHuman.getColor();
System.out.println("-- 让我们听听黑色人种想说什么 --");
blackHuman.talk();
// 女娲第三次造人吸取了前两次的教训,这次烧制成功了
System.out.println("<---- 造出的第三批人是黄种人 ---->");
Human yellowHuman = factory.createHuman(YellowHuman.class);
System.out.println("-- 获取黄色人种的肤色 --");
yellowHuman.getColor();
System.out.println("-- 让我们听听黄色人种想说什么 --");
yellowHuman.talk();
}
}
4、运行结果
到此女娲造人的过程就已经被我们用Java程序实现了,以上就是工厂方法模式
二、工厂方法模式的定义
工厂方法模式: Define an interface for creating an object, but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses. (定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类) 工厂方法模式属于创造类型设计模式
1、工厂方法模式的通用类图
类图解析:
- Product抽象类:该类为抽象产品类,抽象产品类负责定义产品的共性,实现对事物最抽象的定义
- ConcreteProduct类:该类为具体产品类,具体产品类可以有多个,都继承于抽象产品类,该类中定义了该类型产品的具体内容
- Creator抽象类:该类为抽象创建类,也就是抽象工厂。在抽象工厂中负责定义产品对象的产生
- ConcreteCreator类:该类为具体的实现工厂,实现工厂类实现的是具体如何生产出一个产品对象
2、工厂方法模式通用代码
抽象产品类:
public abstract class Product {
// 产品类的公共方法
public void method1(){
// 具体的业务处理
}
// 抽象方法
public abstract void method2();
}
负责定义产品的共性,实现对事物最抽象的定义
具体产品类:
public class ConcreteProduct1 extends Product {
@Override
public void method2() {
// 具体的业务处理
}
}
public class ConcreteProduct2 extends Product {
@Override
public void method2() {
// 具体业务处理
}
}
具体的产品类可以有多个,都继承于抽象产品类
抽象工厂类:
public abstract class Creator {
/**
* 创建一个产品对象,其输入参数的类型可以自行设置
* 通常为String、Enum、Class等,也可以为空
*/
public abstract <T extends Product> T createProduct(Class<T> c);
}
抽象工厂类负责定义产品对象的产生
具体工厂类:
public class ConcreteCreator extends Creator {
@Override
public <T extends Product> T createProduct(Class<T> c) {
// 声明生产产品的成员变量
Product product = null;
try {
// 通过反射获取到指定类的类名并创建此Class对象所表示的类的一个新实例
product = (Product)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
// 异常处理的方法,也可以是其它的业务语句
e.printStackTrace();
}
return (T)product;
}
}
具体如何生产出一个产品的对象,是由具体的工厂类来实现的
场景类:
public class Client {
public static void main(String[] args) {
// 声明具体进行生产的工厂
Creator creator = new ConcreteCreator();
// 声明由工厂生产的具体产品1的对象
Product product1 = creator.createProduct(ConcreteProduct1.class);
// 声明由工厂生产的具体产品2的对象
Product product2 = creator.createProduct(ConcreteProduct2.class);
/**
* 继续进行接下来的业务处理
*/
}
}
从抽象产品类开始到场景类结束就是一个比较实用并且易扩展的工厂方法模式的通用代码
三、工厂方法模式的应用
1、工厂方法模式的优点
- 工厂方法模式具有良好的封装性,代码结构清晰。一个对象创建是有条件约束的,例如==一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的具体过程是什么==。极大的降低了模块间的耦合性。
- 工厂方法模式具有良好的扩展性,在增加产品类的情况下,只要适当地修改具体的工厂类或者扩展一个工厂类,就可以很好的完成新的变化。例如在上面的例子中要加入一个绿色人种,只需要增加一个GreenHuman类,工厂类不用任何修改就可以完成系统的扩展。
- 工厂方法模式可以屏蔽产品类(这一特点非常重要),产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。因为==产品类的实例化工作时由工厂类负责的,一个产品对象具体由哪一个产品生成是由工厂类决定的==。
- 工厂方法模式是典型的解耦框架,高层模块只需要知道生产产品的抽象类,其它的实现类都不用关心,不需要的类也不用去交流,只依赖产品类的抽象。
2、工厂方法模式的使用场景
- 工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用。但是要考虑是否要增加一个工厂类进行管理,因为这样会增加代码的复杂度。
- 当需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。==万物皆对象,同样也可以说万物皆产品类==。例如需要设计一个连接邮件服务器的框架,有三种协议可供选择:POP3、IMAP、HTTP,这时我们就可以把这三种连接方式作为产品类,定义一个接口IConnectMail,然后定义对邮件的操作方法,用不同的方法实现三个具体的产品类(也就是三种连接方式),再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式。这样设计就可以做到很好的扩展。如果某些邮件服务器提供了WebService接口时我们只需要增加一个产品类就可以了。
- 工厂方法模式可以用在异构的项目中,例如通过WebService与一个非Java的项目交互(虽然WebService号称可以做到异构系统的同构化,但是在实际开发过程中还是会遇到很多问题),遇到类型问题、WSDL文件的支持等问题,从WSDL中产生的对象都认为是一个产品,然后这时由一个具体的工厂类进行管理,可以减少与外围系统的耦合。
- 工厂方法模式可以使用在测试驱动开发的框架下。例如测试一个A类,就需要把与A类有关联关系的B类也同时生产出来,这时就可以使用工厂方法模式把B类虚拟出来,避免A类与B类的耦合。(这个使用场景目前已经弱化了,遇到这种情况时可以考虑使用JMock或EasyMock)
四、工厂方法模式的扩展
工厂方法模式有很多扩展,在本文将介绍4种扩展
1、缩小为简单工厂模式
在开发中我们有时会考虑这样一个问题:一个模块仅需要一个工厂类,因此没有必要把抽象工厂生产出来,只需要使用静态方法就可以了。基于这种要求,还是以女娲造人为例,需要将例子中的AbstractHumanFactory修改一下即可
修改后的类图为:
在修改后的类图中,具体的八卦炉中创建人类的方法是静态的
两次类图的对比:
程序代码:
代码没有发生改变的程序:
// 简单工厂模式中的Human接口
public interface Human {
// 每个人种的皮肤都有相应的颜色
public void getColor();
// 只要是人类就都是会说话的
public void talk();
}
// 简单工厂模式中的黄种人类
public class YellowHuman implements Human {
@Override
public void getColor() {
System.out.println("黄种人的肤色是黄色的");
}
@Override
public void talk() {
System.out.println("黄种人:古老的东方有一条龙");
}
}
// 简单工厂模式中的黑种人类
public class BlackHuman implements Human {
@Override
public void getColor() {
System.out.println("黑色人种的肤色当然是黑色的");
}
@Override
public void talk() {
System.out.println("黑种人:我们为黑人牙膏代言");
}
}
// 简单工厂模式中的白种人类
public class WhiteHuman implements Human {
@Override
public void getColor() {
System.out.println("白种人的肤色肯定是白色的");
}
@Override
public void talk() {
System.out.println("白种人:有汰渍,没污渍!汰渍,专业漂白100年");
}
}
代码发生改变的程序:
// 简单工厂模式中的工厂类
public class HumanFactory {
public static <T extends Human> T createHuman(Class<T> c) {
// 定义一个生产出的人种
Human human = null;
try {
//产生一个人种
human = (Human) Class.forName(c.getName()).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return (T) human;
}
}
HumanFactory类仅有两个地方发生了变化:去掉继承抽象类,并在createHuman前增加static关键字;工厂类发生了变化的同时也引起了调用者NvWa的变化
// 简单工厂模式中的NvWa类
public class NvWa {
public static void main(String[] args) {
// 女娲第一次造人,火候不足,于是白色人种产生了
System.out.println("<---- 造出的第一批人是白种人 ---->");
// 发生变化的代码
Human whiteHuman = HumanFactory.createHuman(WhiteHuman.class);
System.out.println("-- 获取白色人种的肤色 --");
whiteHuman.getColor();
System.out.println("-- 让我们听听白色人种想说什么 --");
whiteHuman.talk();
// 女娲第二次开始造人,烧制时间过长出现了黑人
System.out.println("<---- 造出的第二批人是黑种人 ---->");
// 发生变化的代码
Human blackHuman = HumanFactory.createHuman(BlackHuman.class);
System.out.println("-- 获取黑色人种的肤色 --");
blackHuman.getColor();
System.out.println("-- 让我们听听黑色人种想说什么 --");
blackHuman.talk();
// 女娲第三次造人吸取了前两次的教训,这次烧制成功了
System.out.println("<---- 造出的第三批人是黄种人 ---->");
// 发色变化的代码
Human yellowHuman = HumanFactory.createHuman(YellowHuman.class);
System.out.println("-- 获取黄色人种的肤色 --");
yellowHuman.getColor();
System.out.println("-- 让我们听听黄色人种想说什么 --");
yellowHuman.talk();
}
}
运行结果:
可以看出运行结果没有发生变化,但是我们的类图变得更简单了,而且调用者也比较简单。==这个就是工厂方法模式的弱化,因为相较于工厂方法模式来说比较简单,所以被称为简单工厂模式(也叫做静态工厂模式)==,在实际项目中采用的还是比较多的,但是缺点是工厂类的扩展比较困难,不符合开闭原则
2、升级为多个工厂类
当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很耗费精力的情况,所有的产品类都放到一个工厂方法中进行初始化会使代码结构很不清晰(这里的初始化不仅仅是指代new,初始化包括new一个对象,并为对象设置一定的初始值)。例如:一个产品类具有5个具体实现,==每个类的初始化都不相同,如果写在一个工厂方法中,势必会导致该方法无比巨大==,如果遇到这种情况该如何处理呢? 考虑到需要结构清晰,因此我们为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联。还是以女娲造人为例,每个人种都有一个固定的八卦炉,分别造出黑色人种、白色人种、黄色人种。
修改后的类图为:
每个人种(具体的产品类)都对应了一个创建者,每个创建者都独立负责创建对应的产品对象
程序代码:
代码没有发生改变的程序:
// 多工厂模式中的Human接口
public interface Human {
// 每个人种的皮肤都有相应的颜色
public void getColor();
// 只要是人类就都是会说话的
public void talk();
}
// 多工厂模式中的黄色人种类
public class YellowHuman implements Human {
@Override
public void getColor() {
System.out.println("黄种人的肤色是黄色的");
}
@Override
public void talk() {
System.out.println("黄种人:古老的东方有一条龙");
}
}
// 多工厂模式中的白色人种类
public class WhiteHuman implements Human {
@Override
public void getColor() {
System.out.println("白种人的肤色肯定是白色的");
}
@Override
public void talk() {
System.out.println("白种人:有汰渍,没污渍!汰渍,专业漂白100年");
}
}
// 多工厂模式中的黑色人种类
public class BlackHuman implements Human {
@Override
public void getColor() {
System.out.println("黑色人种的肤色当然是黑色的");
}
@Override
public void talk() {
System.out.println("黑种人:我们为黑人牙膏代言");
}
}
代码发生改变的程序:
// 多工厂模式的抽象工厂类
public abstract class AbstractHumanFactory {
public abstract Human createHuman();
}
注意:抽象方法中已经不需要再传递相关的参数了,因为每一个具体的工厂都已经安排非常明确自己的职责:创建自己负责的产品类对象
// 多工厂模式中的黑色人种的创建工厂实现
public class BlackHumanFactory extends AbstractHumanFactory {
@Override
public Human createHuman() {
return new BlackHuman();
}
}
// 多工厂模式中的黄色人种的创建工厂实现
public class YellowHumanFactory extends AbstractHumanFactory {
@Override
public Human createHuman() {
return new YellowHuman();
}
}
// 多工厂模式中的白色人种的创建工厂实现
public class WhiteHumanFactory extends AbstractHumanFactory {
@Override
public Human createHuman() {
return new WhiteHuman();
}
}
以上三个具体的创建工厂都非常简单,但是如果一个系统比较复杂时工厂类也会相应对地变得复杂
// 多工厂模式中的场景类——女娲类
public class NvWa {
public static void main(String[] args) {
// 女娲第一次造人,火候不足,于是白色人种产生了
System.out.println("<---- 造出的第一批人是白种人 ---->");
// 发生变化的代码
Human whiteHuman = (new WhiteHumanFactory()).createHuman();
System.out.println("-- 获取白色人种的肤色 --");
whiteHuman.getColor();
System.out.println("-- 让我们听听白色人种想说什么 --");
whiteHuman.talk();
// 女娲第二次开始造人,烧制时间过长出现了黑人
System.out.println("<---- 造出的第二批人是黑种人 ---->");
// 发生变化的代码
Human blackHuman = (new BlackHumanFactory()).createHuman();
System.out.println("-- 获取黑色人种的肤色 --");
blackHuman.getColor();
System.out.println("-- 让我们听听黑色人种想说什么 --");
blackHuman.talk();
// 女娲第三次造人吸取了前两次的教训,这次烧制成功了
System.out.println("<---- 造出的第三批人是黄种人 ---->");
// 发生变化的代码
Human yellowHuman = (new YellowHumanFactory()).createHuman();
System.out.println("-- 获取黄色人种的肤色 --");
yellowHuman.getColor();
System.out.println("-- 让我们听听黄色人种想说什么 --");
yellowHuman.talk();
}
}
运行结果:
可以发现运行结果与之前的结果仍然相同。每一个产品类都对应了一个创建类,好处就是创建类的职责清晰,而且结构简单,但是给可扩展性和可维护性带来了一定的影响。不妨我们这样想一想,如果要扩展一个产品类,就需要建立一个相应的工厂类,这样就增加了扩展的难度。因为工厂类和产品类的数量相同你那个,维护时就需要考虑到两个对象间的关系。
在比较复杂的应用中一般采用多工厂的方法,然后再==增加一个协调类,避免调用者与各个子工厂交流,修调类的作用是封装子工厂类,对高层模块提供统一的访问接口==。
3、替代单例模式
单例模式的核心要求就是在内存中只有一个对象,通过工厂方法模式也可以只在内存中生产一个对象
类图:
Singleton定义了一个无参的 private的无参构造函数,目的是不允许通过new的方式创建一个对象
代码实现:
单例类:
public class Singleton {
// 不允许通过new产生一个对象
private Singleton(){
}
public void doSomething(){
// 业务处理
}
}
Singleton保证不能通过正常的渠道建立一个对象,那SingletonFactory生产单例类的对象可以通过反射来创建
生产单例的工厂类:
public class SingletonFactory {
private static Singleton singleton;
static {
try {
Class c = Class.forName(Singleton.class.getName());
// 获得无参构造
Constructor constructor = c.getDeclaredConstructor();
// 开启private访问权限,设置无参构造是可以访问的
constructor.setAccessible(true);
// 产生一个实例对象
singleton = (Singleton)constructor.newInstance();
} catch (Exception e) {
// 异常处理
}
}
}
==通过获得类构造器,然后设置访问权限,生成一个对象,接着提供外部访问,保证内存中的对象唯一==。以上通过工厂方法模式创建了一个单例对象,该框架可以继续扩展,在一个项目中可以产生一个单例构造器,只要满足所有产生单例的类的构造方法都是private类型的,就可以通过扩展该框架,只要输入一个类型就可以获得唯一的一个实例。
4、延迟初始化
延迟初始化: 一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用。
延迟初始化通用类图:
类图解析:
- ProductFactory负责产品类对象的创建工作,并且==通过prMap变量产生一个缓存,对需要再次被重复使用的对象保留==。
- Product和ConcreteProduct与之前的通用代码中的抽象产品类和具体产品类一致,负责定义产品的共性,实现对事物最抽象的定义和定义该类型产品的具体内容。
代码实现:
抽象产品类:
public abstract class Product1 {
// 产品类的公共方法
public void method(){
// 具体的业务处理
}
// 抽象方法
public abstract void method1();
}
public abstract class Product2 {
// 产品类的公共方法
public void method(){
// 具体的业务处理
}
// 抽象方法
public abstract void method2();
}
具体产品类:
public class ConcreteProduct1 extends Product1 {
@Override
public void method1() {
// 具体的业务处理
}
}
public class ConcreteProduct2 extends Product2 {
@Override
public void method2() {
// 具体的业务处理
}
}
延迟加载工厂类:
public class ProductFactory {
private static final Map<String, Product> prMap = new HashMap<>();
public static synchronized Product createProduct(String type) throws Exception{
Product product = null;
// 如果Map中已经有这个对象
if (prMap.containsKey(type)){
product = prMap.get(type);
} else {
if (type.equals("Product1")){
product = new ConcreteProduct1();
} else {
product = new ConcreteProduct2();
}
// 同时把对象放到缓存容器中
prMap.put(type,product);
}
return product;
}
}
通过定义一个Map容器,容纳所有产生的对象,==如果在Map容器中已经有的对象,则直接取出返回;如果没有,则根据需要的类型产生一个对象并放入Map容器中,以方便下次调用==。 如果限制某一个产品类最大实例化的数量,可以通过判断 Map中已有的对象的数量来实现,这样处理是非常有意义的,例如JDBCl连接数据库,都会要求设置一个MaxConnections最大连接数量,该数量就是内存中最大实例化的数量。延迟加载还可以使用在对象初始化比较复杂的情况下,例如硬件的访问,设计多方面的交互时可以通过延迟加载来降低对象的产生和销毁带来的复杂性。
总结
以上便是Java设计模式中的工厂方法模式,工厂方法模式在项目中使用的非常频繁,以至于很多代码中都包含工厂方法模式。在学习的过程中感觉工厂方法模式可以很好的做到与其它模式混合使用,变化出无穷的优秀设计,可能这也正是软件设计和开发的乐趣所在。
其它设计模式传送门:
UMLl类图: 为掌握Java设计模式奠基—两步搞懂UML类图
单例模式: 关于单例模式,你应该了解这些