上章讲的是创建型的设计模式,工厂方法(上),这次要讲的是另一本书关于工厂方法的一些概念以及案例、模型等等。就像电影“风雨哈佛路”中那个老师提问,为什么要用另外的一张一张纸质资料,而不直接用书籍。女主回答说,因为不同的资料汇集了不同人的思想。
假设你有一个关于个人事务管理的项目,功能之一是管理预约对象(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章