一、概述
工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
二、解决问题
通常我们需要一个对象的时候,会想到使用new来创建对象
Tea tea = new MilkTea(); //使用了接口,代码更有弹性,体现设计原则“对接口编程,而不是对实现编程”
当我们需要多个对象的时候,”对接口编程“的原则似乎还能派上用场
Tea tea;
if("milk".equals(type)){
tea = new MilkTea();
}else if("coffee".equals(type)){
tea = new CoffeeTea();
}else if("lemon".equals(type)){
tea = new LemonTea();
}
这里也体现了“对接口编程”的好处,运行时决定要实例化哪个对象,让系统具备了弹性,但对于扩展性方面,我们就不敢恭维了。看以上代码,当我们要新增对象或者要扩展时,不得不打开这份代码进行检查和修改。通常这样的修改过的代码会造成部分系统更难维护和更新,而且也容易犯错。
为了有良好的扩展性,我们想到了另外一个设计原则”把变化的代码从不变化的代码中分离出来”。
假设我们要开一家烧饼店,我们每天会做各种口味的烧饼出售,做烧饼的程序包括准备原材料、和面、烘烤、切片、装盒。
//烧饼店
public class ShaobingStore {
public Shaobing orderShaobing(String type){
Shaobing shaobing = null;
if("onion".equals(type)){
//洋葱烧饼
shaobing = new OnionShaobing();
}else if("sour".equals(type)){
//酸菜烧饼
shaobing = new SourShaobing();
}else if("beef".equals(type)){
//牛肉烧饼
shaobing = new BeefShaobing();
}
//以上代码会发生改变,当洋葱烧饼不再出售时,我们会把创建洋葱的代码删除,我们可能会新增新口味的烧饼
else if("pork".equals(type)){
//牛肉烧饼
shaobing = new PorkShaobing();
}
//对于制作烧饼的程序中,以下这些步骤是不变的
if(shaobing != null){
shaobing.prepare();
shaobing.cut();
shaobing.bake();
shaobing.box();
}
return shaobing;
}
}
对于上面代码,我们使用”分离变化“的原则,把创建烧饼代码封装到一个类中,我们把它叫做工厂类,里面专门一个方法用来创建烧饼,如下所示:
package factorymethod.pattern;
public class SimpleShaobingFactory {
public Shaobing createShaobing(String type){
Shaobing shaobing = null;
if("onion".equals(type)){
//洋葱烧饼
shaobing = new OnionShaobing();
}else if("sour".equals(type)){
//酸菜烧饼
shaobing = new SourShaobing();
}else if("beef".equals(type)){
//牛肉烧饼
shaobing = new BeefShaobing();
}
return shaobing;
}
}
改进后的烧饼店如下:
package factorymethod.pattern;
//烧饼店
public class ShaobingStore {
public Shaobing orderShaobing(String type){
Shaobing shaobing = null;
shaobing = factory.createShaobing(type);
//对于制作烧饼的程序中,以下这些步骤是不变的
if(shaobing != null){
shaobing.prepare();
shaobing.cut();
shaobing.bake();
shaobing.box();
}
return shaobing;
}
SimpleShaobingFactory factory;
public ShaobingStore(SimpleShaobingFactory factory){
this.factory = factory;
}
}
如上图所示,不管以后烧饼的口味怎么变,烧饼店的代码都不用变了,要扩展或者修改烧饼,我们只要更改创建烧饼的工厂类,也就是SimpleShaobingFactory 类,这就解开了烧饼店和烧饼的耦合,体现了“对扩展开放,对修改关闭”的设计原则。
其实上面的改进方案使用了一个没有被真正冠名的设计模式“简单工厂模式”,其类图如下所示:
从上面的类图来看,如果我们的烧饼店开在不同的地方,不同地方对洋葱烧饼,酸菜烧饼要求的口味不一样,北方人喜欢放辣椒,南方人喜欢清淡的,我们的烧饼店该怎么开呢?这就是工厂方法模式要帮我们解决的问题,工厂方法模式让类把实例化推迟到子类,让子类决定实例化的类是哪一个,将产品的“实现”从“使用”中解耦出来,让系统同时具备了弹性和扩展性。简单工厂不够弹性,不能改变正在创建的产品(同一种类型的只有一个,拿洋葱烧饼来说,全国各地的口味一样,没有辣与不辣的区分了)
三、结构类图
四、成员角色
抽象创建者(Creator):定义了创建对象模板,实现了所有操纵产品的方法,除了工厂方法。具体创建者必须继承该类,实现工厂方法。
具体创建者(ConcreteCreator):继承抽象创建者,实现工厂方法,负责创建产品对象。
抽象产品(Product):定义了产品的共用资源,提供给子类继承使用,某些方法可以做成抽象方法,强制子类实现。
具体产品(ConcreteProduct):继承自抽象产品,实现父类的抽象方法,也可以覆盖父类的方法,从而产生各种各类的产品。
五、应用实例
下面还是以开烧饼店为例,介绍如何在广州和长沙开烧饼店,卖适合当地风味的烧饼,而且烧饼的种类和名称一样。
首先抽象烧饼店,也就是Creator
package factorymethod.pattern;
public abstract class ShaobingStore {
public Shaobing orderShaobing(String type){
Shaobing shaobing = createShaobing(type);
shaobing.prepare();
shaobing.cut();
shaobing.bake();
shaobing.box();
return shaobing;
}
//未实现的工厂方法
public abstract Shaobing createShaobing(String type);
}
第二步,创建抽象烧饼,也就是Product
package factorymethod.pattern;
public abstract class Shaobing {
//烧饼名称
public String name;
//烧饼用的配料
public String sauce;
//面团
public String dough;
public void prepare(){
System.out.println("Prepareing " + name);
//和面
System.out.println("Kneading dough...");
//加配料
System.out.println("加配料:" + sauce);
}
//烤烧饼
public void bake(){
System.out.println("Bake for 25 minutes at 350C");
}
//切面团
public void cut(){
System.out.println("Cutting the dough into fit slices");
}
//打包
public void box(){
System.out.println("Place shaobing into official box");
}
}
第三步、创建广州风味的烧饼(加番茄酱的洋葱烧饼和牛肉烧饼),对应ConcreteProduct
package factorymethod.pattern;
public class GZOnionShaobing extends Shaobing{
public GZOnionShaobing(){
name = "广州的洋葱烧饼";
//配料
sauce = "番茄酱";
}
}
package factorymethod.pattern;
public class GZBeefShaobing extends Shaobing{
public GZBeefShaobing(){
name = "广州的牛肉烧饼";
//配料
sauce = "番茄酱";
}
}
第四步、创建长沙风味的烧饼(加辣椒酱的洋葱烧饼和牛肉烧饼),对应ConcreteProduct
package factorymethod.pattern;
public class CSOnionShaobing extends Shaobing{
public CSOnionShaobing(){
name = "长沙洋葱烧饼";
//配料
sauce = "辣椒酱";
}
}
package factorymethod.pattern;
public class CSBeefShaobing extends Shaobing{
public CSBeefShaobing(){
name = "长沙牛肉烧饼";
//配料
sauce = "辣椒酱 ";
}
}
第五步、创建广州烧饼店,对应ConcreteCreator
package factorymethod.pattern;
//广州烧饼店
public class GZShaobingStore extends ShaobingStore{
@Override
public Shaobing createShaobing(String type) {
Shaobing shaobing = null;
if("onion".equals(type)){
shaobing = new GZOnionShaobing();
}else if("beef".equals(type)){
shaobing = new GZBeefShaobing();
}
return shaobing;
}
}
第六步、创建长沙烧饼店,对应ConcreteCreator
package factorymethod.pattern;
//长沙烧饼店
public class CSShaobingStore extends ShaobingStore{
@Override
public Shaobing createShaobing(String type) {
Shaobing shaobing = null;
if("onion".equals(type)){
shaobing = new CSOnionShaobing();
}else if("beef".equals(type)){
shaobing = new CSBeefShaobing();
}
return shaobing;
}
}
第七步、测试售出名字相同但风味不一样的烧饼
package factorymethod.pattern;
public class TestShaobingStore {
public static void main(String[] args){
//在广州开一个烧饼店
ShaobingStore gzStore = new GZShaobingStore();
//售出一个洋葱烧饼
gzStore.orderShaobing("onion");
System.out.println("----------------------");
//在长沙开一个烧饼店
ShaobingStore csStore = new CSShaobingStore();
//售出一个洋葱烧饼
csStore.orderShaobing("onion");
}
}
运行结果:
六、工厂方法特有的设计原则
如果我们之间在烧饼店中直接实例化一个烧饼,这种设计师依赖具体类的,类图如下:
这种依赖具体类设计,扩展性、弹性、维护性都比较差。如果将实例化的代码独立出来,使用工厂方法,我们将不再依赖具体类了,请看如下类图:
这就是我们要讲的依赖倒置原则:要依赖抽象,不要依赖具体类。用依赖倒置原则设计的系统,使得对象的实现从使用中解耦,对象的使用是在Creator,实现却在ConcreteCreator中,Creator只有Product的引用,Creator与ConcreteProduct松耦合,这种设计很强的扩展性、弹性和可维护性。
设计中使用以来倒置原则方法:
1、变量不可以持有具体类的引用(就是不能使用new,使用工厂方法)
2、不要让类派生自具体类(继承抽象或者实现接口)
3、不要覆盖基类中已实现的方法
七、优点和缺点
1、优点
(1)、符合“开闭”原则,具有很强的的扩展性、弹性和可维护性。扩展时只要添加一个ConcreteCreator,而无须修改原有的ConcreteCreator,因此维护性也好。解决了简单工厂对修改开放的问题。
(2)、使用了依赖倒置原则,依赖抽象而不是具体,使用(客户)和实现(具体类)松耦合。
(3)、客户只需要知道所需产品的具体工厂,而无须知道具体工厂的创建产品的过程,甚至不需要知道具体产品的类名。
2、缺点
(1)、一个具体产品对应一个类,当具体产品过多时会使系统类的数目过多,增加系统复杂度。
(1)、每增加一个产品时,都需要一个具体类和一个具体创建者,使得类的个数成倍增加,导致系统类数目过多,复杂性增加。
(2)、对简单工厂,增加功能修改的是工厂类;对工厂方法,增加功能修改的是客户端。
八、使用场合
1、当需要一个对象时,我们不需要知道该对象所对应的具体类,只要知道哪个具体工厂可以生成该对象,实例化这个具体工厂即可创建该对象。
2、类的数目不固定,随时有新的子类增加进来,或者是还不知道将来需要实例化哪些具体类。
3、定义一个创建对象接口,由子类决定要实例化的类是哪一个;客户端可以动态地指定工厂子类创建具体产品。