工厂模式是实际工作中用得比较多的设计模式,本笔记把简单工厂模式、工厂模式和抽象工厂模式放在一起,主要因为它们关系有点类似孙子、父亲和爷爷,简单工厂是工厂模式的一种特例,工厂模式又是抽象工厂的一种特例,这种关系很有意思,感觉就是我能变成你,你亦能变成我。
简单工厂模式
简单工厂并不算是23种设计模式的一种,但实际开发中,我们会常常用到这种工厂模式的迷你版,而且也有助于理解工厂模式。
都知道面向接口编程是Java编程的一个重要原则,那何谓接口?接口的重要思想便是:封装隔离。而简单工厂模式便是很好的体现了这个思想。
何谓简单工厂?其定义是:提供一个创建对象实例的功能,而无需关心其具体实现。简单来说,类似现实生活中的,我要买一个罐头,你把罐头给我就对了,罐头是一个抽象的概念,对应Java里的接口,而拿到的沙丁鱼罐头就是对应的具体实现。
简单工厂的本质:选择实现。
提供这么一个编程场景:通过简单工厂得到接口的具体实现,至于是哪个子类实现的,客户端无需关心。
就以罐头为例,来看代码:
package com.dgd.factory.demo1;
/**
* @Author DGD
* @date 2017/10/31.
* 罐头接口
*/
public interface Can {
void show();
}
作为购买者,我不需要知道罐头怎么生产的,我说要罐头,就有一个具体的沙丁鱼罐头了,那罐头怎么来的,就是在简单工厂里生产出来的,所以需要一个生产罐头的工厂类。
package com.dgd.factory.demo1;
/**
* @Author DGD
* @date 2017/10/31.
*/
public class CanFactory {
public static Can createCan() {
//默认只提供一种实现
Can can = null;
try {
can = (Can) Class.forName("com.dgd.factory.demo1.CannedSardines").newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return can;
}
}
可以看到,工厂里创建罐头实例是通过反射的机制来创建的,需要定义具体的罐头类,如下:
package com.dgd.factory.demo1;
/**
* @Author DGD
* @date 2017/10/31.
*/
public class CannedSardines implements Can {
@Override
public void show() {
System.out.println("我是沙丁鱼罐头");
}
}
现在来看看客户端是怎么调用的。
package com.dgd.factory.demo1;
/**
* @Author DGD
* @date 2017/10/31.
* 测试罐头工厂
*/
public class CanClient {
public static void main(String[] args) {
Can can = CanFactory.createCan();
can.show();
}
}
运行代码,控制台就会打印出“我是沙丁鱼罐头”的字样,看客户端也能感受到简单工厂的实用性,我想要罐头,只要调用的工厂类的生产罐头方法就行了CanFactory.createCan()
,使用是不是很方便?
那如果我还要水果罐头呢?因为上面的工厂方法只能生产一种罐头,现在需要根据客户需要再生产水果罐头,那么我们需要一个标识来告诉工厂,如果我要水果罐头,你就能给我生产水果罐头,下面我们来模拟一下,首先在罐头接口里添加水果罐头的类型定义:
package com.dgd.factory.demo1;
/**
* @Author DGD
* @date 2017/10/31.
* 罐头接口
*/
public interface Can {
//添加了水果罐头的标识
Integer CANNED_FRUITS=1;
void show();
}
水果罐头的实现:
package com.dgd.factory.demo1;
/**
* @Author DGD
* @date 2017/10/31.
*/
public class CannedFruits implements Can {
@Override
public void show() {
System.out.println("我是水果罐头");
}
}
简单工厂类里添加选择提供罐头的方法:
public static Can createCan(int type) {
//根据罐头类型来提供相应的罐头:如果标识是1,生产水果罐头,其它情况生产沙丁鱼罐头
Can can = null;
if (Can.CANNED_FRUITS == type) {
can = new CannedFruits();
} else {
can = new CannedSardines();
}
return can;
}
客户端调用:
package com.dgd.factory.demo1;
/**
* @Author DGD
* @date 2017/10/31.
* 测试罐头工厂
*/
public class CanClient {
public static void main(String[] args) {
//默认的罐头实现
Can can = CanFactory.createCan();
can.show();
//选择性的生产水果罐头
Can can1 = CanFactory.createCan(Can.CANNED_FRUITS);
can1.show();
}
}
通过以上代码可以感受到简单工厂为我们创建实例提供的方便,其优点也是很明显的,帮助封装和解耦,实际上,简单工厂常常配合单例模式,即私有化简单工厂的构造方法,因为工厂类我们只需要一个实例即可。简单工厂的一个缺点就是:可能会增加客户端的复杂度,如果通过参数来选择具体实例的话,客户端需要理解各个参数的含义,这样也会暴露部分内部实现。
以上具体代码可以参考Github。
选择实现是简单工厂的本质,下面我们来继续工厂模式,按照我个人理解,它就是简单工厂的爸爸。
工厂模式
工厂模式的定义:定义一个创建对象的接口,让子类决定实例化哪一个类,并用工厂方法将类的实例化延迟到其子类。
定义读起来有些拗口,简单来说,就是工厂模式定义了一个工厂接口,工厂方法生产出来的产品还是抽象的,产品对象的实例化工作被延迟到子工厂里去了。
即工厂模式的本质:延迟实现到子类。
还是利用现实问题来讲吧,提供这么一个场景:想要实现一个文件导出功能,由客户来选择导出的方式,比如说:直接导出文本文件,或者是导出到数据库中,那么如何用工厂模式来解决呢?
由于客户不需要知道文件是如何导出的,你只要告诉他调用这个方法就能导出文件就可以了,所以我们需要先把导出文件的工厂类定义好,如下:
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/26.
* 该类定义成抽象类,因为接口不能有方法的实现
*/
public abstract class ExportFileAbstractFactory {
public boolean export(String data){
return createExportFileApi().export(data);
}
//这个时候并不知道文件导出功能的具体实现
protected abstract ExportFileApi createExportFileApi();
}
导出文件的工厂需要知道导出文件功能的接口,因为工厂就是生产产品的,导出文件功能可以理解成该工厂要生产的一个产品,定义如下:
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/26.
*/
public interface ExportFileApi {
/**
* 导出内容成为文件
* @param data 文件内容
* @return 是否导出成功
*/
boolean export(String data);
}
工厂类被定义成抽象类,是因为我们不确定产品的具体实现方式,所以我们需要通过子工厂来体现出来,这里分别是导出文本文件的工厂和导出数据到数据库的工厂,或者理解成,上面定义的抽象工厂类是一个大工厂,负责具体功能产出的是旗下的子工厂。
将数据导出成文件文件的工厂:
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/26.
* 将数据导出成文本文件
*/
public class ExportTextFileFactory extends ExportFileAbstractFactory {
@Override
protected ExportFileApi createExportFileApi() {
return new ExportTextFile();
}
}
将数据导出到数据库中的工厂:
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/26.
* 将数据导出到数据库中
*/
public class ExportDbFileFactory extends ExportFileAbstractFactory {
@Override
protected ExportFileApi createExportFileApi() {
return new ExportDbFile();
}
}
可以看到,文件导出功能的执行者都是在子工厂里面创建的,它们的定义如下:
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/26.
*/
public class ExportTextFile implements ExportFileApi {
@Override
public boolean export(String data) {
System.out.println("导出数据"+data+"到文本文件");
return true;
}
}
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/26.
*/
public class ExportDbFile implements ExportFileApi {
@Override
public boolean export(String data) {
System.out.println("导出数据"+data+"到数据库备份文件");
return true;
}
}
用户怎么使用导出文件的功能?他需要知道这些实现细节吗?不需要。他只要知道,如果数据要导出成文本文件,跟导出文本文件的子工厂通知一声就行,如果要将数据导出到数据库,那就跟对应的子工厂通知一声就可以了,代码体现如下:
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/26.
*/
public class Client {
public static void main(String[] args) {
//将数据导出成文本文件
ExportFileAbstractFactory factory = new ExportTextFileFactory();
factory.export("123");
//将数据导出到数据库中
factory = new ExportDbFileFactory();
factory.export("DB");
}
}
运行代码,可以看到控制台分别打印出“导出数据123到文本文件“,“导出数据DB到数据库备份文件”。
其实工厂模式的核心思想就是,在不知道产品如何生产出来的时候,也要先给用户出来产品方案,对应代码里ExportFileAbstractFactory
中的这段:
//这个时候并不知道对象的具体实现
protected abstract ExportFileApi createExportFileApi();
也就是说,先跟客户谈好我们能生产这个产品,把合同签下来了,钱到手了,到时具体功能的实现再找下面的子工厂,至于这个子工厂是自家的还是外包的,其实并不重要,重要是能给我生产出这个产品出来就行,在本例中是用子工厂,也就是继承的方式。
即工厂模式的本质:延迟实现到子类。
如果把工厂模式中的功能代码去掉,只保留提供接口实现的部分,比如:
package com.dgd.factory.demo2;
/**
* @Author DGD
* @date 2017/10/31.
*/
public class ExportFatory {
//提供文本文件功能导出的实例
public static ExportFileApi createExportFileApi() {
return new ExportTextFile();
}
}
这样子是不是就是简单工厂模式了?所以说工厂模式退化一下,不把产品的功能实现揽在自己身上,只负责提供产品的话,那么它就变成简单工厂了,是不是感觉工厂模式就是简单工厂的爸爸,因为它比简单工厂懂得更多一点,担子重一些。
工厂模式的相关代码可以参考Github。
抽象工厂模式
之所以说抽象工厂模式在三者之中地位最高,自然是因为它的能力最大,简单工厂和工厂模式都是生产一种产品,比如罐头就是罐头,导出文件就是导出文件(功能也可以理解成是一种产品),而抽象工厂呢?它就是生产一系列产品的。
比如提供这么一种场景:去电脑城组装电脑,有这么两种装机方案(简单起见,只讨论CPU和主板),一是:Intel的CPU+技嘉的主板,二是:AMD的CPU+微星的主板,那么如何用抽象工厂模式来实现这个功能呢?
既然抽象工厂是生产一系列产品,自然要在接口定义里要体现出来,如下:
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 定义抽象工厂的接口
*/
public interface AbstractFactory {
//提供创建CPU的方法
CPUApi createCPUApi();
//提供创建主板的方法
MainboardApi createMainboardApi();
}
上面用到的CPU和主板的接口定义如下:
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* CPU接口
*/
public interface CPUApi {
void calculate();
}
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 主板接口
*/
public interface MainboardApi {
void installCPU();
}
CPU提供两种实现,分别是Intel型号的CPU和AMD型号的CPU:
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
*/
public class IntelCPU implements CPUApi {
//CPU针脚数目
private int pins;
public IntelCPU(int pins) {
this.pins = pins;
}
@Override
public void calculate() {
System.out.println("now in Intel CPU,pins="+pins);
}
}
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
*/
public class AMDCpu implements CPUApi {
private int pins;
public AMDCpu(int pins) {
this.pins = pins;
}
@Override
public void calculate() {
System.out.println("now in AMD CPU,pins="+pins);
}
}
主板也提供两种实现,分别是技嘉的主板和微星的主板:
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 技嘉的主板
*/
public class GAMainboard implements MainboardApi {
/**
* CPU插槽的孔数
*/
private int cpuHoles;
public GAMainboard(int cpuHoles) {
this.cpuHoles = cpuHoles;
}
@Override
public void installCPU() {
System.out.println("now in GAMainboard,cpuHoles="+cpuHoles);
}
}
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 微星的主板
*/
public class MSIMainboard implements MainboardApi {
private int cpuHoles;
public MSIMainboard(int cpuHoles) {
this.cpuHoles = cpuHoles;
}
@Override
public void installCPU() {
System.out.println("now in MSIMainboard,cpuholes="+cpuHoles);
}
}
好了,现在负责生产产品的工厂可以开工了,这里要保证的是CPU型号和主板型号要对应客户给的方案,代码如下:
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 第一种电脑组装实现:Intel的CPU+技嘉的主板
*/
public class Schema1 implements AbstractFactory {
@Override
public CPUApi createCPUApi() {
return new IntelCPU(1156);
}
@Override
public MainboardApi createMainboardApi() {
return new GAMainboard(1156);
}
}
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 第二种装机方案:AMD的CPU+微星的主板
*/
public class Schema2 implements AbstractFactory{
@Override
public CPUApi createCPUApi() {
return new AMDCpu(939);
}
@Override
public MainboardApi createMainboardApi() {
return new MSIMainboard(939);
}
}
现在客户把方案扔过来,我们要给他提供合适的电脑,按理来说我们还需要一个装机工程师,因为工厂负责把装机需要的一系列产品生产出来,需要装机工程师来组装。装机工程师的定义如下:
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 装机工程师
*/
public class ComputerEngineer {
private CPUApi cpuApi;
private MainboardApi mainboardApi;
public void makeComputer(AbstractFactory schema) {
//1,准备好电脑配件
prepareHardwares(schema);
//2,组装电脑
//3,测试电脑
//4,交付用户
}
private void prepareHardwares(AbstractFactory schema) {
//使用抽象工厂来获取相关组件
this.cpuApi = schema.createCPUApi();
this.mainboardApi = schema.createMainboardApi();
//测试组件是否好用
this.cpuApi.calculate();
this.mainboardApi.installCPU();
}
}
也就是说,客户把想要组装的方案交给装机工程师,工程师跟生产产品的工厂说一声,工厂直接把对应的CPU和主板生产好,交给装机工程师,最后装机工程师就把组装好的电脑交付给客户。
对应的客户端代码如下:
package com.dgd.factory.demo3;
/**
* @Author DGD
* @date 2017/10/27.
* 利用抽象工厂模式来实现电脑组装功能,客户可以随意切换装机方案,要保证不同方案下组装的电脑组件一致
*/
public class Client {
public static void main(String[] args) {
//方案一:Intel的CPU+技嘉的主板
AbstractFactory factory = new Schema1();
ComputerEngineer engineer = new ComputerEngineer();
engineer.makeComputer(factory);
//方案二:AMD的CPU+微星的主板
factory = new Schema2();
engineer.makeComputer(factory);
}
}
运行一下,可以看到方案一组装出来的电脑就是Intel+技嘉的主板,方案二组装出来的电脑就是AMD+微星的主板。
我们看看抽象工厂的定义,提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
由此可以知道,抽象工厂适用于工厂要生产一系列产品的场景,并且产品之间是有关系的比如CPU和主板,都是电脑的一部分,这一系列的产品不能一个是汽车发动机,一个是电脑主板,这样就变成单纯的工厂模式或者简单工厂了。
因而,抽象工厂的本质:选择产品簇的实现。
由于工厂模式只专注于一种产品的实现,而抽象工厂是产品簇的实现,因而可以说抽象工厂是工厂模式的父亲,自然就是简单工厂的爷爷了。换句话说,工厂模式是抽象工厂模式的一种特例,而简单工厂模式又是工厂模式的一种特例。
抽象工厂的示例代码可以参考Github。
后记:本文是我学习陈臣,王斌著的《研磨设计模式》的读书心得笔记,不当之处,欢迎指教,以便互相学习,共同进步。