设计模式(四):工厂方法模式(解析设计原则)
一、概述
工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
二、解决问题
通常我们需要一个对象的时候,会想到使用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、定义一个创建对象接口,由子类决定要实例化的类是哪一个;客户端可以动态地指定工厂子类创建具体产品。