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