抽象工厂学习小记

 

由于讨论班上我要讲 Microsoft .Net PetShop 4.0,里面用了工厂模式,所以不得不去了解。我之前听过“设计模式”这个词语,以及“没有写过十万行代码不要去谈设计模式”这句话,但精确的概念我基本上是不知道的。这阵子断断续续地在网上找了些文章看,也翻了些书,自认为模模糊糊地有点理解了,故写篇小记,一来检验自己是否真的有些理解了、能不能用文字描述清楚,二来如果有错误之处可以请大家指出。

对于读者您,您最好已经初步了解了 C#/.Net 和面向对象思想。本文用 C# 描述。

首先,假定有这么两个类——奥迪汽车和波音汽车。(当我自以为风趣地构造出波音造汽车这个词的时候,很无奈地看到波音可能确实造汽车了。)简单起见,只给它们一个方法 Run()。和大多数例程一样,该方法仅仅在控制台输出文字,用于区分是哪个类的方法。

代码如下:

  1. namespace 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# 语言支持接口这个概念,因此这里抽象出接口。——已经熟悉面向对象的读者可以略过相关内容。

现在代码变成这样子了:

  1. namespace 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();),除非重新编译此程序,否则只能继续用奥迪。我们看到写“死”的程序面对变化的需求真是太无力了。所以我们要解决这个灵活性问题,也就是让客户可以决定用奥迪还是波音,并且在开发人员不介入的情况下改变到已部署的系统中去。

以上场景并不复杂,同时跟我们要讲的抽象工厂模式还差了一步。我们继续将场景复杂化,引入第二种产品——飞机,也有奥迪飞机和波音飞机。(奥迪总算不产飞机的吧?)

同样使用接口,代码如下:

  1. namespace 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.             car.Run();
  52.             // Sample2 中的问题还未解决
  53.             // 这里还可以看到,随着“物品”(汽车、飞机)的增加
  54.             // 要改动的地方会越来越多
  55.             // 也就是维护成本会变得越来越高
  56.         }
  57.     }
  58. }

 

相应的图示变为:

 

 

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

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

  1. namespace 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 了。当然,在这样的情形下,上面的工厂类可以用静态类,之所以没有,是为了下面要讲的抽象工厂。

很明显地看到两个工厂具有共性,于是可以抽象出他们的父类。这里不使用接口,因为我希望这个父类有自己的内容——提供一个静态方法用于产生具体工厂的对象。

代码如下:

  1. namespace 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()。这时,具体工厂也不是直接构造了,而改由抽象工厂构造。要得到一个具体工厂,就:
    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>

 

现在可以把
    string factory = "Audi";
换成
    string factory = ConfigurationManager.AppSettings["Factory"];
了,这样,程序中便不出现 Audi 或 Boeing 的选择了,要改变选择,改配置文件即可。

以上说的是传统意义上的抽象工厂。

接下来,我们来看看 .Net 技术下抽象工厂的一些变化。从上面的例子来看,我们本来是直接把类的选择写定在代码里的,后来变成了由一个存储在程序外部的字符串控制,从而解决了灵活性问题的。然而,对这个存储在外部字符串,我们在程序里要来个 switch case,这样显得很难看,如果能够直接给个类名字串就能创建对象(如,ICar = new GetClass(“AudiCar”)),那就方便了。.Net 的反射机制正好为此提供了可能。

所谓反射,抄一段网上的话——

 “程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。您可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。

初步理解,好像可以从已编译的程序集中获取类型信息。先不管这么多了,反正我们现在在自己的系统中,可以看到自己的源代码,只要事先给出类名字符串得到类的对象就行了。

我们来改造刚才的抽象工厂类 AbstractFactory:

  1. namespace 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() 方法,原来一大串变成了一句话。看:
    Activator.CreateInstance(Type.GetType("Sample6." + factoryType + "Factory"));
Type.GetType() 可以从一个类名得到含有该类的信息的 Type 对象,注意这里的参数是完整的类名,要包含命名空间。然后 Activator.CreateInstance() 可以由含该类的信息的 Type 对象创建这个类的实例。在这个例子中,我们这样就去掉了 swich case。

其实可以将反射应用地更加彻底一些。可能在看上面的例程的时候您已经想到了,何不只用一个工厂类,在每个 CreateXXX() 的函数里来判断呢?可是看到一段 switch,又否定了自己的想法了。没错,刚才不这样做正因为有着这段不好看的代码在,这应该也是引入抽象工厂的原因之一吧,至少让这段 switch case 的出现次数减少到了仅有的一次。而现在,没有了 switch case,我们判别不同的类只需要一句话,何不去掉抽象工厂类呢?

就这么干:

  1. namespace 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日

 

(原发表于 CSDN:https://blog.csdn.net/cnStreamlet/article/details/3478674)

posted on 2008-12-08 21:11  溪流  阅读(40)  评论(0编辑  收藏  举报