1、常规的对象创建方法(以更换QQ空间主题为例)
(这里的常规对象指的是由于业务需求,当前实例化的对象有可能被其他相似的对象(有着共同的特性)所取代,例如更换手机铃声:一首歌取代另一首歌(词,曲,和声是他们的共同有的特性(这里说的词,曲,和声是当前实例化对象的属性,如果是对象的话,那我们就要使用抽象工厂了,第4会开始讲),只是词,曲的风格是不一样的!))
1 ModernTheme theme=new ModernTheme();//现代主题
常规的对象创建方法引发的问题是:当我们用户这时需要更换成经典主题(ClassicTheme),那我们程序中用到ModernTheme的地方,都要重新实例化成ClassicTheme了,那么每一次更换主题,都要更换程序代码,代码的更改量将会很大!也不便于程序维护!
2、常规对象创建的解决思路
- 封装变化点 —— 哪里变化,封装哪里
- 潜台词:如果没有变化,当然不需要额外的封装!
- 应该遵循“开闭原则”。即:封闭对原来代码的修改,开放对原来代码的扩展(如类的继承,接口的实现)
3、常规对象创建的解决方法
为能够相互取代的对象都实现同一接口:
那我们代码就可以变为
但是还是无法解决问题的,标号为2的地方我们还是需要更改的,这里我们可以这样解决, 利用配置xml文件进行发射实例化具体的主题类
xml文件:
1 23 4
实例化对象的代码就变为:
1 XmlDocument document = new XmlDocument();2 document.Load("F:\\Theme.xml");3 XmlNode nodes = document.SelectSingleNode("ThemeSettings");4 string key = nodes.SelectSingleNode("Theme").Attributes["value"].Value;//获得要实例化的对象名称5 Assembly assembly = Assembly.LoadFile("F:\\ModernTheme\\ModernTheme.dll");//加载现代主题Dll6 Type t = assembly.GetExportedTypes()[0];7 ITheme theme = (ITheme)Activator.CreateInstance(t); //实例化具体的主题对象 8 //这里的反射我们可以通过缓存进行处理,如果缓存中有此对象,就从缓存中取,如果每次都进行反射!会影响程序的效率!
这样一写,我们就不需要更改程序代码了,只需要配置xml文件即可!从而达到了对修改关闭的效果了!
说了这么多,有的人会认为我上面所说的都是和抽象工厂无关的!都是些废话,但对于我而言不是这么认为的,只是为讲解抽象工厂做的铺垫!
4、引出抽象工厂
当我们想创建一系列相互依赖的对象时,我们该怎么办?
一系列相互依赖的对象:例如更换QQ空间主题,主题类下面的有导航,装饰,播放器,动画,皮肤等一系列相互以来的对象,每一个对象的位置都影响其他对象的位置,每一个对象的皮肤颜色都影响这其他对象皮肤的颜色(一个主题基本色调要确定)!
假如我现在想更换成非主流主题,那么这个非主流主题就对应着常规对象创建中的ModernTheme,又想换成欧式风格主题,那么就对应着常规对象创建中的ClassicTheme,不同的是:一系列对象的创建,他的成员是多个相互依赖的对象,而常规对象的创建,他的成员是多个属性或者字段,不是对象!
如果我们按照常规对象创建的方法来应用到创建一系列对象上去的话,那么应该代码为:
1 //非主流主题 2 //播放器 3 XmlDocument document = new XmlDocument(); 4 document.Load("F:\\Theme.xml"); 5 XmlNode nodes = document.SelectSingleNode("PlayerSettings"); 6 string key = nodes.SelectSingleNode("Player").Attributes["value"].Value; 7 Assembly assembly = Assembly.LoadFile("F:\\ModernTheme\\bin\\Debug\\AlternativeTheme.dll"); 8 Type t = assembly.GetExportedTypes()[0]; 9 IPlayer theme = (IPlayer)Activator.CreateInstance(t);10 //导航11 document.Load("F:\\Theme.xml");12 XmlNode nodes = document.SelectSingleNode("NavigationSettings");13 string key = nodes.SelectSingleNode("Navigation").Attributes["value"].Value;14 Assembly assembly = Assembly.LoadFile("F:\\ModernTheme\\bin\\Debug\\AlternativeTheme.dll");15 Type t = assembly.GetExportedTypes()[1];16 INavigation theme = (INavigation)Activator.CreateInstance(t);17 //装饰18 document.Load("F:\\Theme.xml");19 XmlNode nodes = document.SelectSingleNode("DecorateSettings");20 string key = nodes.SelectSingleNode("Decorate").Attributes["value"].Value;21 Assembly assembly = Assembly.LoadFile("F:\\ModernTheme\\bin\\Debug\\AlternativeTheme.dll");22 Type t = assembly.GetExportedTypes()[2];23 IDecorate theme = (IDecorate)Activator.CreateInstance(t);
这时我们想换成非主流主题的话,那么就需要去更改配置文件下的播放器,导航,装饰的代码了,修改成非主流播放器,非主流导航,非主流装饰,每次更换一下主题,都要更换xml下这三个对象的值
缺点:1、多次运用反射,效率降低
2、对象之间的依赖关系没有体现
3、容易出错,xml文件容易配置出错!
4、代码冗余,不够直观的表达出更换主题的意图
运用抽象工厂,"工厂"顾名思义就是创建一系列对象的地方,我们可以把对播放器、导航、装饰着三个对象放在抽象工厂里面去创建,我们叫工厂创建非主流风格的QQ空间主题,那么工厂内部就会帮我们创建好带有非主流风格的播放器,导航,装饰,然后根据三者的依赖关系进行组合,从而形成非主流QQ空间主题!
既然我们有时候会向工厂要非主流QQ空间主题,有时候也会向工厂要欧式QQ空间主题或者其他主题,这时我们就可以利用接口来应变这种需求!
代码实现为:
1 abstract ThemeAbstractFactory 2 { 3 public abstract Player CreatePlayer(); //Player为抽象类 4 public abstract Navigation CreateNavigation();//Navigation为抽象类 5 public abstract Decorate CreateDecorate();//Decorate为抽象类 6 } 7 //非主流主题 8 class AlternativeTheme : ThemeAbstractFactory 9 {10 public override Player CreatePlayer()11 {12 return new AlternativeRoad();13 }14 public override Navigation CreateNavigation()15 {16 return new AlternativeRoad();17 }18 public override Decorate CreateDecorate()19 {20 return new AlternativeRoad();21 }22 }23 class ThemeManager24 {25 ThemeAbstractFactory themeFactory;26 public ThemeManager(ThemeAbstractFactory themeFactory)27 {28 this.themeFactory = themeFactory;29 }30 //创建主题下各个对象31 public void BuildThemeObject()32 {33 Player player = themeFactory.CreatePlayer();34 Navigation navigation = themeFactory.CreateNavigation();35 Decorate decorate = themeFactory.CreateDecorate();36 //实现三者依赖的代码实现37 }38 }
客户端代码为:
1 XmlDocument document = new XmlDocument();2 document.Load("F:\\Theme.xml");3 XmlNode nodes = document.SelectSingleNode("ThemeSettings");4 string key = nodes.SelectSingleNode("Theme").Attributes["value"].Value;5 Assembly assembly = Assembly.LoadFile("F:\\AlternativeTheme \\AlternativeTheme .dll");6 Type t = assembly.GetExportedTypes()[0];7 ThemeAbstractFactory theme = (ThemeAbstractFactory)Activator.CreateInstance(t); 8 ThemeManager manager = new ThemeManager(theme);
因为更换主题,无非是更换这三者类型的值,所以我们就可以把三者抽象出来,称为Player,Navigation,Decorate三个抽象类
无论我们怎样更换主题,ThemeManager主题管理类是不需要变的,运用了抽象与接口的相结合,达到了对修改关闭的效果,
每次更换主题我们就可以修改下配置文件即可!代码量远远减少,可读性强!
5、整体结构:
6、应对“新对象”产生的问题
抽象工厂模式主要在于应对“新系列”的需求变化。其缺点在于难于应付“新对象”的需求变动,如果非主流主题下需要一个动画对象,那么ThemeManager、ThemAbstractFactory都需要跟着变,这就违反了设计模式的原则了!如果在开发中出现了新对象,该如何去解决呢?这个问题并没有一个好的答案,下面我们看一下李建忠老师的回答:“GOF《设计模式》中提出过一种解决方法,即给创建对象的操作增加参数,但这种做法并不能令人满意。事实上,对于新系列加新对象,就我所知,目前还没有完美的做法,只有一些演化的思路,这种变化实在是太剧烈了,因为系统对于新的对象是完全陌生的。“
如果你的项目中,对象的变更是很长一段时间才变更,那么适合使用抽象工厂模式,如果是经常变更的话,就不适合使用抽象工厂模式了!
7、实现要点
- 抽象工厂将产品对象的创建延迟到它的具体工厂的子类。
- 如果没有应对“多系列对象创建”的需求变化,则没有必要使用抽象工厂模式,这时候使用简单的静态工厂完全可以。
- 系列对象指的是这些对象之间有相互依赖、或作用的关系,例如游戏开发场景中的“道路”与“房屋”的依赖,“道路”与“地道”的依赖。
- 抽象工厂模式经常和工厂方法模式共同组合来应对“对象创建”的需求变化。
- 通常在运行时刻创建一个具体工厂类的实例,这一具体工厂的创建具有特定实现的产品对象,为创建不同的产品对象,客户应使用不同的具体工厂。
- 把工厂作为单件,一个应用中一般每个产品系列只需一个具体工厂的实例,因此,工厂通常最好实现为一个单件模式。
- 创建产品,抽象工厂仅声明一个创建产品的接口,真正创建产品是由具体产品类创建的,最通常的一个办法是为每一个产品定义一个工厂方法,一个具体的工厂将为每个产品重定义该工厂方法以指定产品,虽然这样的实现很简单,但它确要求每个产品系列都要有一个新的具体工厂子类,即使这些产品系列的差别很小。
8、优点
- 分离了具体的类。抽象工厂模式帮助你控制一个应用创建的对象的类,因为一个工厂封装创建产品对象的责任和过程。它将客户和类的实现分离,客户通过他们的抽象接口操纵实例,产品的类名也在具体工厂的实现中被分离,它们不出现在客户代码中。
- 它使得易于交换产品系列。一个具体工厂类在一个应用中仅出现一次——即在它初始化的时候。这使得改变一个应用的具体工厂变得很容易。它只需改变具体的工厂即可使用不同的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻改变。
- 它有利于产品的一致性。当一个系列的产品对象被设计成一起工作时,一个应用一次只能使用同一个系列中的对象,这一点很重要,而抽象工厂很容易实现这一点。
9、缺点
难以支持新种类的产品。难以扩展抽象工厂以生产新种类的产品。这是因为抽象工厂接口确定了可以被创建的产品集合,支持新种类的产品就需要扩展该工厂接口,这将涉及抽象工厂类及其所有子类的改变。
Abstract Factory模式主要在于应对“新系列”的需求变动。其缺点在于难以应对“新对象”的需求变动。
10、适用性
在以下情况下应当考虑使用抽象工厂模式:
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有形态的工厂模式都是重要的。
- 这个系统有多于一个的产品族,而系统只消费其中某一产品族。
- 同属于同一个产品族的产品是在一起使用的,这一约束必须在系统的设计中体现出来。
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于实现。
11、应用场景
- 支持多种观感标准的用户界面工具箱(Kit)。
- 游戏开发中的多风格系列场景,比如道路,房屋,管道等。
- ……
12、总结
总之,抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,运用抽象工厂模式的关键点在于应对“多系列对象创建”的需求变化。
13、参考
《C#面向对象设计模式纵横谈》 李建忠
TerryLee
作者:MrZivChu
2013-08-05 12:57:36