一、通过例子了解工厂模式:
假设有一个披萨店,下面,我们就“做披萨”这件事来进行代码的分析。(注:在这里,我们把做披萨分为四个步骤:准备Prepare、烘烤Bake、切片Cut和装盒Box)
刚开始,我们的店里面只有CheesePizza、GreekPizza和VeggiePizza三种披萨,在这种情况下,我们的代码可能这么写:
1 public class PizzaStore { 2 public Pizza orderPizza(String pizzaType) { 3 Pizza pizza = null; 4 5 // 根据披萨的类型实例化不同的披萨 6 if ("cheese".equals(pizzaType)) { 7 pizza = new CheesePizza(); 8 } else if ("greek".equals(pizzaType)) { 9 pizza = new GreekPizza(); 10 } else if ("veggie".equals(pizzaType)) { 11 pizza = new VeggiePizza(); 12 } 13 14 pizza.prepare(); 15 pizza.bake(); 16 pizza.cut(); 17 pizza.box(); 18 19 return pizza; 20 } 21 }
现在,我们的披萨店只有“做披萨”这项工作,所以,现在的代码看似编写的非常成功,但是,这里面隐藏着一个巨大的问题——那就是扩展性太差。试想,如果将来我们的披萨店提供“披萨外卖”服务(对于不同的披萨提供不同的运输机制),那么我们的“根据披萨类型实例化不同的披萨”这段代码就必须要再写一遍。如果再来一些其他的服务(加入说20个),这个时候如果披萨菜单发生变化(新加入了2中披萨),那么我们就要把与之相关的20段代码统统改一遍,枯燥无味不说,一旦改错了什么地方,那么我们就又要花费更多的时间去修改BUG,得不偿失。
面对这个问题,我们最好的解决方案就是使用面向对象思想中的封装。我们把根据披萨类型实例化不同的披萨这段代码从PizzaStore中抽取出来,放到另外一个类中,这个类专门负责创建披萨对象,我们把这个类成为“工厂”。“工厂”是负责处理创建对象细节的一个类,当对一类对象创建工厂之后,那么项目中的所有该类对象都要由这个工厂进行“生产”。简单的说,在项目中,我们用工厂方法来封装“NEW”这个关键字。下面,我们就来看看我们的第一个工厂中的代码:
1 public class PizzaFactory { 2 public Pizza createPizza(String pizzaType) { 3 Pizza pizza = null; 4 // 根据披萨的类型实例化不同的披萨(从PizzaStore中剪切出来的代码) 5 if ("cheese".equals(pizzaType)) { 6 pizza = new CheesePizza(); 7 } else if ("greek".equals(pizzaType)) { 8 pizza = new GreekPizza(); 9 } else if ("veggie".equals(pizzaType)) { 10 pizza = new VeggiePizza(); 11 } 12 return pizza; 13 } 14 }
从目前的情况来看,我们把这段代码独立出来纯粹是多此一举——并没有什么卵用。那么这样做到底有什么作用呢?大家不要忘了,我们的程序现在还只有一个功能(做披萨),将来,随着需求的改变,我们可能会再添加一些其他的功能,比如说,建立一个披萨菜单的功能,把所有披萨都显示到一个菜单上供顾客阅览;还可以添加一个“披萨外卖”的功能,等等等等。所以,我们把创建披萨的代码装进这样一个工厂类,当以后实现发生改变时,我们只需要修改这里的代码即可。经过我们这一番修改,PizzaStore类中的代码就可以拜托实例化(NEW关键字),而直接使用工厂来创建实例了:
1 public class PizzaStore { 2 private PizzaFactory factory; 3 4 // 初始化披萨店的时候顺便给披萨店绑定一个披萨工厂 5 public PizzaStore(PizzaFactory factory) { 6 this.factory = factory; 7 } 8 9 public Pizza orderPizza(String pizzaType) { 10 Pizza pizza = null; 11 12 // 我们把new关键字提取出来放到披萨工厂中,这里不再使用具体的实例化 13 pizza = factory.createPizza(pizzaType); 14 15 pizza.prepare(); 16 pizza.bake(); 17 pizza.cut(); 18 pizza.box(); 19 20 return pizza; 21 } 22 }
到此,我们来捋一捋我们所做的:首先,我们有一个披萨店(PizzaStore),用它来生产种类不同的披萨;我们还有一些披萨(Pizza),它们是我们的主营产品,有很多不同的口味;我们还有一个披萨工厂(PizzaFactory),用它来统一的生产披萨。这三个类的关系是:披萨店借由披萨工厂来上产披萨,它们具体的关系请看下面的类图:
至此,我们已经大致解决了最初的那个问题,我们可以轻松的应对新加入的披萨,也可以轻松的应对其他功能的接入。但是,问题来了。随着我们披萨店的经营,我们逐渐的扩大经营范围,使我们的披萨店成为了一个风靡全国的品牌,在全国各地都有分店,为了满足不同地区人的饮食习惯,我们决定让菜单“地域化”,即名字还是那些名字,但是做法要适当的改变,比如四川人爱吃辣,所以我们就在CheesePizza中适当的加入辣椒;山东人爱吃咸,所以我们在CheesePizza中是电工的加入盐,另外,在包装、切片等工序上,不同的地方也会有差异。为了兼容这些差异,我们必须要设计一套非常有弹性的代码。
我们可以考虑把PizzaStore升级成父类,把获取Pizza对象的方法抽取出来并设置为抽象方法;让各个地方的披萨店继承这个父类,并实现抽象方法。这样一来,披萨工厂就可以移动到各个地方的披萨店类中了。也就是说,在第二次更改后,实例化披萨对象的责任被移到了一个方法中,这个方法就如同是一个“工厂”。下面展示第二次修改后的披萨店父类PizzaStore和四川披萨店子类SCPizzaStore中的代码:
1 public abstract class PizzaStore { 2 3 public Pizza orderPizza(String pizzaType) { 4 Pizza pizza = null; 5 6 // 用定义的抽象方法从子类中获取披萨对象 7 pizza = createPizza(pizzaType); 8 9 pizza.prepare(); 10 pizza.bake(); 11 pizza.cut(); 12 pizza.box(); 13 14 return pizza; 15 } 16 17 protected abstract Pizza createPizza(String pizzaType); 18 }
1 public class SCPizzaStore extends PizzaStore { 2 3 protected Pizza createPizza(String pizzaType) { 4 if ("cheese".equals(pizzaType)) { 5 return new SCCheesePizza(); 6 } else if ("greek".equals(pizzaType)) { 7 return new SCGreekPizza(); 8 } else if ("veggie".equals(pizzaType)) { 9 return new SCVeggiePizza(); 10 } 11 return null; 12 } 13 }
PizzaStore类中的orderPizza()方法对Pizza对象做了很多事情(例如准备、烘烤、切片、装盒),但是由于Pizza对象是抽象的,所以orderPizza()对象并不知道哪些实际的具体类参与进来了,这就是解耦。当PizzaStore类调用orderPizza()方法时,某个披萨店子类将负责创建披萨。具体做哪种披萨,这个是由披萨店和“顾客”的选择共同决定的。可见,工厂方法用来处理对象的创建,并将这样的行为放在子类中,这样,客户程序中有关超类的代码就和子类中创建对象的代码解耦了。
说了这么半天,到现在还没有贴出Pizza实体类的代码。Pizza类和其一个子类SCCheesePizza类中的代码如下:
1 public class Pizza { 2 protected String name; // 名称 3 protected String dough; // 面团类型 4 protected String sauce; // 酱料类型 5 protected String toppings; // 一套佐料 6 7 public void prepare() { 8 System.out.println("Preparing " + name + "..."); 9 System.out.println("Tossing dough..."); 10 System.out.println("Adding sauce..."); 11 System.out.println("Adding toppings:" + toppings + "..."); 12 } 13 14 public void bake() { 15 System.out.println("Bake for 25 minutes at temperature 350..."); 16 } 17 18 public void cut() { 19 System.out.println("Cutting the pizza into diagonal slices..."); 20 } 21 22 public void box() { 23 System.out.println("Place pizza in official PizzaStore box"); 24 } 25 26 public String getName() { 27 return this.name; 28 } 29 }
1 public class SCCheesePizza extends Pizza { 2 3 public SCCheesePizza() { 4 super.name = "Sichuan Style Cheese Pizza"; 5 super.dough = "Thin Crust Dough"; 6 super.sauce = "Marinara Sauce"; 7 super.toppings = "Grated Reggiano Cheese"; 8 } 9 10 // 覆盖父类中的cut()方法,将披萨切成方形 11 public void cut() { 12 System.out.println("Cutting the pizza into square slices..."); 13 } 14 }
下面,我们来建立一个测试类MainClass,测试我们上面写的代码:
1 public class MainClass { 2 3 public static void main(String[] args) { 4 // 创建一个四川风味的披萨店 5 PizzaStore store = new SCPizzaStore(); 6 // 订购一个CheesePizza 7 Pizza pizza = store.orderPizza("cheese"); 8 9 System.out.println("A " + pizza.getName() + " has been put on your desk."); 10 } 11 }
二、深入了解工厂模式:
所有的工厂模式都用来封装对象的创建。工厂模式可以分为工厂方法模式和抽象工厂模式。
(一)工厂方法模式:
工厂方法模式 通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的(这个定义是暂时的,下面我们会给出正式的定义)。上面的“披萨店”的例子就是用工厂方法模式解决的。在这个例子中,我们创建了两类事物——创建者类(披萨店)和产品类(披萨)。具体来说,PizzaStore类是一个抽象的创建者类,它定义了一个抽象的工厂方法createPizza(),让子类实现它并创建产品(披萨),这个抽象类是用来制造产品的,但它不需要知道具体制造哪种具体的产品,它的子类需要知道;SCPizzaStore是PizzaStore许多子类中的一个,它与其他的子类一样,可以生产具体的产品,所以,它们又被成为具体创建者,也正因如此,每个地方特色的披萨店才可以利用实现自父类的createPizza()方法来生产自己地区的风味披萨。从水平的角度来分析,PizzaStore和Pizza类都是抽象类,在这两个类中定义了抽象的方法,供其子类实现。
有人可能会认为,当创建者类只有一个子类的时候,工厂模式就失去其效用了,这种方法是不对的。相比于直接创建对象,工厂方法有其优势,尽管只有一个创建者类,工厂方法依然很有用,因为它帮助我们将产品的“实现”从“使用”中解耦,另外,如果增减产品或者改变产品的实现,创建者类也不会受到影响。
下面我们来给出工厂方法模式的正式定义:工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。简单的说,工厂方法让类把实例化推迟到子类中进行。将创建对象的方法集中在一个方法中,可以避免代码的重复,并且更方便以后的维护,这也意味着客户在实例化对象时,只会依赖于接口,而不是具体类。回到上面的类图,抽象的PizzaStore提供了一个创建对象的方法的接口,也成为“工厂方法”。在抽象的PizzaStore中,任何其他实现的方法都可能使用到这个工厂方法所制造出来的产品,但只有子类真正实现这个工厂方法并创建产品。我们常说,“工厂方法让子类决定要实例化的类是哪一个”,所谓的“决定”,并不是指模式允许子类本身在运行时做决定,而是指在编写创建者类时,不需要知道实际创建的产品是哪一个——选择了使用哪个类,自然就决定了创建的产品是什么。在这里顺便声明一下,工厂方法和前面提到的简单工厂虽然看上去很相似,但其实不是一回事。简单工厂是把全部事情在一个地方都处理完了,而工厂方法却是创建一个框架,让子类决定如何实现。换言之,简单工厂可以把对象的创建封装起来,但是简单工厂不具备工厂方法的弹性,因为简单工厂不能变更正在创建的产品。
利用上面的理论,我们可以扩展前面的程序,比如:我们给每个地区的披萨店都提供原料源,这样一来,我们就需要再次使用工厂模式来创建原料的工厂。
(二)抽象工厂模式:
先来看定义:抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道(或关心)实际产出的具体产品是什么。这样一来,客户就从具体的产品中被解耦。抽象工厂的任务是定义一个负责创建一组产品的接口,这个接口内的每个方法都负责创建一个具体的产品,同时我们通过利用实现抽象工厂接口的子类来提供这些具体的做法。也就是说,前面提到的“材料工厂”,更适合用抽象工厂模式进行处理。
(三)工厂方法模式和抽象工厂模式的异同与关系:
@相同点:两种模式都是负责将程序从特定的实现中解耦
@不同点:工厂方法模式完成解耦操作的方法是使用继承,而抽象工厂模式使用的方法是对象的组合;利用工厂方法模式创建对象,需要扩展一个类,并覆盖它“创建对象”的方法,而抽象工厂模式则是提供一个用来创建一个产品家族的抽象类型(可以把一群相关的产品集合起来);当有新的产品加入时,抽象工厂方法必须改变总的接口和每个子类的接口(毕竟是很多对象的集合),而工厂方法模式只需要改变工厂类中的工厂方法即可。
@关系:抽象工厂模式常常需要使用工厂方法来实现具体的工厂。