1

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

工厂方法模式

假设你有一个关于个人事务管理的项目,功能之一是管理预约对象(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抽象类写一个新的实现
clipboard.png

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

抽象工厂模式

上面例子中我们只关注了预约功能。
我们通过加入更多编码格式,使结构“横向”增长
如果想扩展功能,使其能够处理待办事宜和联系人,那应该让它进行纵向增长
clipboard.png

实现

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

下面是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章


R_Jeff
405 声望21 粉丝

坚持拍黄片的大四网页狗要成为PHPer