首页 / 技术类 / 设计 / 抽象工厂学习小记

抽象工厂学习小记

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}

相应的图示变为:

场景复杂了,但是灵活性问题还没解决。我们看到,场景是可以任意复杂下去的,只要不断地增加“物品”即可。再来点奥迪/波音卡车、奥迪/波音高射炮之类的,之后的维护代价会大大增加。

下面得引出“工厂”的概念了,先不忙着讲,请看代码,注意最后两个类 AudiFactoryBoeingFactory

 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}

最后面这两个类就是所谓的“工厂”,但不是抽象工厂,是具体工厂。具体工厂并不是为了解决上文提出的灵活性问题的,而是解决对象构造问题的。在有工厂的系统中,产品类(AudiCarBoeingCarAudiPlaneBoeingPlane)往往是不被直接构造的。原因呢,看到有一篇文章里说,可能需要很多初始化工作要完成,而这些工作是不适合放在构造函数的。但也不尽是这样。现在不妨理解为为了使用工厂而是用工厂吧。

现在,要得到一个产品对象,得先有一个工厂对象,再调用相应的方法,而不是直接 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



NoteIsSite/0.4