初识设计模式 chapter 04-工厂模式
1 引言
除了使用new操作符之外,还有更多制造对象的方法。你将了解到实例化这个活动不应该总是公开地进行,也会认识到初始化经常造成耦合问题。你不希望这样,对吧?读下去,你将了解工厂模式如何从复杂的依赖中帮你脱困。
2 正文
2.1 new有什么不对劲
当有一群相关的具体类时,通常会写出这样的代码:
Duck duck;
if (picnic)
{
duck = new MallardDuck();
}
else if (hunting)
{
duck = new DecoyDuck();
}
else if (inBathTub)
{
duck = new RubberDuck();
}
这里有一些要实例化的具体类,究竟实例化哪个类,要在运行时由一些条件决定。
当看到这样的代码,一旦有变化或扩展,就必须重新打开这段代码进行检查和修改。通常这样修改过的代码将造成部分系统更难维护和更新,而且更容易犯错。
但是,总是要创建对象吧!而Java只提供一个new关键词创建对象,不是吗?还能有些什么?
在技术上,new没有错,毕竟这是Java的基础部分。真正的犯人是我们的老朋友“change",以及它是如何影响new的使用的。
针对接口编程,可以隔离掉以后系统可能发生的一大堆改变。为什么呢?如果代码是针对接口而写,那么通过多态,它可以与任何新类实现该接口。但是,当代码使用大量的具体类时,等于是自找麻烦,因为一旦加入新的具体类,就必须改变代码。也就是说,你的代码并非”对修改关闭“。想用新的具体类型来扩展代码,必须重新打开它。
所以,该怎么办?当遇到这样的问题时,就应该回到OO设计原则去寻找线索。别忘了,我们的第一个原则用来处理改变,并帮助我们”
找出会变化的方面,把它们从不变的部分分离出来“。
2.2 识别变化的部分
假设你又一个披萨店,身为对乡村内最先进的披萨主任,你的代码可能这么写
public Pizza orderPizza(String type)
{
Pizza pizza;
//根据顾客的口味匹配不同种类的披萨
//问题出现了,随着时间的变化,披萨的菜单会改变,这里就必须已改再改
if (type.equals("cheese"))
{
pizza = new CheesePizza();
}
else if
{
}
//这个是我们不想改变的地方。因为披萨的准备、烘烤、切片、包装,多年来都保持不变,
//所以这部分代码不会改变,只有发生这些动作的披萨会改变
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
很明显,如果实例化”某些“具体类,将使orderPizza()出问题,而且也无法让orderPizza()对修改关闭;但是,现在我们已经知道
哪些会改变,哪些不会改变,该是试用封装的时候了。
2.3 建立一个简单披萨工厂
首先,把创建对象的代码从orderPizza()中抽离,然后把这部分的代码搬到另一个对象中,这个新对象只管如何创建披萨。如果任何对象想要创建披萨,找它就对了。
public class SimplePizzaFactory {
//SimplePizzaFactory只做一件事情,帮它的客户创建Pizza
//首先,在这个工厂内定义一个createPizza()方法。所有客户用这个方法来实例化新对象
public Pizza createPizza(String type) {
Pizza pizza = null;
//这个代码没有什么变动,和原本orderPizza()方法中的代码一样,依然是以pizza的类型为参数
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
} else if (type.equals("clam")) {
pizza = new ClamPizza();
} else if (type.equals("veggie")) {
pizza = new VeggiePizza();
}
return pizza;
}
}
除此之外,利用静态方法定义一个简单的工厂,这是很常见的技巧,常被称为静态工厂。为何使用静态方法?因为不需要使用创建对象的方法来实例化对象。但请记住,这也有缺点,不能通过继承来改变创建方法的行为。
重做PizzaStore类
public class PizzaStore {
//为PizzaStore加上一个对SimplePizzaFactory的引用
SimplePizzaFactory factory;
//PizzaStore的构造器,需要一个工厂作为参数
public PizzaStore(SimplePizzaFactory factory) {
this.factory = factory;
}
public Pizza orderPizza(String type) {
Pizza pizza;
//而orderPizza()方法通过简单传入订单类型来使用工厂创建pizza
//请注意,我们把new操作符替换成工厂对象的创建方法,这里不再使用具体实例化!
pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
2.4 定义简单工厂
简单工厂其实不是一个设计模式,反而比较像是一种编程习惯。有些开发人员的确是把这个编程习惯误认为是”工厂模式“(Factory Patter)。
不要因为简单工厂不是一个”真正的“设计模式,就忽略了他的用法。谢谢简单工厂来为我们暖身。接下来登场的是两个重量级的模式,他们都是工厂。
2.5 工厂方法模式
对象村Pizza店经营又称,现在大家都希望对象村Pizza点能够在自家附近有加盟店。
其中一件加盟店希望工厂能制造纽约风味的Pizza:薄饼、美味的酱料和少量的芝士;另一家加盟店希望能制造芝加哥风味的Pizza:厚饼、重味的酱料和大量的芝士。
如果
利用SimplePizzaFactory,写出三种不用的工厂,那么各地的加盟店都有自己适合工厂可以使用,这是一种做法。
在推广SimpleFactory时,你发现加盟店的确是你采用你的工厂创建Pizza,但是其他部分,却开始
采用他们自创的流程:烘烤的做法有些差异、不要切片、使用其他厂商的盒子。
再想想这个问题,你真的希望能够
建立一个框架,把加盟店和创建pizza绑在一起的同时又保持一定的弹性。
工厂方法类的设计
public abstract class PizzaStore {
/*
* 工厂方法用来处理对象的创建,并将这样的行为封装在子类中。
* 这样,客户程序中关于超类的代码就和子类对象创建代码解耦了。
* 1、工厂方法是抽象的,所以依赖子类来处理对象的创建;
* 2、工厂方法必须返回一个产品。超类中定义的方法,通常使用到工厂方法的返回值;
* 3、工厂方法将客户(也就是超类中的代码,例如orderPizza())和实际创建具体产品的代码分离
*/
abstract Pizza createPizza(String item);
public Pizza orderPizza(String type) {
Pizza pizza = createPizza(type);
System.out.println("--- Making a " + pizza.getName() + " ---");
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
刚刚忽略了一件重要的事,Pizza本身。
public abstract class Pizza {
//每个Pizza都具有名称、面团类型、酱料类型、一套酢料
String name;
String dough;
String sauce;
ArrayList toppings = new ArrayList();
void prepare() {
System.out.println("Preparing " + name);
System.out.println("Tossing dough...");
System.out.println("Adding sauce...");
System.out.println("Adding toppings: ");
for (int i = 0; i < toppings.size(); i++) {
System.out.println(" " + toppings.get(i));
}
}
void bake() {
System.out.println("Bake for 25 minutes at 350");
}
void cut() {
System.out.println("Cutting the pizza into diagonal slices");
}
void box() {
System.out.println("Place pizza in official PizzaStore box");
}
public String getName() {
return name;
}
public String toString() {
StringBuffer display = new StringBuffer();
display.append("---- " + name + " ----\n");
display.append(dough + "\n");
display.append(sauce + "\n");
for (int i = 0; i < toppings.size(); i++) {
display.append((String )toppings.get(i) + "\n");
}
return display.toString();
}
}
以下罗列一些具体的pizza子类
public class NYStyleCheesePizza extends Pizza {
public NYStyleCheesePizza() {
name = "NY Style Sauce and Cheese Pizza";
dough = "Thin Crust Dough";
sauce = "Marinara Sauce";
toppings.add("Grated Reggiano Cheese");
}
}
public class ChicagoStyleCheesePizza extends Pizza {
public ChicagoStyleCheesePizza() {
name = "Chicago Style Deep Dish Cheese Pizza";
dough = "Extra Thick Crust Dough";
sauce = "Plum Tomato Sauce";
toppings.add("Shredded Mozzarella Cheese");
}
//这个Pizza覆盖了cut()方法,将Pizza切成了正方形
void cut() {
System.out.println("Cutting the pizza into square slices");
}
}
具体的PizzaStroe类
public class NYPizzaStore extends PizzaStore {
Pizza createPizza(String item) {
if (item.equals("cheese")) {
return new NYStyleCheesePizza();
} else if (item.equals("veggie")) {
return new NYStyleVeggiePizza();
} else if (item.equals("clam")) {
return new NYStyleClamPizza();
} else if (item.equals("pepperoni")) {
return new NYStylePepperoniPizza();
} else return null;
}
}
测试类
public class PizzaTestDrive {
public static void main(String[] args) {
PizzaStore nyStore = new NYPizzaStore();
PizzaStore chicagoStore = new ChicagoPizzaStore();
Pizza pizza = nyStore.orderPizza("cheese");
System.out.println("Ethan ordered a " + pizza.getName() + "\n");
pizza = chicagoStore.orderPizza("cheese");
System.out.println("Joel ordered a " + pizza.getName() + "\n");
}
认识工厂方法模式的时刻终于到了:
所有工厂模式都用来封装对象的创建。
工厂方法模式(Factory Method Pattern)通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的。
让我们来看下有哪些组成元素:
1、
创建者类(creator):PizzaStore、NYPizzaStore、ChicagoPizzaStore
PizzaStore是抽象创建者类,
它定义了一个抽象的工厂方法,让子类实现此方法制造产品。创建者通常会包含依赖于抽象产品的代码,而这些抽象产品由子类制造。创建者不需要真的知道在制造哪种具体产品。
NYPizzaStore、ChicagoPizzaStore是具体创建者类,
createPizza()方法是真正的工厂方法,用来制造产品。因为每个加盟店都有自己的PizzaStore子类,所以可以利用实现createPizza()方法创建自己风味的Pizza。
2、
产品类:Pizza以及各个加盟店的产品类(Pizza子类)
平行的类层级:我们已经看到,将一个orderPizza()方法和一个工厂方法联合起来,就可以成为一个框架。除此之外,工厂方法将生产知识封装进各个创建者,这样的做法,也可以被视为一个框架。
定义工厂方法模式:工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
根据上面PizzaStore的分工我们不难理解,首先有两个很重要父类,FactoryCreator(抽象接口)和Product(产品),通过抽象工厂方法让子类实现制造产品(继承于Product)。
简单工厂和工厂方法模式的区别:
简单工厂把全部事情,都在一个地方处理完了。然而工厂方法却是创建一个框架,让子类决定要如何实现。比方说,在工厂方法中,orderPizza()方法提供了一般的框架,以便创建Pizza,orderPizza()方法依赖工厂方法创建具体类,并制造出实际的Pizza。可通过继承PizzaStore类,决定实际制造出的Pizza是什么。简单工厂的做法,可以将对象的创建封装起来,但是简单工厂不具备工厂方法的弹性,因为简单工厂不能变更正在创建的产品。
2.6 抽象工厂模式
代码里减少对于具体类的依赖是件”好事“。事实上有一个OO设计原则就正式阐明了这一点;这个原则甚至还有一个又响亮又正式的名称:依赖倒置原则(Dependency Inversion Principle)。
依赖倒置原则:要依赖抽象,不要依赖具体类。
首先,这个原则听起来很像是“针对接口编程,不针对实现编程”,不是吗?的确很相似,然而这里更强调抽象。这个原则说明了:不能让高层组件依赖低层组件,而且,不管高层或者低层组件,”两者“都应该依赖于抽象。
所谓”高层“组件,是由其他低层组件定义其行为的类。例如,PizzaStore是个高层组件,因为它的行为是由Pizza定义的:PizzaStore创建所有不同的Pizza对象,准备、烘烤、切片、装盒;而Pizza本身属于低层组件。
这到底是什么意思?
这个嘛,让我们再看一次上文创建这类和产品类的关系,PizzaStore是”高层组件”,而Pizza实现是“低层组件”,很清楚地,PizzaStore依赖这些具体Pizza类。
现在,这个原则告诉我们,应该重写代码以便于我们依赖抽象类,而不依赖具体类。对于高层及低层模块都应该如此。
依赖倒置原则,究竟倒置在哪里?
在依赖倒置原则中的倒置指的是和一般OO设计的思考方法完全相反。低层组件居然依赖高层的抽象,同样地,高层组件现在也依赖相同的抽象。前面的依赖是从上而下的,现在却倒置了,而且高层与低层模块现在都依赖这个抽象。
几个指导方针帮助你遵循此原则:
1、变量不可以持有具体类的引用。如果使用new,就会持有具体类的引用。你可以改用工厂来避开这样的做法。
2、不要让类派生自具体类。如果派生自具体类,你就会依赖具体类。请派生自一个抽象(接口或者抽象类)。
3、不要覆盖基类中已实现的方法。如果覆盖基类中已实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该由所有的子类共享。
应该尽量达到这个原则,而不是随时都要遵循这个原则。我们都很清楚,任何java程序都有违反这些指导方针的地方。
但是,如果你深入体验这些方针,将这些方针内化成你思考的一部分,那么在设计时,你将指导何时有足够的理由违反这样的原则。比如说,如果有一个不像是会改变的类,那么在代码中直接实例化具体类也就没什么大碍。比如实例化字符串对象。
另一方面,如果有个类可能改变,你可以采用一些好技巧(例如工厂方法)来封装变化。
再回到Pizza店
现在,对象村Pizza店的成功关键在于新鲜、高质量的原料,而且通过导入新的框架,加盟店将遵循你的流程,但是有一些加盟店,使用低价原料来增加利润。你必须采取一些手段,以免长此以往毁了对象村的品牌。
你打算建造一家生产原料的工厂,并将原料运送到各家加盟店。加盟店坐落在不同的区域,每个地区的原料是不一样的,所以对于不同的地区(纽约、芝加哥),你准备了两组不同的原料。
建造原料工厂
public interface PizzaIngredientFactory {
//在接口中,每个原料都有一个对应的方法创建该原料
//如果每个工厂实例内都有某一种通用的“机制”需要实现,就可以把这个例子改写成抽象类
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClam();
}
纽约原料工厂
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
//对于原料家族内的每一种原料,我们都提供了纽约的版本
public Dough createDough() {
return new ThinCrustDough();
}
public Sauce createSauce() {
return new MarinaraSauce();
}
public Cheese createCheese() {
return new ReggianoCheese();
}
public Veggies[] createVeggies() {
Veggies veggies[] = { new Garlic(), new Onion(), new Mushroom(), new RedPepper() };
return veggies;
}
public Pepperoni createPepperoni() {
return new SlicedPepperoni();
}
public Clams createClam() {
return new FreshClams();
}
}
重做Pizza类
public abstract class Pizza {
String name;
//每个Pizza都有一组在准备时会用到的原料
Dough dough;
Sauce sauce;
Veggies veggies[];
Cheese cheese;
Pepperoni pepperoni;
Clams clam;
//现在把prepare()方法生命诚抽象。在这个方法中,我们需要收集披萨所需的原料,而这些原料当然是来自原料工厂了。
abstract void prepare();
void bake() {
System.out.println("Bake for 25 minutes at 350");
}
void cut() {
System.out.println("Cutting the pizza into diagonal slices");
}
void box() {
System.out.println("Place pizza in official PizzaStore box");
}
void setName(String name) {
this.name = name;
}
String getName() {
return name;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("---- " + name + " ----\n");
if (dough != null) {
result.append(dough);
result.append("\n");
}
if (sauce != null) {
result.append(sauce);
result.append("\n");
}
if (cheese != null) {
result.append(cheese);
result.append("\n");
}
if (veggies != null) {
for (int i = 0; i < veggies.length; i++) {
result.append(veggies[i]);
if (i < veggies.length-1) {
result.append(", ");
}
}
result.append("\n");
}
if (clam != null) {
result.append(clam);
result.append("\n");
}
if (pepperoni != null) {
result.append(pepperoni);
result.append("\n");
}
return result.toString();
}
}
现在我们不需要设计两个不同的类来处理不同风味的Pizza了,让原料工厂处理这种区域差异就可以了。下面是CheesePizza
public class CheesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
//要之作Pizza,需要工厂提供原料。所以每个Pizza类都需要从构造器参数中得到一个工厂,
//并把这个工厂存储在一个实例变量中
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
//神奇的事情发生在这里,prepare()方法一步一步地创建芝士Pizza,每当需要原材料,就跟工厂要
void prepare() {
System.out.println("Preparing " + name);
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
}
Pizza的代码利用相关的工厂生产原料。所生产的原料依赖所使用的工厂,Pizza类根本不关心这些原料,它只知道如何制作Pizza。现在,Pizza和区域原料之间被解耦,无论原料工厂是在洛杉矶山脉还是西北沿岸地区,Pizza类都可以轻易复用,完全没有问题。
再回到Pizza店:
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) {
Pizza pizza = null;
//纽约店会用到纽约Pizza原料工厂,由该原料工厂负责生产所有原料
PizzaIngredientFactory ingredientFactory =
new NYPizzaIngredientFactory();
/*
* 把工厂传递给每一个Pizza,以便能从工厂中取得原料
* 对于每一种Pizza,我们实例化一个新的Pizza,并传进该中Pizza所需的工厂,以便Pizza取得它的原材料
*/
if (item.equals("cheese")) {
pizza = new CheesePizza(ingredientFactory);
pizza.setName("New York Style Cheese Pizza");
} else if (item.equals("veggie")) {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("New York Style Veggie Pizza");
} else if (item.equals("clam")) {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("New York Style Clam Pizza");
} else if (item.equals("pepperoni")) {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("New York Style Pepperoni Pizza");
}
return pizza;
}
}
我们到底做了些什么?
我们引入新类型的工厂,也就是所谓抽象工厂,来创建原料家族。
通过抽象工厂所提供的接口,可以创建产品的家族,利用这个接口书写代码,我们的代码将从实际工厂解耦,以便在不同上下文中实现各式各样的工厂,制造出各种不同的产品。例如:不同的区域、不同的操作系统、不同的外观及操作。
因为代码从实际的产品中解耦,所以我们可以替换不同的工厂来取得不同的行为。
定义抽象工厂模式:提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体的类。
抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道(或关心)实际产出的具体产品是什么。这样依赖,客户就从具体的产品中被解耦。
3 本章小结
好长的一章啊,这一章我们学习了简单工厂、工厂方法模式和抽象工厂模式。
这三者在文字定义上就已经把我们搞混淆了,三者间具体的区别唯有经过码农们在实际应用中的使用才能够细细体会。
以下罗列本章的一些要点,来看看你是不是真正理解了这些概念:
1、所有的工厂都是用来封装对象的创建。
2、简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类解耦。
3、工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象。
4、抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中。
5、所有工厂模式都通过减少应用程序与具体类之间的依赖促进松耦合。
6、工厂方法允许类将实例化延迟到子类进行。
7、抽象工厂创建相关的对象家族,而不需要依赖它们的具体类。
8、依赖倒置原则,指导我们避免依赖具体类型,而要尽量依赖抽象。
9、工厂是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程。