2008-12-08 21:11:00
由于讨论班上我要讲 Microsoft .Net PetShop 4.0,里面用了工厂模式,所以不得不去了解。我之前听过“设计模式”这个词语,以及“没有写过十万行代码不要去谈设计模式”这句话,但精确的概念我基本上是不知道的。这阵子断断续续地在网上找了些文章看,也翻了些书,自认为模模糊糊地有点理解了,故写篇小记,一来检验自己是否真的有些理解了、能不能用文字描述清楚,二来如果有错误之处可以请大家指出。
对于读者您,您最好已经初步了解了 C#/.Net 和面向对象思想。本文用 C# 描述。
首先,假定有这么两个类——奥迪汽车和波音汽车。(当我自以为风趣地构造出波音造汽车这个词的时候,很无奈地看到波音可能确实造汽车了。)简单起见,只给它们一个方法 Run()
。和大多数例程一样,该方法仅仅在控制台输出文字,用于区分是哪个类的方法。
代码如下:
1namespace Sample1
2{
3 // 假设有一个奥迪汽车类和一个波音汽车类
4 class AudiCar
5 {
6 public void Run()
7 {
8 Console.WriteLine("AudiCar Runs.");
9 }
10 }
11 class BoeingCar
12 {
13 public void Run()
14 {
15 Console.WriteLine("BoeingCar Runs.");
16 }
17 }
18 // 测试
19 class Program
20 {
21 static void Main(string[] args)
22 {
23 AudiCar audiCar = new AudiCar();
24 audiCar.Run();
25 BoeingCar boeingCar = new BoeingCar();
26 boeingCar.Run();
27 }
28 }
29}
形象起见,给它们画这么个图:
很尴尬的一点是,我不懂 UML,因此无法用标准的 UML 来图示,只能根据自己的理解给出一些自认为可以看懂的图示。
自己观察这两个类,会发现它们很相似,可以抽象出它们的父类。C# 语言支持接口这个概念,因此这里抽象出接口。——已经熟悉面向对象的读者可以略过相关内容。
现在代码变成这样子了:
1namespace Sample2
2{
3 // 因为两类汽车都支持 Run() 这个方法(有共性),所以可以抽象出父类,或者抽象出接口
4 // 接口定义如下:
5 interface ICar
6 {
7 void Run();
8 }
9 // 下面是两个具体的汽车定义
10 class AudiCar : ICar
11 {
12 public void Run()
13 {
14 Console.WriteLine("AudiCar Runs.");
15 }
16 }
17 class BoeingCar : ICar
18 {
19 public void Run()
20 {
21 Console.WriteLine("BoeingCar Runs.");
22 }
23 }
24 class Program
25 {
26 static void Main(string[] args)
27 {
28 // 使用的时候根据实际需要加以选择
29 ICar car = new AudiCar();
30 //ICar car = new BoeingCar();
31 car.Run();
32 // 但是,这里有一个问题
33 // 我一旦改变汽车品牌,这里的代码要被改过重新编译
34 // 也就是说,离开了开发人员,这个选择是不可改变的
35 }
36 }
37}
相应的图示为:
这是很传统的多态的应用,C++ 教科书上基本上都能找到类似的例子。
在这里我们引出一个问题。在实际应用中,同一接口的两个不同实现经常不是都用到的,可能在一些场合只用某一种。比如刚才的例子中,系统在最初可能会让客户选择使用奥迪还是波音,一旦选定(比如选择了奥迪)之后,以后所有的关于汽车的东西将都由被选择的奥迪汽车来执行——并不需要同时波音汽车。可是,也许有一天,应用环境变了,需要改为使用波音了,这时候问题来了。因为这个选择已经写在了代码里面(ICar car = new AudiCar();
),除非重新编译此程序,否则只能继续用奥迪。我们看到写“死”的程序面对变化的需求真是太无力了。所以我们要解决这个灵活性问题,也就是让客户可以决定用奥迪还是波音,并且在开发人员不介入的情况下改变到已部署的系统中去。
以上场景并不复杂,同时跟我们要讲的抽象工厂模式还差了一步。我们继续将场景复杂化,引入第二种产品——飞机,也有奥迪飞机和波音飞机。(奥迪总算不产飞机的吧?)
同样使用接口,代码如下:
1namespace Sample3
2{
3 // 变动一下场景,新增一个产品——飞机
4 interface ICar
5 {
6 void Run();
7 }
8 class AudiCar : ICar
9 {
10 public void Run()
11 {
12 Console.WriteLine("AudiCar Runs.");
13 }
14 }
15 class BoeingCar : ICar
16 {
17 public void Run()
18 {
19 Console.WriteLine("BoeingCar Runs.");
20 }
21 }
22 // 飞机接口定义
23 interface IPlane
24 {
25 void Fly();
26 }
27 class AudiPlane : IPlane
28 {
29 public void Fly()
30 {
31 Console.WriteLine("AudiPlane Flies.");
32 }
33 }
34 class BoeingPlane : IPlane
35 {
36 public void Fly()
37 {
38 Console.WriteLine("BoeingPlane Flies.");
39 }
40 }
41 class Program
42 {
43 static void Main(string[] args)
44 {
45 // 使用的时候根据实际需要加以选择
46 ICar car = new AudiCar();
47 //ICar car = new BoeingCar();
48 car.Run();
49 //IPlane plane = new AudiPlane();
50 IPlane plane = new BoeingPlane();
51 plane.Fly();
52 // Sample2 中的问题还未解决
53 // 这里还可以看到,随着“物品”(汽车、飞机)的增加
54 // 要改动的地方会越来越多
55 // 也就是维护成本会变得越来越高
56 }
57 }
58}
相应的图示变为:
场景复杂了,但是灵活性问题还没解决。我们看到,场景是可以任意复杂下去的,只要不断地增加“物品”即可。再来点奥迪/波音卡车、奥迪/波音高射炮之类的,之后的维护代价会大大增加。
下面得引出“工厂”的概念了,先不忙着讲,请看代码,注意最后两个类 AudiFactory
和 BoeingFactory
:
1namespace Sample4
2{
3 // 场景不变
4 interface ICar
5 {
6 void Run();
7 }
8 class AudiCar : ICar
9 {
10 public void Run()
11 {
12 Console.WriteLine("AudiCar Runs.");
13 }
14 }
15 class BoeingCar : ICar
16 {
17 public void Run()
18 {
19 Console.WriteLine("BoeingCar Runs.");
20 }
21 }
22 interface IPlane
23 {
24 void Fly();
25 }
26 class AudiPlane : IPlane
27 {
28 public void Fly()
29 {
30 Console.WriteLine("AudiPlane Flies.");
31 }
32 }
33 class BoeingPlane : IPlane
34 {
35 public void Fly()
36 {
37 Console.WriteLine("BoeingPlane Flies.");
38 }
39 }
40 // 新增两个类,用于“生产”汽车和飞机
41 // 所以这两个类叫做“工厂”
42 // 奥迪工厂
43 class AudiFactory
44 {
45 public ICar CreateCar()
46 {
47 return new AudiCar();
48 }
49 public IPlane CreatePlane()
50 {
51 return new AudiPlane();
52 }
53 }
54 // 波音工厂
55 class BoeingFactory
56 {
57 public ICar CreateCar()
58 {
59 return new BoeingCar();
60 }
61 public IPlane CreatePlane()
62 {
63 return new BoeingPlane();
64 }
65 }
66 class Program
67 {
68 static void Main(string[] args)
69 {
70 // 这个时候把对产品的选择转移到了对工厂的选择
71 // 暂不测试,继续看下例
72 }
73 }
74}
最后面这两个类就是所谓的“工厂”,但不是抽象工厂,是具体工厂。具体工厂并不是为了解决上文提出的灵活性问题的,而是解决对象构造问题的。在有工厂的系统中,产品类(AudiCar
、BoeingCar
、AudiPlane
、BoeingPlane
)往往是不被直接构造的。原因呢,看到有一篇文章里说,可能需要很多初始化工作要完成,而这些工作是不适合放在构造函数的。但也不尽是这样。现在不妨理解为为了使用工厂而是用工厂吧。
现在,要得到一个产品对象,得先有一个工厂对象,再调用相应的方法,而不是直接 new
了。当然,在这样的情形下,上面的工厂类可以用静态类,之所以没有,是为了下面要讲的抽象工厂。
很明显地看到两个工厂具有共性,于是可以抽象出他们的父类。这里不使用接口,因为我希望这个父类有自己的内容——提供一个静态方法用于产生具体工厂的对象。
代码如下:
1namespace Sample5
2{
3 // 场景不变
4 interface ICar
5 {
6 void Run();
7 }
8 class AudiCar : ICar
9 {
10 public void Run()
11 {
12 Console.WriteLine("AudiCar Runs.");
13 }
14 }
15 class BoeingCar : ICar
16 {
17 public void Run()
18 {
19 Console.WriteLine("BoeingCar Runs.");
20 }
21 }
22 interface IPlane
23 {
24 void Fly();
25 }
26 class AudiPlane : IPlane
27 {
28 public void Fly()
29 {
30 Console.WriteLine("AudiPlane Flies.");
31 }
32 }
33 class BoeingPlane : IPlane
34 {
35 public void Fly()
36 {
37 Console.WriteLine("BoeingPlane Flies.");
38 }
39 }
40 // 又看到两个工厂也有共性,于是又可以抽象出父类
41 // 因为这个类里要写一个非抽象的方法 GetFactory(),所以不用接口而用抽象类
42 abstract class AbstractFactory
43 {
44 public static AbstractFactory GetFactory(string factoryType)
45 {
46 switch (factoryType)
47 {
48 case "Audi":
49 return new AudiFactory();
50 case "Boeing":
51 return new BoeingFactory();
52 default:
53 break;
54 }
55 throw new Exception("No such factory.");
56 }
57 public abstract ICar CreateCar();
58 public abstract IPlane CreatePlane();
59 }
60 // 奥迪工厂
61 class AudiFactory : AbstractFactory
62 {
63 public override ICar CreateCar()
64 {
65 return new AudiCar();
66 }
67 public override IPlane CreatePlane()
68 {
69 return new AudiPlane();
70 }
71 }
72 // 波音工厂
73 class BoeingFactory : AbstractFactory
74 {
75 public override ICar CreateCar()
76 {
77 return new BoeingCar();
78 }
79 public override IPlane CreatePlane()
80 {
81 return new BoeingPlane();
82 }
83 }
84 class Program
85 {
86 static void Main(string[] args)
87 {
88 string factory = "Audi";
89 // 实际上这时已经解决了 Sample2 末尾的问题了
90 // 这个字符串完全可以保存在程序外部(如配置文件中)
91 // 运行的时候读入即可
92 // 无需重新编译代码即可实现汽车品牌的选择
93 // string factory = ConfigurationManager.AppSettings["Factory"];
94 AbstractFactory f = AbstractFactory.GetFactory(factory);
95 ICar car = f.CreateCar();
96 car.Run();
97 IPlane plane = f.CreatePlane();
98 plane.Fly();
99 }
100 }
101 // 至此我们已经使用了“抽象工厂”设计模式
102 // 例中 AbstractFactory 叫做抽象工厂,负责实例化一个具体工厂
103 // AudiFactory 和 BoeingFactory 是具体工厂,负责生产产品
104}
系统图示变为:
注意新增加的类 AbstractFactory
,这就是本文的主角“抽象工厂”——它生产具体工厂。生产具体工厂的方法为 GetFactory()
。这时,具体工厂也不是直接构造了,而改由抽象工厂构造。要得到一个具体工厂,就:
1 AbstractFactory f = AbstractFactory.GetFactory(factory);
参数 factory 是一个字符串,有效的选择是“Audi”或者“Boeing”。实际上现在我们已经解决了上面提出的灵活性问题了。为什么呢?我们看到,代码中对类的选择已经由一个字符串控制了,这个参数 factory 完全可以不用像string factory = "Audi"; 这样写在程序里面了。我们可以将它写在程序外部,如配置文件 App.config 中:
1<?xml version="1.0" encoding="utf-8" ?>
2<configuration>
3 <appSettings>
4 <add key="Factory" value="Audi"/>
5 </appSettings>
6</configuration>
现在可以把
1 string factory = "Audi";
换成
1 string factory = ConfigurationManager.AppSettings["Factory"];
了,这样,程序中便不出现 Audi 或 Boeing 的选择了,要改变选择,改配置文件即可。
以上说的是传统意义上的抽象工厂。
接下来,我们来看看 .Net 技术下抽象工厂的一些变化。从上面的例子来看,我们本来是直接把类的选择写定在代码里的,后来变成了由一个存储在程序外部的字符串控制,从而解决了灵活性问题的。然而,对这个存储在外部字符串,我们在程序里要来个 switch case
,这样显得很难看,如果能够直接给个类名字串就能创建对象(如,ICar = new GetClass(“AudiCar”)
),那就方便了。.Net 的反射机制正好为此提供了可能。
所谓反射,抄一段网上的话——
“程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。您可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。”
初步理解,好像可以从已编译的程序集中获取类型信息。先不管这么多了,反正我们现在在自己的系统中,可以看到自己的源代码,只要事先给出类名字符串得到类的对象就行了。
我们来改造刚才的抽象工厂类 AbstractFactory
:
1namespace Sample6
2{
3 // 来看看 .Net 中的新技术给设计模式带来的改变
4 interface ICar
5 {
6 void Run();
7 }
8 class AudiCar : ICar
9 {
10 public void Run()
11 {
12 Console.WriteLine("AudiCar Runs.");
13 }
14 }
15 class BoeingCar : ICar
16 {
17 public void Run()
18 {
19 Console.WriteLine("BoeingCar Runs.");
20 }
21 }
22 interface IPlane
23 {
24 void Fly();
25 }
26 class AudiPlane : IPlane
27 {
28 public void Fly()
29 {
30 Console.WriteLine("AudiPlane Flies.");
31 }
32 }
33 class BoeingPlane : IPlane
34 {
35 public void Fly()
36 {
37 Console.WriteLine("BoeingPlane Flies.");
38 }
39 }
40 // 使用反射技术,可以去掉难看的 switch case
41 // 何谓反射,可以先不管,这里它给我们带来的用处就是——提供类名字符串,获得该类型的对象
42 abstract class AbstractFactory
43 {
44 public static AbstractFactory GetFactory(string factoryType)
45 {
46 return (AbstractFactory)Activator.CreateInstance(Type.GetType("Sample6." + factoryType + "Factory"));
47 }
48 public abstract ICar CreateCar();
49 public abstract IPlane CreatePlane();
50 }
51 class AudiFactory : AbstractFactory
52 {
53 public override ICar CreateCar()
54 {
55 return new AudiCar();
56 }
57 public override IPlane CreatePlane()
58 {
59 return new AudiPlane();
60 }
61 }
62 class BoeingFactory : AbstractFactory
63 {
64 public override ICar CreateCar()
65 {
66 return new BoeingCar();
67 }
68 public override IPlane CreatePlane()
69 {
70 return new BoeingPlane();
71 }
72 }
73 class Program
74 {
75 static void Main(string[] args)
76 {
77 string factory = ConfigurationManager.AppSettings["Factory"];
78 AbstractFactory f = AbstractFactory.GetFactory(factory);
79 ICar car = f.CreateCar();
80 car.Run();
81 IPlane plane = f.CreatePlane();
82 plane.Fly();
83 }
84 }
85 // 还可以进一步改进,见下例
86}
其他的都没变,变的仅仅是 GetFactory()
方法,原来一大串变成了一句话。看:
1 Activator.CreateInstance(Type.GetType("Sample6." + factoryType + "Factory"));
Type.GetType()
可以从一个类名得到含有该类的信息的 Type
对象,注意这里的参数是完整的类名,要包含命名空间。然后 Activator.CreateInstance()
可以由含该类的信息的 Type
对象创建这个类的实例。在这个例子中,我们这样就去掉了 swich case
。
其实可以将反射应用地更加彻底一些。可能在看上面的例程的时候您已经想到了,何不只用一个工厂类,在每个 CreateXXX()
的函数里来判断呢?可是看到一段 switch
,又否定了自己的想法了。没错,刚才不这样做正因为有着这段不好看的代码在,这应该也是引入抽象工厂的原因之一吧,至少让这段 switch case
的出现次数减少到了仅有的一次。而现在,没有了 switch case
,我们判别不同的类只需要一句话,何不去掉抽象工厂类呢?
就这么干:
1namespace Sample7
2{
3 // 前面部分一样
4 interface ICar
5 {
6 void Run();
7 }
8 class AudiCar : ICar
9 {
10 public void Run()
11 {
12 Console.WriteLine("AudiCar Runs.");
13 }
14 }
15 class BoeingCar : ICar
16 {
17 public void Run()
18 {
19 Console.WriteLine("BoeingCar Runs.");
20 }
21 }
22 interface IPlane
23 {
24 void Fly();
25 }
26 class AudiPlane : IPlane
27 {
28 public void Fly()
29 {
30 Console.WriteLine("AudiPlane Flies.");
31 }
32 }
33 class BoeingPlane : IPlane
34 {
35 public void Fly()
36 {
37 Console.WriteLine("BoeingPlane Flies.");
38 }
39 }
40 // 这次我们去掉抽象工厂
41 // 去不到终点回到原点?不是。
42 static class Factory
43 {
44 static string factory = ConfigurationManager.AppSettings["Factory"];
45 public static ICar CreateCar()
46 {
47 return (ICar)Activator.CreateInstance(Type.GetType("Sample7." + factory + "Car"));
48 }
49 public static IPlane CreatePlane()
50 {
51 return (IPlane)Activator.CreateInstance(Type.GetType("Sample7." + factory + "Plane"));
52 }
53 }
54 class Program
55 {
56 static void Main(string[] args)
57 {
58 ICar car = Factory.CreateCar();
59 car.Run();
60 IPlane plane = Factory.CreatePlane();
61 plane.Fly();
62 }
63 }
64 // 在这里,抽象工厂形式上退化为一个具体工厂
65 // 然而在设计上它仍然算是一个抽象工厂
66 // 这是被称为“.Net 反射工厂”,是抽象工厂在 .Net 中的变体
67 // PetShop 中大量使用了这种反射工厂(如 DALFactory 等)
68}
我们的代码改造到此为止。我们的系统最终变成了如下的样子:
看上去是不是清爽多了?在这里,我们的设计思想仍然是抽象工厂,但是从形式上来看,它已经是一个具体工厂(简单工厂?)了。这就是抽象工厂在应用 .Net 反射技术之后的变体,被称为“.Net 反射工厂”。PetShop 中大量使用了这种反射工厂,如 DALFactory
。
网上关于工厂模式的文章中,往往都一起谈到简单工厂、工厂方法、抽象工厂。细心的读者可能注意到,我在上文中几乎只字不提前两个。为什么呢?因为我还没搞清楚它们的区别,现在我仅仅理解了抽象工厂(也许还没理解)。我也看过那些文章,但是呢,不客气地说,它们没有把三者区分清楚。如果在一篇文章中同时谈到这三者,我认为,至少场景应该是同一体系。比如我如果在这里要谈简单工厂和工厂方法,我肯定也会用奥迪汽车、波音汽车作例子,或许会做些改变,比如变成奥迪玩具汽车、奥迪真实汽车、波音玩具汽车、波音真实汽车等等,只要有需要都可以变,但不管怎么样,要同一体系的东西,否则很难具有辨析度。目前我看到的一些文章中,通常它们换个模式就换个场景,而且有些讲得也不是非常清楚,或者场景过于庞大。如果那位看官能给出这样的例子那最好不过了,呵呵。
最后,如果我有理解错误的,请各位不吝指出,这也是我发表这篇文章的目的之一。曾看到有篇文章楼主在讲抽象工厂,后面有评论说“只不过是个简单工厂而已”,希望我没犯这样的错误。呵呵。
溪流
2008年12月6日
首发:https://blog.csdn.net/cnStreamlet/article/details/3478674