在《Head First 设计模式》一书中,一共介绍了一种类似于工厂模式的编程习惯、两种工厂模式,在这篇文章中,我将对它们分别进行介绍,下面首先来看一下简单工厂。
按照惯例,我们应该先介绍一下简单工厂的定义,然后举例说明,这一次,我们不这样做,我们首先举例说明,然后再引出简单工厂的定义,后续两个工厂模式均按此方式进行叙述,从案例中理解模式的定义才能够更加印象深刻。
假设我们经营一家比萨店,店中提供各种口味的比萨,例如芝士比萨、蛤蜊比萨、蔬菜比萨等等,根据顾客点的比萨类型的不同,店里会为顾客准备不同口味的比萨并为顾客打包装盒。现在我们需要写一套代码来描述顾客预定比萨的过程。
首先,我们来分析一下需求,需求是要能够写代码来描述顾客预定比萨的过程,也就是说比萨的准备、烘焙、切块、打包这几个过程都不能少,很自然的,我们想到写一个类来表示比萨,这个类中包含比萨的所有功能代码,而需求中也描述了比萨会有多种口味,所以我们可以定义一个接口Pizza来表示比萨,并定义多个Pizza的实现类来实现这个接口,下面是我们定义的Pizza接口及其实现类:
/**比萨接口*/
public interface Pizza {
public void prepare();
public void bake();
public void cut();
public void box();
}
/**芝士比萨*/
public class CheesePizza implements Pizza {
@Override
public void prepare() {
System.out.println("开始准备芝士比萨");
}
@Override
public void bake() {
System.out.println("开始烘焙芝士比萨");
}
@Override
public void cut() {
System.out.println("开始将芝士比萨分块");
}
@Override
public void box() {
System.out.println("开始打包芝士比萨");
}
}
/**蛤蜊比萨*/
public class ClamPizza implements Pizza {
@Override
public void prepare() {
System.out.println("开始准备蛤蜊比萨");
}
@Override
public void bake() {
System.out.println("开始烘焙蛤蜊比萨");
}
@Override
public void cut() {
System.out.println("开始将蛤蜊比萨分块");
}
@Override
public void box() {
System.out.println("开始打包蛤蜊比萨");
}
}
/**蔬菜比萨*/
public class VeggiePizza implements Pizza {
@Override
public void prepare() {
System.out.println("开始准备蔬菜比萨");
}
@Override
public void bake() {
System.out.println("开始烘焙蔬菜比萨");
}
@Override
public void cut() {
System.out.println("开始将蔬菜比萨分块");
}
@Override
public void box() {
System.out.println("开始打包蔬菜比萨");
}
}
上面的Pizza接口及其实现类不是工厂模式的重点,属于通用类,在后续介绍中我们还会使用这几个类,所以我们先将定义给出。
定义完比萨类之后,我们开始考虑一下如何才能描述比萨的预定过程,一个比萨的预定过程包括比萨的准备、烘焙、切块、打包,所以我们需要一个方法来将比萨的几个方法包含进去,但是因为比萨有多种,具体选择哪一种比萨,我们需要根据用户预定的类型来决定,所以我们定义一个PizzaStore类,在其中有一个orderPizza()方法,它可以根据用户预定的类型来选择比萨,然后再进行比萨的准备、烘焙等工作。
public class PizzaStore {
public void orderPizza(String type) {
Pizza pizza = null;
if("cheese".equals(type)) {
pizza = new CheesePizza();
} else if ("clam".equals(type)) {
pizza = new ClamPizza();
} else if("veggie".equals(type)) {
pizza = new VeggiePizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
}
上面的代码就已经完成了我们的需求,我们可以根据顾客预定的比萨类型创建不同的比萨,然后对比萨进行准备、烘焙等工作,想一想,上面的这种方式是否存在问题?
上面这种实现方式有一个很明显的问题,因为比萨菜单是动态变化的,所以如果我们需要新增或删除一种比萨类型,都需要修改上面的orderPizza()方法,orderPizza()方法中其他部分都是稳定基本不变的,但是创建比萨的过程不断变化,这样每次增删比萨类型,都要改动PizzaStore类,增加了PizzaStore的维护成本,那么我们是否能够优化一下代码解决上述问题呢?
在上面的例子中,因为创建比萨的过程不断变化,所以我们可以将创建比萨的过程单独剥离出来,专门使用一个类来维护比萨的创建,这样将动态变化的代码与不变的部分分离开,减少了PizzaStore的维护成本,下面是我们创建的一个类SimplePizzaFactory来负责创建比萨:
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;
if("cheese".equals(type)) {
pizza = new CheesePizza();
} else if("clam".equals(type)) {
pizza = new ClamPizza();
} else if("veggie".equals(type)) {
pizza = new VeggiePizza();
}
return pizza;
}
}
可以看到,上面创建比萨的过程和之前PizzaStore中定义的是一样的,其实这也并不奇怪,因为SimplePizzaFactory中创建比萨的过程本就是从PizzaStore中剥离出来的,这样我们就可以修改一下PizzaStore类了:
public class PizzaStore {
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory) {
this.factory = factory;
}
public void orderPizza(String type) {
Pizza pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
}
在这个PizzaStore类中,我们使用SimplePizzaFactory来创建比萨,这样,不管比萨种类如何变化,都无需改动PizzaStore类。
通过上面的方法,我们将创建比萨的过程从PizzaStore中分离出来,这样做有什么好处呢?在上面的例子中,我们好像看不到具体的好处,因为这种方式只不过是将一部分代码搬到另一个类中罢了,需要定义比萨时,反而需要先定义一个新的工厂对象,好像没有第一种实现方式方便,其实不然,在上面这个例子中,我们只在orderPizza()方法中使用了比萨对象,但是实际上可能还会有更多的地方要用到比萨对象,例如比萨菜单、比萨外卖都可能用到比萨对象,这样如果使用第一种实现方式的话,就需要在每次使用比萨对象的地方都写入一段获取对象的逻辑,这样对这段逻辑的维护将会异常繁琐,每次需要新增或删除一种比萨对象的时候都需要修改每个获取对象的逻辑,代码的可维护性会很差,而是用上述方式,我们将获取比萨对象的方法写在一个类中,这样如果需要修改获取对象的逻辑,只需要修改这一个类就可以了,使用起来很方便,代码也就更具有弹性。
看了上述的案例,现在我们来介绍一下简单工厂:其实简单工厂不是一个设计模式,反而比较像一种编程习惯,将创建对象的过程从对象使用者中分离开来,这样就可以通过简单工厂专门负责创建产品了。在上面的例子中,SimplePizzaFactory就是专门负责创建产品的简单工厂。
我们还是通过一个案例引入工厂方法模式的定义,在简单工厂案例的介绍中,我们设计了一个比萨店预定比萨的全部过程,现在假设比萨店扩大经营,在各地开设了分店,每个地区的分店都需要制作出符合当地口味的比萨,例如,对于芝士比萨,在纽约分店和芝加哥分店,它们制作的芝士比萨口味都是不相同的,那么在不同分店预定比萨的过程又是怎样的?
有了简单工厂的知识,我们很容易想到是否可以通过简单工厂来实现这个需求?当然是可以的,针对不同地区的分店,我们可以定义不同的简单工厂,例如纽约和芝加哥分店,我们可以分别定义NYPizzaFactory和ChicagoPizzaFactory用于创建当地风味的比萨,创建比萨的过程和SimplePizzaFactory一样,可以根据顾客预订的比萨类型来创建不同的比萨对象,这种实现方式和第一节中介绍的过程是一样的,在这里我们就不用具体代码进行描述了。
除了这一种实现方式,我们是否还有其他的实现方式呢?因为通过简单工厂创建比萨都是需要通过外部类来实现,我们是否能够不通过外部类,而是将比萨制作活动局限于PizzaStore中,同时又能让各地区的加盟店自由地制作该区域风味的比萨呢?
我们首先考虑一下,要将比萨制作活动局限于PizzaStore中,同时还要能够保持各加盟店的自由度,我们可以将比萨制作过程与比萨预定流程剥离开,因为比萨预定流程每个加盟店都是固定的,唯一的不同就是各个加盟店制作的比萨风味不同,所以我们可以将比萨的创建过程和使用过程分离开,这种思想在简单工厂中也有体现,但是这一次我们不再单独用一个类来负责比萨的创建,而是将创建比萨的方法声明为抽象方法:
public abstract class PizzaStore {
public abstract Pizza createPizza(String type);
public void orderPizza(String type) {
Pizza pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
}
可以看到,在这里我们将PizzaStore声明为抽象类,并定义了抽象方法createPizza()来负责比萨的创建,而在orderPizza()方法中,我们使用createPizza()创建的比萨来进行准备、烘焙、切片和打包,其实orderPizza()方法也不知道createPizza()方法创建的比萨是哪一种,它是由PizzaStore的子类来决定的。接下来我们就可以定义两个PizzaStore的子类分别表示纽约分店和芝加哥分店:
/**纽约分店*/
public class NYPizzaStore extends PizzaStore {
@Override
public Pizza createPizza(String type) {
Pizza pizza = null;
if("cheese".equals(type)) {
pizza = new NYCheesePizza();
} else if("clam".equals(type)) {
pizza = new NYClamPizza();
} else if("veggie".equals(type)) {
pizza = new NYVeggiePizza();
}
return pizza;
}
}
/**芝加哥分店*/
public class ChicagoPizzaStore extends PizzaStore {
@Override
public Pizza createPizza(String type) {
Pizza pizza = null;
if("cheese".equals(type)) {
pizza = new ChicagoCheesePizza();
} else if("clam".equals(type)) {
pizza = new ChicagoClamPizza();
} else if("veggie".equals(type)) {
pizza = new ChicagoVeggiePizza();
}
return pizza;
}
}
在这两个子类中都实现了createPizza()方法,用于根据顾客的预定类型创建特定区域风味的比萨,在这个过程中,创建什么比萨都是由子类决定的。
在上面的案例中,createPizza()就是工厂方法,工厂方法是用于处理对象的创建,并将这样的行为封装在子类中,这样,程序中关于超类的代码就和子类对象创建代码解耦了。
工厂方法模式 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
在上面的定义中讲到,工厂方法模式让子类决定要实例化的类是哪一个,这里的决定并不是允许子类在运行时做决定,而是在编写创建者类时,不需要知道实际创建的产品是哪一个,选择了使用哪个子类,自然就决定了实际创建的产品是什么。工厂方法模式通过让子类决定创建的对象是什么,来达到将对象创建的过程封装的目的。
工厂方法模式帮助我们将产品的“实现”从“使用”中解耦,如果增加或改变产品,产品的“使用”将不会受到影响。下面的类图就描述了工厂方法模式(注:图片摘自《Head First 设计模式》):
我们仍然以比萨为例,现在我们有了创建比萨的方法,但是每一种比萨都由多种配料组成,例如面团、干酪、蔬菜、蛤蜊等,每个地区分店所用的材料不同,例如蛤蜊,纽约地区通常使用新鲜蛤蜊,而芝加哥地区通常就是用冷冻蛤蜊,我们需要一个工厂专门负责生产各种原料,且不同地区生产的原料是不同的。我们想一下这个需求我们该如何实现?
我们先来分析一下需求,我们现在需要一个工厂专门负责生产各种原料,所以我们可以定义一个接口,其中包含所有创建原料的方法:
public interface PizzaIngredientFactory {
public Cheese createCheese();
public Dough createDough();
public Veggie createVeggie();
public Clam createClam();
}
在这里,我们省略了原材料类的定义,这些类都是一些简单的Java类,不在这里赘述了。有了创建原料的接口之后,我们就可以针对不同的地区写不同的实现类用于生产原料,我们先定义两个实现类:纽约的材料工厂和芝加哥的材料工厂:
/**纽约材料工厂*/
public class NyPizzaIngredientFactory implements PizzaIngredientFactory {
@Override
public Cheese createCheese() {
return new ReggianoCheese();
}
@Override
public Dough createDough() {
return new ThinCrustDough();
}
@Override
public Veggie createVeggie() {
return new NyStyleVeggie();
}
@Override
public Clam createClam() {
return new FreshClam();
}
}
/**芝加哥材料工厂*/
public class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory {
@Override
public Cheese createCheese() {
return new MozzarellaCheese();
}
@Override
public Dough createDough() {
return new ThickCrustDough();
}
@Override
public Veggie createVeggie() {
return new ChicagoStyleVeggie();
}
@Override
public Clam createClam() {
return new FrozenClam();
}
}
在这两个实现类中,我们分别实现了材料的纽约版本和芝加哥版本,例如,对于面团,纽约工厂生产的是薄面团,而芝加哥工厂生产的是厚面团,对于蛤蜊,纽约工厂生产新鲜蛤蜊,芝加哥工厂生产冷冻蛤蜊等。现在我们已经完成了原料的生产了,我们可以重新定义一下Pizza类,向其中添加原材料:
public abstract class Pizza {
Cheese cheese;
Dough dough;
Veggie veggie;
Clam clam;
public abstract void prepare();
public void bake() {
System.out.println("开始烘焙");
}
public void cut() {
System.out.println("开始将比萨切块");
}
public void box() {
System.out.println("开始打包比萨");
}
}
和之前的例子不同,我们将Pizza声明为一个抽象类,并在其中定义了一系列的原材料属性,另外,我们将prepare()方法声明为抽象类型的,这样,子类实现Pizza类的时候必须要实现prepare()方法,在prepare()方法中用来准备比萨的原材料,下面是我们实现的两个比萨类:
/**芝士比萨*/
public class CheesePizza extends Pizza {
PizzaIngredientFactory factory;
public CheesePizza(PizzaIngredientFactory factory) {
this.factory = factory;
}
@Override
public void prepare() {
cheese = factory.createCheese();
veggie = factory.createVeggie();
dough = factory.createDough();
}
}
/**蛤蜊比萨*/
public class ClamPizza extends Pizza {
PizzaIngredientFactory factory;
public ClamPizza(PizzaIngredientFactory factory) {
this.factory = factory;
}
@Override
public void prepare() {
cheese = factory.createCheese();
veggie = factory.createVeggie();
dough = factory.createDough();
clam = factory.createClam();
}
}
在这两个实现类中,我们都定义了一个原材料生产工厂,当比萨需要什么材料时,我们都可以通过工厂获得,另外,我们也无需关心材料工厂生产的是什么地区的材料,它是可以通过运行时指定的。
抽象工厂模式 提供了一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
上面的定义描述的比较抽象,我们可以结合上一节介绍的实例来进行分析,在上一节中我们需要一个工厂来生产比萨的原材料,原材料有多种,这就类似于一个对象的家族,所以我们定义了一个PizzaIngredientFactory接口用于生产原材料,但是原材料的具体类型则是由PizzaIngredientFactory的实现类决定的,客户在使用PizzaIngredientFactory创建原材料时,完全不用实例化具体的材料,因为选择了指定的工厂就会生产出相应的原材料。
抽象工厂模式允许客户使用抽象接口来创建一组相关的产品,而不需要知道实际产出的具体产品是什么,这样一来,客户就从具体的产品中被解耦。下图就描述了抽象工厂模式中的关系(注:图片摘自《Head First 设计模式》):
这一章中我们主要介绍了简单工厂、工厂方法模式和抽象工厂模式。
简单工厂不是设计模式,是一种编程习惯,简单工厂将对象的创建过程与使用过程隔离开,单独使用一个类来负责创建产品,这样使代码的可维护性更强,但是简单工厂一个最大的缺点在于如果需要修改产品就需要必须要修改工厂类。
工厂方法模式的思想与简单工厂是类似的,都是将创建产品的过程与使用产品隔离开,不同的是,工厂方法模式将创建产品的方法在子类中实现,这样,如果新增一个产品,我们完全可以不用修改原有的工厂,直接新建一个新的工厂子类来实现创建产品的方法即可。
抽象工厂模式的任务是定义一个负责创建产品组的接口,我们通过上面的例子可以看到,抽象工厂使用了工厂方法来创建产品,它使用工厂方法只是为了创建产品,而工厂方法模式中抽象创建者中所实现的代码通常会用到子类所创建具体类型,抽象工厂模式有一个缺点在于如果我们需要新增一个产品,就需要修改接口及其所有的实现类,这可能是一项很繁重的工作。