举个生活中常见的例子——组装电脑,我们在组装电脑的时候,通常需要选择一系列的配件,比如:CPU、硬盘、内存、主板、电源、机箱等等。为了使讨论简单点,只考虑选择CPU和主板的问题。
事实上,我们在选择CPU的时候,面临一系列的问题,比如:品牌、型号、针脚数目、主频等问题,只有把这些都确定下来,才能确定具体的CPU。同样,在选择主板的时候,也有一系列的问题,比如:品牌、芯片组、集成芯片、总线频率等问题,也只有这些都确定了,才能确定具体的主板。
选择不同的CPU和主板,是每个客户去组装电脑的时候,向装机公司提出的要求,也就是我们每个人自己拟定的装机方案。
在最终确定这个装机方案之前,还需要整体考虑各个配件之间的兼容性,比如:CPU和主板,如果CPU针脚数和主板提供的CPU插口不兼容,是无法组装的。也就是说,装机方案是有整体性的,里面选择的各个配件之间是有关联的。
对于装机工程师而言,他只知道组装一台电脑,需要相应的配件,但是具体使用什么样的配件,还得由客户说了算。也就是说装机工程师只是负责组装,而客户负责选择装配所需要的具体的配件。因此,当装机工程师为不同的客户组装电脑时,只需要按照客户的装机方案,去获取相应的配件,然后组装即可。
现在需要使用程序来把这个装机的过程,尤其是选择组装电脑配件的过程实现出来,该如何实现呢?
考虑客户的功能,需要选择自己需要的CPU和主板,然后告诉装机工程师自己的选择,接下来就等着装机工程师组装机器了。
对装机工程师而言,只是知道CPU和主板的接口,而不知道具体实现,很明显可以用上简单工厂或工厂方法模式,为了简单,这里选用简单工厂吧。客户告诉装机工程师自己的选择,然后装机工程师会通过相应的工厂去获取相应的实例对象。
1.先来看看CPU和主板的接口, 示例代码如下:
/**
* cpu接口
*
* @author mengqa
*/
public interface CpuApi {
/*
* cpu具有计算功能
*/
public void calculate();
}
/**
* 类描述:
* 主板接口
* @author mengqa
*/
public interface MainboardApi {
/*
* 安装cpu
*/
public void installCpu();
}
2.具体的实现:
/**
* 类描述:
* InterCpu的具体实现
*
* 使用方式:
* 创建时间:2017年9月3日
* @author mengqa
*/
public class AmdCpu implements CpuApi {
// 针数
private int pins = 0;
/**
* 构造方法,传入CPU的针脚数目
* @param pins CPU的针脚数目
*/
public AmdCpu(int pins) {
this.pins = pins;
}
@Override
public void calculate() {
System.out.println("now in AMD CPU,pins="+pins);
}
}
// intel的cpu实现
public class InterCpu implements CpuApi {
// 针数
private int pins = 0;
/**
* 构造方法,传入CPU的针脚数目
* @param pins CPU的针脚数目
*/
public InterCpu(int pins) {
this.pins = pins;
}
@Override
public void calculate() {
System.out.println("now in Intel CPU,pins="+pins);
}
}
// 主板实现略...
3.简单工厂类:
public class CpuFactory {
public static CpuApi createCPUApi(int type) {
switch (type) {
case 1:
return new InterCpu(1156);
case 2:
return new AmdCpu(965);
default:
break;
}
return null;
}
}
public class MainBoardFactory {
public static MainboardApi createMainBoardApi(int type) {
switch (type) {
case 1:
return new GAMainboard(1156);
case 2:
return new MsiMainboard(965);
default:
break;
}
return null;
}
}
4.电脑工程师调用简单工厂, 装机过程
public class ComputerEngineer {
private CpuApi cpu = null;
private MainboardApi mainboard = null;
/**
* 装机过程
* @param cpuType 客户选择所需CPU的类型
* @param mainboardType 客户选择所需主板的类型
*/
public void makeComputer(int cpuType,int mainboardType){
//1:首先准备好装机所需要的配件
prepareHardwares(cpuType,mainboardType);
//2:组装机器
//3:测试机器
//4:交付客户
}
public void prepareHardwares(int cpuType,int mainboardType) {
this.cpu = CpuFactory.createCPUApi(cpuType);
this.mainboard = MainBoardFactory.createMainBoardApi(mainboardType);
// 测试配件
this.cpu.calculate();
this.mainboard.installCpu();
}
}
5.最后客户端如何调用的:
public class Client {
public static void main(String[] args) {
//创建装机工程师对象
ComputerEngineer engineer = new ComputerEngineer();
//告诉装机工程师自己选择的配件,让装机工程师组装电脑
engineer.makeComputer(1,1);
}
}
看了上面的实现,会感觉到很简单嘛,通过使用简单工厂来获取需要的CPU和主板对象,然后就可以组装电脑了。有何问题呢?
虽然上面的实现,通过简单工厂解决解决了:对于装机工程师,只知CPU和主板的接口,而不知道具体实现的问题。
但还有一个问题没有解决,什么问题呢?
那就是这些CPU对象和主板对象其实是有关系的,是需要相互匹配的。
而在上面的实现中,并没有维护这种关联关系,CPU和主板是由客户随意选择的。这是有问题的。
这就是没有维护配件之间的关系造成的。该怎么解决这个问题呢?
1.应用抽象工厂模式来解决的思路
仔细分析上面的问题,其实有两个问题点,
一个是只知道所需要的一系列对象的接口,而不知具体实现,或者是不知道具体使用哪一个实现;
另外一个是这一系列对象是相关或者相互依赖的。
也就是说既要创建接口的对象,还要约束它们之间的关系。
有朋友可能会想,工厂方法模式或者是简单工厂,不就可以解决只知接口而不知实现的问题吗?怎么这些问题又冒出来了呢?
请注意,这里要解决的问题和工厂方法模式或简单工厂解决的问题是有很大不同的,工厂方法模式或简单工厂关注的是单个产品对象的创建,比如创建CPU的工厂方法,它就只关心如何创建CPU的对象,而创建主板的工厂方法,就只关心如何创建主板对象。
这里要解决的问题是,要创建一系列的产品对象,而且这一系列对象是构建新的对象所需要的组成部分,也就是这一系列被创建的对象相互之间是有约束的。
解决这个问题的一个解决方案就是抽象工厂模式。
在这个模式里面,会定义一个抽象工厂,在里面虚拟的创建客户端需要的这一系列对象,所谓虚拟的就是定义创建这些对象的抽象方法,并不去真的实现,然后由具体的抽象工厂的子类来提供这一系列对象的创建。这样一来可以为同一个抽象工厂提供很多不同的实现,那么创建的这一系列对象也就不一样了,也就是说,抽象工厂在这里起到一个约束的作用,并提供所有子类的一个统一外观,来让客户端使用。
AbstractFactory:抽象工厂,定义创建一系列产品对象的操作接口。
ConcreteFactory:具体的工厂,实现抽象工厂定义的方法,具体实现一系列产品对象的创建。
AbstractProduct:定义一类产品对象的接口。
ConcreteProduct:具体的产品实现对象,通常在具体工厂里面,会选择具体的产品实现对象,来创建符合抽象工厂定义的方法返回的产品类型的对象。
Client:客户端,主要使用抽象工厂来获取一系列所需要的产品对象,然后面向这些产品对象的接口编程,以实现需要的功能。
1.抽象工厂代码:
/**
* 抽象工厂
* 定义创建一系列产品对象的操作接口。
*/
public interface AbstractFactory {
/**
* 示例方法,创建抽象产品A的对象
* @return 抽象产品A的对象
*/
public AbstractProductA createProductA();
/**
* 示例方法,创建抽象产品B的对象
* @return 抽象产品B的对象
*/
public AbstractProductB createProductB();
}
2.接下来看看产品的定义,由于只是示意,并没有去定义具体的方法,示例代码如下:
/**
* 类描述:
* 定义一类产品对象的接口。
*
* 使用方式:
* 创建时间:2017年9月3日
* @author mengqa
*/
public abstract class AbstractProductA {
/**
* 产品A
*/
}
public abstract class AbstractProductB {
/**
* 产品B
*/
}
3.同样的,产品的各个实现对象也是空的,示例代码如下:
/**
* 产品A的具体实现
*/
public class ProductA1 implements AbstractProductA {
//实现产品A的接口中定义的操作
}
/**
* 产品A的具体实现
*/
public class ProductA2 implements AbstractProductA {
//实现产品A的接口中定义的操作
}
/**
* 产品B的具体实现
*/
public class ProductB1 implements AbstractProductB {
//实现产品B的接口中定义的操作
}
/**
* 产品B的具体实现
*/
public class ProductB2 implements AbstractProductB {
//实现产品B的接口中定义的操作
}
4.接下来看看具体的工厂的实现示意,示例代码如下:
/**
* 具体的工厂实现对象,实现创建具体的产品对象的操作
*/
public class ConcreteFactory1 implements AbstractFactory {
public AbstractProductA createProductA() {
return new ProductA1();
}
public AbstractProductB createProductB() {
return new ProductB1();
}
}
/**
* 具体的工厂实现对象,实现创建具体的产品对象的操作
*/
public class ConcreteFactory2 implements AbstractFactory {
public AbstractProductA createProductA() {
return new ProductA2();
}
public AbstractProductB createProductB() {
return new ProductB2();
}
}
5.最后来看看客户端的实现示意,示例代码如下:
public class Client {
public static void main(String[] args) {
//创建抽象工厂对象
AbstractFactory af = new ConcreteFactory1();
//通过抽象工厂来获取一系列的对象,如产品A和产品B
af.createProductA();
af.createProductB();
}
}
要使用抽象工厂模式来重写示例,先来看看如何使用抽象工厂模式来解决前面提出的问题。
装机工程师要组装电脑对象,需要一系列的产品对象,比如CPU、主板等,于是创建一个抽象工厂给装机工程师使用,在这个抽象工厂里面定义抽象的创建CPU和主板的方法,这个抽象工厂就相当于一个抽象的装机方案,在这个装机方案里面,各个配件是能够相互匹配的。
每个装机的客户,会提出他们自己的具体装机方案,或者是选择已有的装机方案,相当于为抽象工厂提供了具体的子类,在这些具体的装机方案类里面,会创建具体的CPU和主板实现对象。
1.新加入的抽象工厂的定义,示例代码如下:
/**
* 类描述:
* 抽象电脑系列装机方案
* 这个抽象工厂就相当于一个抽象的装机方案,在这个装机方案里面,各个配件是能够相互匹配的。
每个装机的客户,会提出他们自己的具体装机方案,或者是选择已有的装机方案,相当于为抽象工厂提供了具体的子类,在这些具体的装机方案类里面,会创建具体的CPU和主板实现对象。
* 创建时间:2017年9月3日
* @author mengqa
*/
public interface ComputerSeriesFactory {
/**
* 创建CPU的对象
* @return CPU的对象
*/
public CpuApi createCpu();
/**
* 创建MainBoard的对象
* @return MainBoard的对象
*/
public MainboardApi createMainBoard();
}
2.看看具体的实现工厂
public class InterSeriesFactory implements ComputerSeriesFactory {
/**
* 装机方案一:Intel 的CPU + 技嘉的主板
* 这里创建CPU和主板对象的时候,是对应的,能匹配上的
*/
@Override
public CpuApi createCpu() {
return new InterCpu(1699);
}
@Override
public MainboardApi createMainBoard() {
return new MsiMainboard(959);
}
}
public class AmdSeriesFactory implements ComputerSeriesFactory {
/**
* 装机方案二:AMD 的CPU + 微星的主板
* 这里创建CPU和主板对象的时候,是对应的,能匹配上的
*/
@Override
public CpuApi createCpu() {
return new AmdCpu(1099);
}
@Override
public MainboardApi createMainBoard() {
return new GAMainboard(959);
}
}
3.再来看看装机工程师类的实现,在现在的实现里面,装机工程师相当于使用抽象工厂的客户端,虽然是由真正的客户来选择和创建具体的工厂对象,但是使用抽象工厂的是装机工程师对象。
装机工程师类跟前面的实现相比,主要的变化是:从客户端,不再传入选择CPU和主板的参数,而是直接传入客户选择并创建好的装机方案对象。这样就避免了单独去选择CPU和主板,客户要选就是一套,就是一个系列。示例代码如下:
public class ComputerEngineer {
private CpuApi cpu = null;
private MainboardApi mainboard = null;
/**
* 装机过程
* @param cpuType 客户选择所需CPU的类型
* @param mainboardType 客户选择所需主板的类型
*/
public void makeComputer(ComputerSeriesFactory series){
//1:首先准备好装机所需要的配件
prepareHardwares(series);
//2:组装机器
//3:测试机器
//4:交付客户
}
public void prepareHardwares(ComputerSeriesFactory series) {
this.cpu = series.createCpu();
this.mainboard = series.createMainBoard();
// 测试配件
this.cpu.calculate();
this.mainboard.installCpu();
}
}
4.客户端代码
public class Client {
public static void main(String[] args) {
ComputerEngineer engineer = new ComputerEngineer();
engineer.makeComputer(new InterSeriesFactory());
engineer.makeComputer(new AmdSeriesFactory());
/**
* CPU和主板是相关的对象,是构建电脑的一系列相关配件,
* 这个抽象工厂就相当于一个装机方案,客户选择装机方案的时候,
* 一选就是一套,CPU和主板是确定好的,不让客户分开选择,
* 这就避免了出现不匹配的错误。
*/
}
}
在前面的示例中,抽象工厂为每一种它能创建的产品对象都定义了相应的方法,比如创建CPU的方法和创建主板的方法等。
这种实现有一个麻烦,就是如果在产品簇中要新增加一种产品,比如现在要求抽象工厂除了能够创建CPU和主板外,还要能够创建内存对象,那么就需要在抽象工厂里面添加创建内存的这么一个方法。当抽象工厂一发生变化,所有的具体工厂实现都要发生变化,这非常的不灵活。
1.先来改造抽象工厂,示例代码如下:
public interface ComputerSeriesFactory {
/**
* type 的含义: 1代表 返回cpu部件 2.代表返回 主板部件 3.代表返回 内存部件
*/
public Object createComputerPart(int type);
}
2.下面来提供具体的工厂实现,也就是相当于以前的装机方案,先改造原来的方案一吧,现在的实现会有较大的变化,示例代码如下:
/**
* 装机方案一:AMD 的CPU + 微星的主板
* 这里创建CPU和主板对象的时候,是对应的,能匹配上的
*
* 可以看到具体工厂实现方法已经改成通过参数返回不同类型的对象
*/
@Override
public Object createComputerPart(int type) {
Object obj = null;
switch (type) {
case 1:
obj = new AmdCpu(1099);
break;
case 2:
obj = new MsiMainboard(789);
break;
case 3:
obj = new KinMemery();
break;
default:
break;
}
return obj;
}
// intel方案结构同此代码, 略...
3.看看这个时候使用抽象工厂的客户端实现,也就是在装机工程师类里面,通过抽象工厂来获取相应的配件产品对象,示例代码如下:
public class ComputerEngineer {
private CpuApi cpu = null;
private MainboardApi mainboard = null;
private Memery memery = null;
/**
* 装机过程
* @param cpuType 客户选择所需CPU的类型
* @param mainboardType 客户选择所需主板的类型
*/
public void makeComputer(ComputerSeriesFactory series){
//1:首先准备好装机所需要的配件
prepareHardwares(series);
//2:组装机器
//3:测试机器
//4:交付客户
}
public void prepareHardwares(ComputerSeriesFactory series) {
this.cpu = (CpuApi) series.createComputerPart(1);
this.mainboard = (MainboardApi) series.createComputerPart(2);
this.memery = (Memery) series.createComputerPart(3);
// 测试配件
this.cpu.calculate();
this.mainboard.installCpu();
this.memery.cal();
}
}
你会发现创建产品对象返回来过后,需要造型成为具体的对象,因为返回的是Object,如果这个时候没有匹配上,比如返回的不是CPU对象,但是要强制造型成为CPU,那么就会发生错误,因此这种实现方式的一个潜在缺点就是不太安全。
如果希望一个系统独立于它的产品的创建,组合和表示的时候,换句话说,希望一个系统只是知道产品的接口,而不关心实现的时候。
如果一个系统要由多个产品系列中的一个来配置的时候,换句话说,就是可以动态的切换产品簇的时候。
如果要强调一系列相关产品的接口,以便联合使用它们的时候。