转载

Chap3:创建型设计模式————工厂方法设计模式(下)

上章讲的是创建型的设计模式,工厂方法(上),这次要讲的是另一本书关于工厂方法的一些概念以及案例、模型等等。就像电影“风雨哈佛路”中那个老师提问,为什么要用另外的一张一张纸质资料,而不直接用书籍。女主回答说,因为不同的资料汇集了不同人的思想。

工厂方法模式

假设你有一个关于个人事务管理的项目,功能之一是管理预约对象(Appointment)。现在要和另一个公司建立关系,需要一个叫做BloggsCal的格式来和他们交流预约相关的数据。但是你将来可能要面对更多的数据格式

在接口上可以立即定义两个类,

1.Class ApptEncoder:数据编码器,将Appointment转换成一个专有格式2.Class CommsManager:管理员类,用来获取数据编码器,并使用编码器进行第三方通信

使用模型属于来描述的话,CommsManager就是 创建者(Creator) ,而ApptEncoder是 产品(product)

那么如何得到一个具体的ApptEncoder对象?

<?php abstract class ApptEncoder{  //产品类  abstract function encode(); } class BloggsApptEncoder extends ApptEncoder{  //实际产品1  function encode(){   return "Appointment data encoded in BloggsCal Format/n";  } } class MegaApptEncoder extends ApptEncoder{   //实际产品2  function encode(){   return "Appointment data encoded in MegaCal Format/n";  } } class CommsManager{  //创建者(管理者)  function getApptEncoder(){   return new BloggsApptEncoder();  } } ?> 

CommsManager类负责生成BloggsApptEncoder对象,但是当你和合作方关系改变,被要求转换系统来使用一个新的格式MegaCal时,那么代码就需要做另外的改变了

class CommsManager{  const BLOGGS = 1;  const MEGA = 2;  private $mode =1;  function __construct($mode){   $this->mode = $mode;  }  function getApptEncoder(){   switch($this->mode){    case (self::MEGA):     return new MegaApptEncoder();    default:     return new BloggsApptEncoder();   }  } } $comms = new CommsManager(CommsManager::MEGA); $appt = $comms->getApptEncoder(); print $appt->encode(); 

在类中我们使用常量标志定义了脚本可能运行的两个模式:MEGA和BLOGGS,在getApptEncoder()方法中使用switch语句来检查$mode属性,并实例化相关编码器

但是这种方法还有一种小缺陷,通常情况下,创建对象需要指定条件,但是有时候条件语句会被当作Awful的“Code taste”,因为 可能会导致重复的条件语句 蔓延在代码中。我们知道创建者已经能够提供交流日历数据的功能,但是如果合作方要求提供页眉和页脚来约束每次预约,那该怎么办?

结果是,你需要在上面的代码中加入新的方法

function getHeaderText(){       switch($this->mode){             case (self::MEGA):                 return "This is Mega format header!/n";             default:                 return "This is Bloggs format header!/n";         } }

Obviously,这会使得它在getApptEncoder()方法同时使用时,重复地使用了switch判断,一旦客户要增加其它需求,那工作量以及冗余程度会更重

总结一下当前需要思考的:

1.在代码运行时我们才知道要生成的对象类型(BloggsApptEncoder或者是MegaApptEncoder)

2.我们需要能够相对轻松地加入一些新的产品类型(如新的业务处理方式SyncML)

3.每一个产品类型都可定制特定的功能(如上文提到的页眉页脚)

另外注意我们使用的条件语句,其实可以被多态替代,而工厂方法模式恰好能让我们用继承和多态来封装具体产品的创建,黄菊花说,我们要为每种协议创建CommsManager的每一个子类,而每一个子类都要实现getApptEncoder方法

实现

工厂方法模式把创建者类与要生产的产品分离开来。创建者是一个工厂类,其中定义了用于生成产品对象的类方法,如果没有提供默认实现,那么就由创建者类的子类来执行实例化。一般来说,就是创建者类的每个子类实例化一个相应产品子类

所以我们把CommsManager重新指定为抽象类,这样就可以得到一个灵活的父类,并把所有特定协议相关的代码放到具体的子类中

下面是简化过的代码:

abstract class ApptEncoder{  abstract function encode(); } class BloggsApptEncoder extends ApptEncoder{  function encode(){   return "Appointment data encode in BloggsCal format!/n";  } } abstract class CommsManager{  abstract class getHeaderText();  abstract class getApptEncoder();  abstract class getFooterText(); } class BloggsCommsManager extends CommsManager{  function getHeaderText(){   return "BloggsCal Header";  }   function getHeaderText(){   return new BloggsApptEncoder();  }   function getFooterText(){   return "BloggsCal Footer";  } } 

现在当我们要求实现MegaCal时,只需要给CommsManager抽象类写一个新的实现

注意到上面的创建者类与产品的层次结构很相似,这是使用工厂方法模式的常见结果,形成了一种特殊的代码重复。另一个问题是该模式可能会导致不必要的子类化,如果你为创建者创建子类的原因是为了实现工厂方法模式,那么最好再考虑一下(这就是为什么在例子中引入页眉页脚)

抽象工厂模式

上面例子中我们只关注了预约功能。

我们通过加入更多编码格式,使结构“横向”增长

如果想扩展功能,使其能够处理待办事宜和联系人,那应该让它进行纵向增长

实现

CommsManager抽象类定义了用于生成3个产品(ApptEncoder、TtdEncoder、ContactEncoder)的接口,我们需要先实现一个具体的创建者,然后才能创建一个特定类型的具体产品,下图模型创建了BloggsCal格式的创建

下面是CommsManager和BloggsCommsManager的代码

abstract class CommsManager{  abstract function getHeaderText();  abstract function getApptEncoder();  abstract function getTtdEncoder();  abstract function getContactEncoder();  abstract function getFooterText(); } class BloggsCommsManager extends CommsManager{  function getHeaderText(){   return "BloggsCal header/n";  }  function getApptEncoder(){   return new BloggsApptEncoder();  }  function getTtdEncoder(){   return new BloggsTtdEncoder();  }  function getContactEncoder(){   return new BloggsContactEncoder();  }  function getFooterText(){   return "BloggsCal footer/n";  } } 

在这个例子中使用了工厂方法模式,getContactEncoder()是CommsManager的抽象方法,并在BloggsCommManager中实现。设计模式间经常会这样写作:一个模式创建可以把它自己引入到另一个模式的上下文环境中,我们加入了对MegaCal格式的支持

这样的模式带来了什么?

1.系统与实现的细节分离开来,我们可以在实例中添加移除任意树木的编码格式而不会影响系统

2.对系统中功能相关的元素强制进行组合,因此通过使用BloggsCommsManager,可以确保值使用与BloggsCal相关的类

3.添加新产品比较麻烦,不仅要创建新产品的具体实现,而且必须修改抽象创建者和它的每一个具体实现

我们可以创建一个使用标志来决定返回什么对象的单一make()方法,而不用给每个工厂方法创建独立的方法,如下

abstract class CommsManager{  const APPT = 1;  const TTD = 2;  const CONTACT = 3;  abstract function getHeaderText();  abstract function make($flag_int);  abstract function getFooterText(); } class BloggsCommsManager extends CommsManager{  function getHeaderText(){   return "BloggsCal header";  }  function make($flag_int){   switch($flag_int){    case self::APPT:     return new BloggsApptEncoder();    case self::CONTACT:     return new BloggsContactEncoder();    case self::TTD:     return new BloggsTtdEncoder();   }  }  function getFooterText(){   return "BloggsCal footer/n";  } } 

类的接口更加紧凑,但也有代价,在使用工厂方法时,我们定义了一个清晰的接口强制所有具体工厂对象遵循它,而使用丹仪的make()方法,我们必须在所有的具体创建者中支持所有的产品对象。每个具体创建者都必须实现相同的标志检测(flag),客户类无法确定具体的创建者是否可以生成所有产品,因为make方法需要对每种情况进行考虑并进行选择

本章参考 《深入PHP:面向对象、模式与实践》第9章

正文到此结束
Loading...