游戏开发设计模式4 工厂模式
第四章 工厂模式
接下来讲松耦合的面向对象设计,除了使用new操作符之外,还有更多制造对象的方法。你将了解到实例化这个活动不应该总是公开地进行,也会认识到初始化经常造成“耦合”问题。你将了解工厂模式如何从复杂的依赖中帮你脱困。。。(话不多说 直接开喷)
思考New
已经过了三个章节,我们来一起思考new的问题。我们不应该针对实现编程,但是当我每次使用new时,不正是在针对实现编程吗?
当看到“new”,就会想到“具体’
是的,当使用“new”时,你的确是在实例化一个具体类,所以用的确实是实现,而不是接口。这是一个好问题,你已经知道了代码绑着具体类会导致代码更脆弱,更缺乏弹性。
当有一群相关的具体类时,通常会写出这样的代码:
这里有一些要实例化的具体类,究竟实例化哪个类,要在运行时由一些条件来决定。
当看到这样的代码,一旦有变化或扩展,就必须重新打开这段代码进行检查和修改。通常这样修改过的代码将造成部分系统更难维护和更新,而且也更容易犯错。
但是,总是要创建对象吧!而c#只提供一个new关键词创建对象,不是吗?还能有些什么?
“new”有什么不对劲?
在技术上,new没有错,毕竟这是c#的基础部分。真正的犯人是我们的老朋友“改变”,以及它是如何影响new的使用的。
针对接口编程,可以隔离掉以后系统可能发生的一大堆改变。为什么呢?如果代码是针对接口而写,那么通过多态,它可以与任何新类实现该接口。但是,当代码使用大量的具体类时,等于是自找麻烦,因为一旦加入新的具体类,就必须改变代码。
也就是说,你的代码并非“对修改关闭”。
想用新的具体类型来扩展代码,必须重新打开它。所以,该怎么办?当遇到这样的问题时,就应该回到00设计原则去寻找线索。别忘了,我们的第一个原则用来处理改变,并帮助我们“找出会变化的方面,把它们从不变的部分 分离出来”。
对扩展开放,对修改关闭 上章提到过的 很重要 开闭原则
识别变化的方面
假设你有一个比萨店,身为对象村内最先进的比萨店主人,你的代码可能这么写:
假设需要更多的比萨类型
所以必须增加一些代码,来“决定”适合的比萨类型,然后再“制造”这个比萨
发现问题,当我们想增加一个新的类型比萨,或者从菜单删除一个过时的比萨,是不是都得重复反复去修改 实现的代码
很明显地,如果实例化“某些”具体类,将使orderPizza()出问题,而且也无法让orderPizza()对修改关闭;但是,现在我们已经知道哪些会改变,哪些不会改变,该是使用封装的时候了。
封装创建对象的代码
现在最好将创建对象移到orderPizza()之外,但怎么做呢?这个嘛,要把创建比萨的代码移到另一个对象中,由这个新对象专职创建比萨。
我们称这个新对象为“工厂”
工厂(factory)处理创建对象的细节。一日有了SimplePizzaFactoryorderPizza()就变成此对象的客户。当需要比萨时,就叫比萨工厂做一个。那些orderPizza()方法需要知道水果比萨或者蛤蜊比萨的日子一去不复返了。
现在orderPizza0方法只关心从工厂得到了一个比萨,而这个比萨实现了Pizza接口,所以它可以调用prepare()、bake()、cut()、box()来分别进行准备、烘烤、切片、装盒。
还有一些细节有待补充,比方说,原本在orderPizza()方法中的创建代码,现在该怎么写?现在就来为比萨店实现一个简单的比萨工厂来研究这个问题……
建立一个简单比萨工厂
先从工厂本身开始。我们要定义一个类,为所有比萨封装创建对象的代码。代码像这样……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// <summary>
/// SimplePizxaFactory是我们的新类,它只做一件事
/// 情: 帮它的客户创建比萨。
/// </summary>
public class SimplePizzaFactory
{
/// <summary>
/// 首先,在这个工厂内定一个CreatePizza()方法,所有客户用这个方法来实例化新对象
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public Pizza CreatePizza(string type)
{
Pizza pizza = null;
if (type.Equals("cheese"))
{
pizza = new CheesePizza();
}
else if (type.Equals("pepperoni"))
{
pizza = new PepperoniPizza();
}
else if (type.Equals("clam"))
{
pizza = new CamPizza();
}
else if (type.Equals("veggie"))
{
pizza = new VeggiePizza();
}
return pizza;
}
}
问:这么做有什么好处?似乎只是把问题搬到另一个对象罢了,问题依然存在。
答:别忘了,SimplePizza-Factory可以有许多的客户。虽然目前只看到orderPizza()方法是它的客户,然而,可能还有PizzaShopMenu(比萨店菜单)类,会利用这个工厂来取得比萨的价钱和描述。可能还有一个HomeDelivery(宅急送)类,会
以与PizzaShop类不同的方式来处理比萨。总而言之,SimplePizza-Factory可以有许多的客户。
所以,把创建比萨的代码包装进一个类,当以后实现改变时,只需修改这个类即可
别忘了,我们也正要把具体实例化的过程,从客户的代码中删除!
问:我曾看过一个类似的设计方式,把工厂定义成一个静态的方法。这有何差别?
答:利用静态方法定义一个简单的工厂,这是很常见的技巧,常被称为静态工厂。为何使用静态方法?因为不需要使用创建对象的方法来实例化对象。但请记住,这也有缺点,不能通过继承来改变创建方法的行为。
重做PizzaStore类
是时候修改我们的客户代码了,我们所要做的是仰仗工厂来为我们创建比萨,要做这样的改变:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PizzaStore
{
/// <summary>
/// 现在我们为 PizzaStore加上一个对SimplePizzaFactory的引用。
/// </summary>
SimplePizzaFactory factory;
/// <summary>
/// PizzaStore的构造器,需要一个工厂作为参数。
/// </summary>
/// <param name="factory"></param>
public PizzaStore(SimplePizzaFactory factory)
{
this.factory = factory;
}
public Pizza OrderPizza(string type)
{
Pizza pizza;
//通过简单传入订单类型来使用工厂创建比萨
//请注意,我们把new操作符替换成工厂对象的创建方法。这里不再使用县体实例化!
pizza = factory.CreatePizza(type);
pizza.Prepare();
pizza.Bake();
pizza.Cut();
pizza.Box();
return pizza;
}
}
定义简单工厂
简单工厂其实不是一个设计模式,反而比较像是一种编程习惯。但是经常会被使用。有些开发人员的确是把这个编程习惯误认为是“工厂模式”(Factory Pattern)。
不要因为简单工厂不是一个“真正的”模式,就忽略了它的用法。让我们来看看新的比萨店类图:
上面是简单工厂来为我们打个样。接下来登场的是两个重量级的模式,它们都是工厂。但是别担心,未来还有更多的比萨!
再提醒一次:在设计模式中,所谓的“实现一个接口”并“不一定”表示“写一个类,并利用 interface “关键词来实现某个接口。“实现一个接口”泛指“实现某个超类型(可以是类或接口)的某个方法。
加盟比萨店
举例说明:
对象村比萨店经营有成,击败了竞争者,现在大家都希望对象村比萨店能够在自家附近有加盟店。身为加盟公司经营者,你希望确保加盟店营运的质量,所以希望这些店都使用你那些经过时间考验的代码。
但是区域的差异呢?每家加盟店都可能想要提供不同风味的比萨(比方说纽约、芝加哥、加州),这受到了开店地点及该地区比萨美食家口味的影响。
如果利用SimplePizzaFactory,写出三种不同的工厂,分别是NYPizzaFactory、ChicagoPizzaFactory、CaliforniaPizzaFactory,那么各地加盟店都有适合的工厂可以使用,这是一种做法。
让我们来看看会变成什么样子……
让子类决定
给比萨店使用的框架
有个做法可让比萨制作活动局限于PizzaStore类,而同时又能让这些加盟店依然可以自由地制作该区域的风味。
所要做的事情,就是把createPizza()方法放回到PizzaStore中,不过要把它设置成“抽象方法”,然后为每个区域风味创建一个PizzaStore的子类。
首先,看看PizzaStore所做的改变:
1 | /// <summary> |
现在已经有一个PizzaStore作为超类;让每个域类型(NYPizzaStore、ChicagoPizzaStore、CaliforiaPizzaStore)都继承这个PizzaStore,每个子类各自决定如何制造比萨。让我们看看这要如何进行。
允许子类做决定
别忘了,PizzaStore已经有一个不错的订单系统,由orderPizza()方法负责处理订单你希望所有加盟店对于订单的处理都能够一致。
各个区域比萨店之间的差异在于他们制作比萨的风味(纽约比萨的饼薄,芝加哥比的饼厚等),我们现在要让createPizza()能够应对这些变化来负责创建正确种类的比萨。
做法是让PizzaStore的各个子类负责定义自己的createPizza0方法。所以我们会得到一些PizzaStore具体的子类,每个子类都有自己的比萨变体,而仍然适合PizzaStore框架,并使用调试好的orderPizza()方法。
披萨类
现在让我们来实现比萨,假如比萨卖光了 我们要进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 //从一个抽象比萨类开始,所有的具体比萨都必须派生自这个类。
public abstract class Pizza
{
//每个比萨都具有名称、面团类型、酱料类型、一套作料。
public string name;
public string dough;
public string sauce;
public List<string> toppings = new List<string>();
//此抽象类提供了某些默认的基本做法,用来进行烘烤、切片、装盒
public void Prepare()
{
Debug.Log("准备制作:" + name);
Debug.Log("做面团");
Debug.Log("加酱料..");
Debug.Log("添加配料");
for (int i = 0; i < toppings.Count; i++)
{
Debug.Log(toppings[i]);
}
}
public virtual void Bake()
{
Debug.Log("在350度的温度下烤25分钟");
}
public virtual void Cut()
{
Debug.Log("把披萨切成对角的小片");
}
public virtual void Box()
{
Debug.Log("将披萨放入盒子中");
}
public virtual string GetName()
{
return name;
}
}
现在我们需要一些具体子类……来定义纽约和芝加哥风味的芝士比萨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class NYStyleCheesePizza : Pizza
{
public NYStyleCheesePizza()
{
name = "纽约风味酱汁芝士披萨";
dough = "薄皮面团";
sauce = "番茄酱";
toppings.Add("碎奶酪");
}
}
public class ChicagoStyleCheesePizza : Pizza
{
public ChicagoStyleCheesePizza()
{
name = "芝加哥风味酱汁芝士披萨";
dough = "厚面团";
sauce = "小番茄";
toppings.Add("意大利白干酪");
}
public override void Cut()
{
Debug.Log("切成方形的");
}
}
做一些比萨
1
2
3
4
5
6
7
8
9
10
//首先建立两个不同的店铺
PizzaStore nyStore= new NYPizzaFactory();
PizzaStore chicagoStore = new ChicagoPizzaFactory();
//然后开始下订单
Pizza pizza = nyStore.OrderPizza("cheese");
Debug.Log(pizza.GetName());
pizza = chicagoStore.OrderPizza("cheese");
Debug.Log(pizza.GetName());
看看结果
认识工厂方法模式的时刻终于到了
所有工厂模式都用来封装对象的创建。工厂方法模式(Factory Method Pattern)通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的。让我们来看看这些类图,以了解有哪些组成元素:
创建者和产品
另一个观点:平行的类层级
我们已经看到,将一个orderPizza()方法和一个工厂方法联合起来,就可以成为一个框架。除此之外,工厂方法将生产知识封装进各个创建者,这样的做法,也可以被视为是一个框架。
让我们来看看这两个平行的类层级,并认清它们的关系:
定义工厂方法模式
下面是工厂方法模式的正式定义:
工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
工厂方法模式能够封装具体类型的实例化。看看下面的类图,抽象的Creator提供了一个创建对象的方法的接口,也称为“工厂方法”。在抽象的Creator中,任何其他实现的方法,都可能使用到这个工厂方法所制造出来的产品,但只有子类真正实现这个工厂方法并创建产品。
如同在正式定义中所说的,常常听到其他开发人员说:工厂方法让子类决定要实例化的类是哪一个。希望不要理解错误,所谓的“决定”,并不是指模式允许子类本身在运行时做决定,而是指在编写创建者类时,不需要知道实际创建的产品是哪一个。选择了使用哪个子类,自然就决定了实际创建的产品是什么。
问:当只有一个ConcreteCreator的时候,工厂方法模式有什么优点?
答:尽管只有一个具体创建者,工厂方法模式依然很有用,因为它帮助我们将产品的“实现”从“使用”中解耦。如果增加产品或者改变产品的实现,Creator并不会受到影响(因为Creator与任何ConcreteProduct之间都不是紧耦合)
问:如果说纽约和芝加哥的商店是利用简单工厂创建的,这样的说法是否正确?看起来倒是很像。
答:他们很类似,但用法不同。虽每个具体商店的实现看起来都很像是SimplePizza-Factory,但是别忘了,这里的具体商店是扩展自一个类,此类有一个抽象的方法createPizza()。由每个商店自行负责createPizza()方法的行为。在简单工厂中,工厂是另一个由Pizzasfore使用的对象。
问:工厂方法和创建者是否总是抽象的?
答:不,可以定义一个默认的工厂方法来产生某些具体的产品,这么一来,即使创建者没有任何子类,依然可以创建产品。
问:每个商店基于传入的类型制造出不同种类的比萨。是否所有的具体创建者都必须如此?能不能只创建一种比萨?
答:这里所采用的方式称为“参数化工厂方法”。它可以根据传入的参数创建不同的对象。然而,工厂经常只产生一种对象,不需要参数化。模式的这两种形式都是有效的。
问:利用字符串传入参数化的类型,似乎有点危险,万一把Clam(蛤蜊)英文拼错,成了Calm(平静),要求供应“CalmPizza”,怎么办?
答:说得很对,这样的情形会造成所谓的“运行时错误”。有几个其他更复杂的技巧可以避开这个麻烦,在编译时期就将参数上的错误挑出来。比方说,你可以创建代表参数类型的对象和使用静态常量或者C#所支持的enum。
问:对于简单工厂和工厂方法之间的差异,我依然感到困惑。他们看起来很类似,差别在于,在工厂方法中,返回比萨的类是子类。能解释一下吗?
答:子类的确看起来很像简单工厂。简单工厂把全部的事情,在一个地方都处理完了,然而工厂方法却是创建一个框架,让子类决定要如何实现。比方说,在工厂方法中,orderPizza()方法提供了一般的框架,以便创建比萨,orderPizza()方法依赖工厂方法创建具体类,并制造出实际的比萨。可通过继承PizzaStore类,决定实际制造出的比萨是什么。简单工厂的做法,可以将对象的创建封装起来,但是简单工厂不具备工厂方法的弹性,因为简单工厂不能变更正在创建的产品。
一个很依赖的比萨店
假设你从未听说过OO工厂。下面是一个不使用工厂模式的比萨店版本。数一数,这个类所依赖的具体比萨对象有几种。如果又加了一种加州风味比萨到这个比萨店中,那么届时又会依赖几个对象?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class DependentPizzaStore
{
public Pizza CreatePizza(string style, string type)
{
Pizza pizza = null;
if (style.Equals("NY"))
{
if (type.Equals("cheese"))
{
pizza = new NYStyleCheesePizza();
}
else if (type.Equals("pepperoni"))
{
pizza = new NYStylePepperoniPizza();
}
else if (type.Equals("clam"))
{
pizza = new NYStyleCamPizza();
}
else if (type.Equals("veggie"))
{
pizza = new NYStyleVeggiePizza();
}
}
else if (style.Equals("Chicago"))
{
if (type.Equals("cheese"))
{
pizza = new ChicagoStyleCheesePizza();
}
else if (type.Equals("pepperoni"))
{
pizza = new ChicagoStylePepperoniPizza();
}
else if (type.Equals("clam"))
{
pizza = new ChicagoStyleCamPizza();
}
else if (type.Equals("veggie"))
{
pizza = new ChicagoStyleVeggiePizza();
}
}
else
{
Debug.Log("没有此披萨");
return null;
}
pizza.Prepare();
pizza.Bake();
pizza.Cut();
pizza.Box();
return pizza;
}
}
对象依赖
看看对象依赖
当你直接实例化一个对象时,就是在依赖它的具体类。请返回前页看看这个依赖性很高的比萨店例子,它由比萨店类来创建所有的比萨对象,而不是委托给工厂。
如果把这个版本的比萨店和它依赖的对象画成一张图,看起来是这样的:
每新增一个比萨种类,就等于让PizzaStore多了一个依赖。
依赖倒置原则
很清楚地,代码里减少对于具体类的依赖是件“好事”事实上,有一个OO设计原则就正式阐明了这一点,这个原则甚至还有一个又响亮又正式的名称:“依赖倒置原则”(Dependency Inversion Principle) (吊的一批)
通则如下:
要依赖抽象,不要依赖具体类
首先,这个原则听起来很像是“针对接口编程,不针对实现编程”,不是吗?的确很相似,然而这里更强调“抽象”。这个原则说明了:不能让高层组件依赖低层组件,而且,不管高层或低层组件,“两者”都应该依赖于抽象。
所谓“高层”组件,是由其他低层组件定义其行为的类例如,PizzaStore是个高层组件,因为它的行为是由比萨定义的:PizzaStore创建所有不同的比萨对象,准备、烘烤、切片、装盒;而比萨本身属于低层组件。
这到底是什么意思?
这个嘛,让我们再次看看前一页比萨店的图。PizzaStore是“高层组件”,而比萨实现是“低层组件”很清楚地,PizzaStore依赖这些具体比萨类。
现在,这个原则告诉我们,应该重写代码以便于我们依赖抽象类,而不依赖具体类。对于高层及低层模块都应该如此。
但是怎么做呢?我们来想想看怎样在“非常依赖比萨店”实现中,应用这个原则……
依赖倒置原则
原则的应用
非常依赖比萨店的主要问题在于;它依赖每个比萨类型。因为它是在自己的orderPizza()方法中,实例化这些具体类型的。
虽然我们已经创建了一个抽象,也就是Pizza,但我们仍然在代码中,实际地创建了具体的Pizza,所以,这个抽象没什么影响力。
如何在orderPizza()方法中,将这些实例化对象的代码独立出来?我们都知道,工厂方法刚好能派上用场。
所以,应用工厂方法之后,类图看起来就像这样:
在应用工厂方法之后,你将注意到,高层组件(也就是PizzaStore)和低层组件(也就是这些比萨)都依赖了Pizza抽象。想要遵循依赖倒置原则,工厂方法并非是唯一的技巧,但却是最有威力的技巧之一。
这个时候有观众就要问了:好吧!我已经知道什么是依赖,但为什么叫做依赖“倒置”?
依赖倒置原则,究竟倒置在哪里?
在依赖倒置原则中的倒置指的是和一般OO设计的思考方式完全相反。看看上面的图,你会注意到低层组件现在竟然依赖高层的抽象。同样地,高层组件现在也依赖相同的抽象。前几页所绘制的依赖图是由上而下的,现在却倒置了,而且高层与低层模块现在都依赖这个抽象。
让我们好好地回顾一个设计过程来看看,究竟使用了这个原则之后,对设计的思考方式会被怎样地倒置……
倒置你的思考方式
问:你需要实现一个比萨店,你第一个想到的事情是什么?
答:嗯!比萨店进行准备、烘烤、装盒,所以我的店必须能制作许多不同风味的比萨,倒如:芝士比萨、素食比萨、蛤蜊比萨……
问:没错!先从顶端开始,然后往下到具体类是,但是,正如你所看到的你不想让比萨店理会这些具体类,要不然比萨店将全都依赖这些具体类现在,“倒置”你的想法……别从顶端开始而是从比萨(Pizza)开始,然后想想看能抽象化些什么。
答:是的,芝士比萨、景食比萨和蛤蜊比萨都是比萨,所以它们应该共享一个Pizza接口。
问:对了,你想要抽象化一个Pizza。好,现在回头重新思考如何设计比萨店。
答:既然我已经有一个比萨抽象,就可以开始设计比萨店,而不用理会具体的比萨类了。
很接近了,但是要这么做,必须靠一个工厂来将这些具体类取出比萨店。一旦你这么做了各种不同的具体比萨类型就只能依赖一个抽象而比萨店也会依赖这个抽象。我们已经倒置了一个商店依赖具体类的设计,而且也倒置了你的思考方式。
几个指导方针帮助你遵循此原则……
下面的指导方针,能帮你避免在面向对象设计中违反依赖倒置原则:
1.变量不可以持有具体类的引用。(如果使用new,就会持有具体类的引用。你可以改用工厂来避开这样的做法)
2.不要让类派生自具体类。(如果派生自具体类,你就会依赖具体类。请派生自一个抽象(接口或抽象类)。)
3.不要覆盖基类中已实现的方法。(如果覆盖基类已实现的方法,那么你的基类就不是一个真正适合被继承的抽象。基类中已实现的方法,应该由所有的子类共享。)
这个时候你就要问了:但是,等等,要完全遵守这些指导方针似乎不太可能吧?如果遵守这些方针,我连一个简单程序都写不出来!
你说的没错!正如同我们的许多原则一样,应该尽量达到这个原则,而不是随时都要遵循这个原则。我们都很清楚,任何C#程序都有违反这些指导方针的地方!
你说的没错!正如同我们的许多原则一样,应该尽量达到这个原则,而不是随时都要遵循这个原则。我们都很清楚,任何程序都有违反这些指导方针的地方!
但是,如果你深入体验这些方针,将这些方针内化成你思考的一部分,那么在设计时,你将知道何时有足够的理由违反这样的原则比方说,如果有一个不像是会改变的类,那么在代码中直接实例化具体类也就没什么大碍。想想看,我们平常还不是在程序中不假思索地就实例化字符串对象吗?就没有违反这个原则?当然有!可以这么做吗?可以!为什么?因为字符串不可能改变。
另一方面,如果有个类可能改变,你可以采用一些好技巧(例如工厂方法)来封装改变。
原料家族
再回到比萨店…
比萨店的设计变得很棒:具有弹性的框架,而且遵循设计原则。
现在,对象村比萨店成功的关键在于新鲜高质量的原料,而且通过导入新的框架加盟店将遵循你的流程,但是有一些加盟店,使用低价原料来增加利润。你必须采取一些手段,以免长此以往毁了对象村的品牌。
确保原料的一致…
要如何确保每家加盟店使用高质量的原料?你打算建造一家生产原料的工厂,并将原料运送到各家加盟店。对于这个做法,现在还剩下了一个问题:加盟店座落在不同的区域,纽约的红酱料和芝加哥的红酱料是不一样的。所以对于纽约和芝加哥,你准备了两组不同的原料。让我们看得更仔细些:
原料家族
纽约使用一组原料,而芝加哥使用另一组原料。对象村比萨是如此受欢迎可能不久之后加州就有加盟店了,到时候又需要运送另一组区域的原料。
想要行得通,必须先清楚如何处理原料家族。
原料工厂
建造原料工厂
现在,我们要建造一个工厂来生产原料,这个工厂将负责创建原料家族中的每一种原料。也就是说,工厂将需要生产面团、酱料、芝士等。待会儿,你就会知道如何处理各个区域的差异了
开始先为工厂定义一个接口,这个接口负责创建所有的原料:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// 在接口中,每个原料都有一个对应的方法创建该原料。
/// </summary>
public interface PizzaIngredientFactory
{
//这里有许多新类,每个原料都是一个类。
public Dough CreateDough();
public Sauce CreateSauce();
public Cheese CreateCheese();
public Veggies[] Createveggies();
public Pepperoni CreatePepperoni();
public Clams CreateClam();
//如果每个工厂实例内都有某一种通用的“机制”需要实现,就可以把
// 这个例子改写成抽象类……
}
要做的事情是:
1.为每个区域建造一个工厂。你需要创建一个继承自PizzalngredientFactory的子类来实现每一个创建方法。
2.实现一组原料类供工厂使用,例如ReggianoCheese、RedPeppers、ThickCrustDough。这些类可以在合适的区域间共享。
3.然后你仍然需要将这一切组织起来,将新的原料工厂整合进旧的PizzaStore代码中。
好了,这是纽约原料工厂的实现。这工厂专精于大蒜番茄酱料、Reggiano干酪,新鲜蛤蜊……
重做比萨 …
工厂已经一切就绪,准备生产高质量原料了;现在我们只需要重做比萨,好让它们只使用工厂生产出来的原料。我们先从抽象的Pizza类开始:
将原料解耦
继续重做比萨……
现在已经有了一个抽象比萨,可以开始创建纽约和芝加哥风味的比萨了。从今以后,加盟店必需直接从工厂取得原料.
我们曾经写过工厂方法的代码,有NYCheesePizza和ChicagoCheesePizza类。比较一下这两个类,唯一的差别在于使用区域性的原料,至于比萨的做法都一样(面团+酱料+芝士),其他的比萨(蔬菜、蛤蜊等)也是如此。它们都依循着相同的准备步骤,只是使用不同的原料。
所以,其实我们不需要设计两个不同的类来处理不同风味的比萨,让原料工厂处理这种区域差异就可以了。下面是CheesePizza:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CheesePizza : Pizza
{
PizzaIngredientFactory ingredientFactory;
/// <summary>
/// 要制作比萨,需要工厂提供原料。所以每个比萨类都需要从构造器参数中得到一个工厂,并把这个工厂存储在一个实例变量中。
/// </summary>
/// <param name="pizzaIngredientFactory"></param>
public CheesePizza(PizzaIngredientFactory pizzaIngredientFactory)
{
ingredientFactory = pizzaIngredientFactory;
}
/// <summary>
/// prepare()方法一步一步地创建芝士比萨,每省需要原料时,就跟工厂要。
/// </summary>
public override void Prepare()
{
Debug.Log("Preparing " + name);
dough = ingredientFactory.CreateDough();
sauce = ingredientFactory.CreateSauce();
cheese = ingredientFactory.CreateCheese();
}
}
Pizza的代码利用相关的工厂生产原料。所生产的原料依赖所使用的工厂,Pizza类根本不关心这些原料,它只知道如何制作比萨。现在,Pizza和区域原料之间被解耦,无论原料工厂是在洛基山脉还是在西北沿岸地区,Pizza类都可以轻易地复用,完全没有问题
也来看看蛤蜊比萨:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ClamPizza : Pizza
{
PizzaIngredientFactory ingredientFactory;
/// <summary>
/// 蛤蜊比萨也需要原料工厂
/// </summary>
/// <param name="pizzaIngredientFactory"></param>
public ClamPizza(PizzaIngredientFactory pizzaIngredientFactory)
{
ingredientFactory = pizzaIngredientFactory;
}
/// <summary>
/// 要做出蛤蜊比萨prepare()方法就必须从本地工厂中取得正确的原料。
/// </summary>
public override void Prepare()
{
Debug.Log("Preparing " + name);
dough = ingredientFactory.CreateDough();
sauce = ingredientFactory.CreateSauce();
cheese = ingredientFactory.CreateCheese();
//如果是纽约工厂,就会使用新鲜的蛤蜊:如果是芝加哥工厂就是冷冻的蛤蜊。
clam = ingredientFactory.CreateClam();
}
}
使用正确的原料工厂
再回到比萨店
我们几乎完工了,只需再到加盟店短暂巡视一下,确认他们使用了正确的比萨。也需要让他们能和本地的原料工厂搭上线:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 public class NYPizzaFactory : PizzaStore
{
public override Pizza CreatePizza(string type)
{
Pizza pizza = null;
//纽约店会用到纽约比萨原料工由该原料工厂负责生产所有组约味比萨所需的原料。
PizzaIngredientFactory pizzaIngredientFactory = new NYPizzaIngredientFactory();
//对于每一种比萨,我们实例化一个新的比萨,并传进该种比萨所需的工厂,以便比萨取得它的原料
if (type.Equals("cheese"))
{
//把工厂传递给每一个比萨,便比萨能从工厂中取得原科
pizza = new CheesePizza(pizzaIngredientFactory);
}
else if (type.Equals("pepperoni"))
{
pizza = new PepperoniPizza(pizzaIngredientFactory);
}
else if (type.Equals("clam"))
{
pizza = new ClamPizza(pizzaIngredientFactory);
}
else if (type.Equals("veggie"))
{
pizza = new VeggiePizza(pizzaIngredientFactory);
}
return pizza;
}
}
我们做了些什么?
一连串的代码改变;我们到底做了些什么?
我们引入新类型的工厂,也就是所谓的抽象工厂,来创建比萨原料家族。
通过抽象工厂所提供的接口可以创建产品的家族,利用这个接口书写代码,我们的代码将从实际工厂解耦,以便在不同上下文中实现各式各样的工厂,制造出各种不同的产品例如:不同的区域、不同的操作系统、不同的外观及操作。
因为代码从实际的产品中解耦了,所以我们可以替换不同的工厂来取得不同的行为(例如:取得大蒜番茄酱料,而不是取得番茄酱料)。
接下来让我们从新捋一捋 从开始下订单到制作比萨的结束的一整个流程
1.首先我们需要一个纽约比萨店:
PizzaStore nyPizzaStore=new NYPizzaStore();
2.现在已经有一个比萨店了,可以接受订单
nyPizzaStore.orderPizza(“cheese");
3.orderPizza()方法首先调用createPizza()方法
Pizza pizza =createPizza("cheese");
4.当createPizza()方法被调用时,也就开始涉及原料工厂了
Pizza pizza =new CheesePizza(nyIngredientFactory);
5.接下来需要准备比萨一旦调用了prepare()方法,工厂将被要求准备原料:
void prepare()
{
dough = factory.createDough();
sauce =factory.createSauce();
cheese =factory.createCheese();
}
6.最后,我们得到了准备好的比萨,orderPizza()就会接着烘烤、切片、装盒。
定义抽象工厂模式
我们又在模式家族中新增了另一个工厂模式,这个模式可以创建产品的家族。看看这个模式的正式定义:
抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道(或关心)实际产出的具体产品是什么。这样一来,客户就从具体的产品中被解耦。让我们看看类图来了解其中的关系:
尾章 复习
来到第4章的结尾,我们的设计原则又多了一些东西…· 从头捋一下 先说我们的OO设计原则
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为对象之间的松耦合而努力
对扩展开放,对修改关闭
依赖抽象,不要依赖具体类 (新增 我们有了一个新原则,指导我们尽可能地让事情保持抽象。)
再说我们的00设计模式
这些新的模式可以将对象的创建封装起来,以便于得到更松耦合、更有弹性的设计。
1.抽象工厂模式–提供一个接口具有于创建相关或依赖对象的家族,而不需要明确指定具体类。
2.工厂方法模式–定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
要点
1.所有的工厂都是用来封装对象的创建
2.简单工厂,虽然不是真正的设计模式,但仍不失仍为一个简单的方法,可以将客户程序从具体类解耦。
3.工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象。
4.抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中。
5.所有工厂模式都通过减少应用程序和具体类之间的依赖进行松耦合。
6.工厂方法允许类将实例化延迟到子类进行。
7.抽象工厂创建相关的对象家族,而不需要依赖它们的具体类
8.依赖倒置原则,指导我们是免依赖具体类型,而要尽量依赖抽象。
9.工厂是很有威力的技巧,帮助我们针对抽象编程,而要针对具体类编程