1 实际场景 1.1 组装电脑 设想一个组装电脑 的场景,在组装电脑时,需要选着一些配件,如:CPU
、硬盘、内存、主板、电源、机箱等。为降低复杂度,目前只考虑 CPU
和主板。
事实上,我们在选择 CPU
的时候,面临一系列的问题,比如:品牌、型号、针脚数目、主频等问题,只有把这些都确定下来,才能确定具体的 CPU
。同样,在选择主板的时候,也有一系列的问题,比如:品牌、芯片组、集成芯片、总线频率等问题,也只有这些都确定了,才能确定具体的主板。
在最终确定这个装机方案之前,还需要整体考虑各个配件之间的兼容性,比如:CPU
和主板,如果 CPU
针脚数和主板提供的 CPU
插口不兼容,是无法组装的。也就是说,装机方案是有整体性的,里面选择的各个配件之间是有关联的 。
对于装机工程师而言,他只知道组装一台电脑,需要相应的配件,但是具体使用什么样的配件,还得由客户说了算。也就是说装机工程师只是负责组装,而客户负责选择装配所需要的具体的配件。 因此,当装机工程师为不同的客户组装电脑时,只需要按照客户的装机方案,去获取相应的配件,然后组装即可。
如何利用代码模拟选择配件进行装机的过程?
1.2 非抽象工厂的解决方案 考虑客户的功能,需要选择自己需要的 CPU
和主板,然后告诉装机工程师自己的选择,接下来就等着装机工程师组装机器了。
对装机工程师而言,只是知道 CPU
和主板的接口,而不知道具体实现,很明显可以用上简单工厂或工厂方法模式,为了简单,这里选用简单工厂。客户告诉装机工程师自己的选择,然后装机工程师会通过相应的工厂去获取相应的实例对象。
1、cpu
和主板的接口定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface CpuApi { void calculate () ; } public interface MainboardApi { void installCpu () ; }
2、两个品牌的 cpu
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class IntelCpu implements CpuApi { private int pins = 0 ; public IntelCpu (int pins) { this .pins = pins; } @Override public void calculate () { System.out.println("now in intel cpu, pins=" + pins); } } public class AmdCpu implements CpuApi { private int pins = 0 ; public AmdCpu (int pins) { this .pins = pins; } @Override public void calculate () { System.out.println("now in amd cpu, pins=" + pins); } }
3、两个品牌的主板实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class GAMainboard implements MainboardApi { private int cpuHoles = 0 ; public GAMainboard (int cpuHoles) { this .cpuHoles = cpuHoles; } @Override public void installCpu () { System.out.println("now in GAMainboard, couHoles=" + cpuHoles); } } public class MSIMainboard implements MainboardApi { private int cpuHoles = 0 ; public MSIMainboard (int cpuHoles) { this .cpuHoles = cpuHoles; } @Override public void installCpu () { System.out.println("now in MSIMainboard, cpuHoles=" + cpuHoles); } }
4、cpu
工厂实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class CpuFactory { public static CpuApi createCpuApi (int type) { CpuApi cpu = null ; if (type == 1 ) { cpu = new IntelCpu (1156 ); } else if (type == 2 ) { cpu = new AmdCpu (939 ); } return cpu; } }
5、主板工厂实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MainboardFactory { public static MainboardApi createMainboardApi (int type) { MainboardApi mainboard = null ; if (type == 1 ) { mainboard = new GAMainboard (1156 ); } else if (type == 2 ) { mainboard = new MSIMainboard (939 ); } return mainboard; } }
6、装机工程师的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class ComputerEngineer { private CpuApi cpu = null ; private MainboardApi mainboard = null ; public void assembleComputer (int cpuType, int mainboardType) { prepareHardwares(cpuType, mainboardType); } private void prepareHardwares (int cpuType, int mainboardType) { this .cpu = CpuFactory.createCpuApi(cpuType); this .mainboard = MainboardFactory.createMainboardApi(mainboardType); this .cpu.calculate(); this .mainboard.installCpu(); } }
7、客户端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Client { public static void main (String[] args) { ComputerEngineer engineer = new ComputerEngineer (); engineer.assembleComputer(1 , 1 ); } }
1.3 有何问题 虽然上面的实现,通过简单工厂解决解决了:对于装机工程师无需感知 CPU
和主板的具体实现的问题。但还有一个问题没有解决:没有解决 CPU
对象与主板对象之间的关系匹配问题。而在上面的实现中,并没有维护这种关联关系,若客户随意选择 CPU
和主板的类型,可能导致针脚不配位的问题。
如何维护对象之间的关系?
2 抽象工厂模式 2.1 定义 抽象工厂模式(Abstract Factory Pattern
)属于类创建型模式。提供一个创建一系列相关或者相互依赖对象的接口,而无需指定具体的类。
2.2 抽象工厂模式解决问题思路 上述场景中,实际有两个问题:
对于使用者而言,只需要一系列对象的接口,而无需了解对象的具体实现;
对于这些对象是相关的或者说相互依赖,即:创建接口的对象还需要约束这些对象之间的关系;
需要明确简单工厂模式、工厂方法模式无法解决上述问题的关键所在:简单工厂和工厂方法模式关注的单个对象的创建 ,如:CPU
如何创建,主板如何创建。无法解决对象之间的关联关系。
解决这个问题的一个解决方案就是抽象工厂模式。 在这个模式里面,会定义一个抽象工厂,在里面虚拟的创建客户端需要的这一系列对象,所谓虚拟的就是定义创建这些对象的抽象方法,并不去真的实现,然后由具体的抽象工厂的子类来提供这一系列对象的创建 。这样一来可以为同一个抽象工厂提供很多不同的实现,那么创建的这一系列对象也就不一样了,也就是说,抽象工厂在这里起到一个约束的作用,并提供所有子类的一个统一外观,来让客户端使用 。
2.3 结构说明
AbstractFactory
:抽象工厂,定义创建一系列产品对象的操作接口;ConcreteFactory
:具体的工厂,实现抽象工厂定义的方法,具体操作一系列产品对象的创建;AbstractProduct
:定义一类产品对象的接口;ConcreProduct
:具体的产品实现对象,通常在具体工厂里面,会选择具体的产品实现对象,来创建符合抽象工厂定义的方法返回的产品类型的对象;Client
:客户端,主要使用抽象工厂来获取一系列所需要的产品对象,然后面向这些产品对象的接口编程,以实现需要的功能;
2.4 示例代码 1、抽象工厂的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface AbstractFactory { public AbstractProductA createProductA () ; public AbstractProductB createProductB () ; }
2、产品定义示意代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface AbstractProductA { } public interface AbstractProductB { }
3、具体产品的实现定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class ProductA1 implements AbstractProductA { } public class ProductA2 implements AbstractProductA { } public class ProductB1 implements AbstractProductB { } public class ProductB2 implements AbstractProductB { }
4、具体工厂的实现 - 维护了不同对象之间的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class ConcreteFactory1 implements AbstractFactory { @Override public AbstractProductA createProductA () { return new ProductA1 (); } @Override public AbstractProductB createProductB () { return new ProductB1 (); } } public class ConcreteFactory2 implements AbstractFactory { @Override public AbstractProductA createProductA () { return new ProductA2 (); } @Override public AbstractProductB createProductB () { return new ProductB2 (); } }
5、客户端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class FactoryClient { public static void main (String[] args) { AbstractFactory factory = new ConcreteFactory1 (); factory.createProductA(); factory.createProductB(); } }
2.5 重写方案 使用抽象工厂模式重写前面的例子:装机工程师要组装电脑对象,需要一系列的产品对象,比如 CPU
、主板等,于是创建一个抽象工厂给装机工程师使用,在这个抽象工厂里面定义抽象的创建 CPU
和主板的方法,这个抽象工厂就相当于一个抽象的装机方案,在这个装机方案里面,各个配件是能够相互匹配的 。
1、CPU
与主板的接口定于以及实现同上
2、抽象工厂定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface AbstractFactory { public CpuApi createCpuApi () ; public MainboardApi createMainboardApi () ; }
3、装机方案的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Schema1 implements AbstractFactory { @Override public CpuApi createCpuApi () { return new IntelCpu (1156 ); } @Override public MainboardApi createMainboardApi () { return new GAMainboard (1156 ); } } public class Schema2 implements AbstractFactory { @Override public CpuApi createCpuApi () { return new AmdCpu (939 ); } @Override public MainboardApi createMainboardApi () { return new MSIMainboard (939 ); } }
4、装机工程师的实现。
装机工程师相当于使用抽象工厂的客户端,虽然是由真正的客户来选择和创建具体的工厂对象,但是使用抽象工厂的是装机工程师对象。
与前面的实现相比,主要的变化是:从客户端,不再传入选择 CPU
和主板的参数,而是直接传入客户选择并创建好的装机方案对象。这样就避免了单独去选择 CPU
和主板,客户要选就是一套,就是一个系列 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class ComputerEngineer { private CpuApi cpu = null ; private MainboardApi mainboard = null ; public void assembleComputer (AbstractFactory schema) { prepareHardwares(schema); } private void prepareHardwares (AbstractFactory schema) { this .cpu = schema.createCpuApi(); this .mainboard = schema.createMainboardApi(); this .cpu.calculate(); this .mainboard.installCpu(); } }
5、客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Client { public static void main (String[] args) { ComputerEngineer engineer = new ComputerEngineer (); AbstractFactory schema = new Schema1 (); engineer.assembleComputer(schema); } }
如同前面的示例,定义了一个抽象工厂 AbstractFactory,在里面定义了创建 CPU 和主板对象的接口的方法,但是在抽象工厂里面,并没有指定具体的 CPU 和主板的实现,也就是无须指定它们具体的实现类。
CPU 和主板是相关的对象,是构建电脑的一系列相关配件,这个抽象工厂就相当于一个装机方案,客户选择装机方案的时候,一选就是一套,CPU 和主板是确定好的,不让客户分开选择,这就避免了出现不匹配的错误。
3 模式讲解 3.1 认识抽象工厂模式 3.1.1 功能 抽象工厂的功能是为一系列相关对象或相互依赖的对象创建一个接口,一定要注意,这个接口内的方法不是任意堆砌的,而是一系列相关或相互依赖的方法 ,比如上面例子中的 CPU
和主板,都是为了组装一台电脑的相关对象。
从某种意义上看,抽象工厂其实是一个产品系列,或者是产品簇 。上面例子中的抽象工厂就可以看成是电脑簇,每个不同的装机方案,代表一种具体的电脑系列。
3.1.2 抽象工厂为接口
AbstractFactory
在 Java
中通常实现成为接口 ,不要被名称所误导,以为是实现成为抽象类,当然,如果需要为这个产品簇提供公共的功能,也不是不可以把 AbstractFactory
实现成为抽象类 ,但一般不这么做。
关于抽象类与接口选择,可以参考文章
3.1.3 使用工厂方法 AbstractFactory
定义了创建产品所需要的接口,具体的实现是在实现类里面,通常在实现类里面就需要选择多种更具体的实现,所以 AbstractFactory
定义的创建产品的方法可以看成是工厂方法,而这些工厂方法的具体实现就延迟到了具体的工厂里面 。也就是说使用工厂方法来实现抽象工厂 。
3.1.4 产品簇 由于抽象工厂定义的一系列对象,通常是相关或者相依赖的,这些产品对象就构成了一个产品簇,也就是抽象工厂定义了一个产品簇。这就带来非常大的灵活性,切换一个产品簇的时候,只要提供不同的抽象工厂实现就好了,也就是说现在是以产品簇做为一个整体被切换 。
3.2 定义可扩展的工厂 前面组装电脑的示例中,抽象工厂为每一种对象,如 CPU
、主板都定义了相应的方法。这有个不灵活的地方,若产品簇中要增加一个对象,所有的具体实现工厂都需要随之改变。
可以有一种不安全(类型强制转换)但是相对灵活的改进方法:将抽象工厂的方法设置为一个带参数的方法,通过参数来判断具体创建何种产品对象;由于只有一个方法,在返回类型上就不能是具体的某个产品类型,只能是某个产品簇的父类或者接口,又或者为 Object
类型。示例代码如下:
1、抽象工厂方法改造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface AbstractFactory { public Object createProduct (int type) ; }
这里要特别注意传入 createProduct
的参数所代表的含义,这个参数只是用来标识现在是在创建什么类型的产品 ,比如标识现在是创建 CPU 还是创建主板,一般这个 type 的含义到此就结束了,不再进一步表示具体是什么样的 CPU 或具体什么样的主板,也就是说 type 不再表示具体是创建 Intel 的 CPU 还是创建 AMD 的 CPU,这就是一个参数所代表的含义的深度的问题,要注意 。虽然也可以延伸参数的含义到具体的实现上,但这不是可扩展工厂这种设计方式的本意,一般也不这么做。
2、CPU 和主板的实现与前面实例一致:CPU 分为 Intel 和 Amd,主板分为技嘉和微星
3、具体工厂实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Schema1 implements AbstractFactory { @Override public Object createProduct (int type) { Object retObj = null ; if (type==1 ){ retObj = new IntelCpu (1156 ); }else if (type==2 ){ retObj = new GAMainboard (1156 ); } return retObj; } } public class Schema2 implements AbstractFactory { @Override public Object createProduct (int type) { Object retObj = null ; if (type == 1 ) { retObj = new AmdCpu (939 ); } else if (type == 2 ) { retObj = new MSIMainboard (939 ); } return retObj; } }
4、装机工程师的实现,拓展的基本实现与之前有所不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class ComputerEngineer { private CpuApi cpu = null ; private MainboardApi mainboard = null ; public void assembleComputer (AbstractFactory schema) { prepareHardwares(schema); } private void prepareHardwares (AbstractFactory schema) { this .cpu = (CpuApi) schema.createProduct(1 ); this .mainboard = (MainboardApi) schema.createProduct(2 ); this .cpu.calculate(); this .mainboard.installCpu(); } }
5、产品簇新增内存接口,可拓展性体现在已有代码无需改动
接口定义
1 2 3 4 5 6 7 8 9 10 11 public interface MemoryApi { public void cacheData () ; }
接口实现
1 2 3 4 5 6 7 8 9 10 11 public class HyMemory implements MemoryApi { @Override public void cacheData () { System.out.println("现在正在使用现代内存" ); } }
新增产品簇
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Schema3 implements AbstractFactory { @Override public Object createProduct (int type) { Object retObj = null ; if (type==1 ){ retObj = new IntelCpu (1156 ); }else if (type==2 ){ retObj = new GAMainboard (1156 ); } else if (type==3 ){ retObj = new HyMemory (); } return retObj; } }
装机工程师实现改动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class ComputerEngineer { private CpuApi cpu = null ; private MainboardApi mainboard = null ; private MemoryApi memory = null ; public void assembleComputer (AbstractFactory schema) { prepareHardwares(schema); } private void prepareHardwares (AbstractFactory schema) { this .cpu = (CpuApi) schema.createProduct(1 ); this .mainboard = (MainboardApi) schema.createProduct(2 ); this .memory = (MemoryApi) schema.createProduct(3 ); this .cpu.calculate(); this .mainboard.installCpu(); if (memory != null ) { this .memory.cacheData(); } } }
3.3 抽象工厂模式和 DAO 3.3.1 什么是 DAO DAO(Data Access Object):数据访问对象
DAO 是 JEE(也称 JavaEE,原 J2EE)中的一个标准模式,通过它来解决访问数据对象所面临的一系列问题,比如:数据源不同、存储类型不同、访问方式不同、供应商不同、版本不同等等,这些不同会造成访问数据的实现上差别很大。
数据源的不同 :比如存放于数据库的数据源,存放于 LDAP(轻型目录访问协议)的数据源;又比如存放于本地的数据源和远程服务器上的数据源等等
存储类型的不同 :比如关系型数据库(RDBMS)、面向对象数据库(ODBMS)、纯文件、XML 等等
访问方式的不同 :比如访问关系型数据库,可以用 JDBC、EntityBean、JPA 等来实现,当然也可以采用一些流行的框架,如 Hibernate、IBatis 等等
供应商的不同 :比如关系型数据库,流行如 Oracel、DB2、SqlServer、MySql 等等,它们的供应商是不同的
版本不同 :比如关系型数据库,不同的版本,实现的功能是有差异的,就算是对标准的 SQL 的支持,也是有差异的
DAO 需要抽象和封装所有对数据的访问,DAO 承担和数据仓库交互的职责,意味着,访问数据所面临的所有问题,都需要 DAO 在内部来自行解决。
3.3.2 DAO 与抽象工厂的关系 事实上,在实现 DAO 模式的时候,最常见的实现策略就是使用工厂的策略,而且多是通过抽象工厂模式来实现,当然在使用抽象工厂模式来实现的时候,可以结合工厂方法模式 。因此 DAO 模式和抽象工厂模式有很大的联系。
3.3.3 DAO 的工厂实现策略 3.3.3.1 工厂方法模式 假设需要实现一个订单处理模块,其中订单一般分为两部分,一是订单主记录或者订单主表,另一个是订单明细记录或者订单子表。业务逻辑一般会对两者都进行操作。
如果业务逻辑比较简单,且对数据的操作是固定的,操作数据库,不管订单的业务如何变化,底层数据存储都一样,此时可以选用工厂方法模式,系统结构为:
从结构示意图可以看出,如果底层存储固定的时候,DAOFactory 就相当于工厂方法模式中的 Creator,在里面定义两个工厂方法,分别创建订单主记录的 DAO 对象和创建订单子记录的 DAO 对象,因为固定是数据库实现,因此提供一个具体的工厂 RdbDAOFactory(Rdb:关系型数据库),来实现对象的创建。也就是说 DAO 可以采用工厂方法模式来实现。
采用工厂方法模式的情况,要求 DAO 底层存储实现方式是固定的,这种多用在一些简单的小项目开发上。
3.3.3.2 抽象工方法模式 实际上更多的时候,DAO 底层存储实现方式是不固定的,DAO 通常会支持多种存储实现方式,具体使用哪一种存储方式可能是由应用动态决定,或者是通过配置来指定。这种情况多见于产品开发、或者是稍复杂的应用、或者是较大的项目中。
对于底层存储方式不固定的时候,一般是采用抽象工厂模式来实现 DAO。比如现在的实现除了 RDB 的实现,还会有 Xml 的实现,它们会被应用动态的选择,此时系统结构如图所示:
从结构示意图可以看出,采用抽象工厂模式来实现 DAO 的时候,DAOFactory 就相当于抽象工厂,里面定义一系列创建相关对象的方法,分别是创建订单主记录的 DAO 对象和创建订单子记录的 DAO 对象,此时 OrderMainDAO 和 OrderDetailDAO 就相当于被创建的产品,RdbDAOFactory 和 XmlDAOFactory 就相当于抽象工厂的具体实现,在它们里面会选择相应的具体的产品实现来创建对象。
3.4 抽象工厂的优缺点 3.4.1 分离接口和实现 客户端使用抽象工厂来创建需要的对象,而客户端根本就不知道具体的实现是谁,客户端只是面向产品的接口编程而已,也就是说,客户端从具体的产品实现中解耦。
3.4.2 产品簇切换 可以定义产品簇的概念,给出对象之间的关联关系
3.4.3 不容易扩展新产品 前面也提到这个问题,如果需要给整个产品簇添加一个新的产品,那么就需要修改抽象工厂,这样就会导致修改所有的工厂实现类。在前面提供了一个可以扩展工厂的方式来解决这个问题,但是又不够安全 。如何选择,需要根据实际应用来权衡。
3.4.4 容易造成类层次复杂 在使用抽象工厂模式的时候,如果需要选择的层次过多,那么会造成整个类层次变得复杂。
那么客户端怎么选呢?不会把所有可能的实现情况全部都做到一个层次上吧,这个时候客户端就需要一层一层选择,也就是整个抽象工厂的实现也需要分出层次来,每一层负责一种选择,也就是一层屏蔽一种变化,这样很容易造成复杂的类层次结构 。
3.5 思考 3.5.1 抽象工厂模式的本质 本质:选择产品簇的实现
3.5.2 何时选用
若希望一个系统独立于它的产品的创建、组合和表现时,即:希望一个系统只知道产品的接口,而不需要关心具体的实现。
若系统需要由多个产品系列中的一个来配置时,即:可以动态切换产品簇时。
若需要强调一系列产品的接口,方便联合使用的时候。
3.6 相关模式 3.6.1 抽象工厂模式和工厂方法模式 工厂方法模式一般针对单独的产品对象的创建,而抽象工厂模式注重产品簇对象的创建。
若将抽象工厂创建的产品簇简化,产品簇只有一个产品,这是抽象工厂和工厂方法是差不多的,即:抽象工厂可以退化成工厂方法,工厂方法可以退化成简单工厂。
在抽象工厂的实现中,可以使用工厂方法提供抽象工厂的具体实现,可以组合使用。
3.6.2 抽象工厂和单例模式 可以组合使用。在抽象工厂模式例,具体的工厂实现可以实现成单例模式。