当前位置: 首页 > 文档资料 > PHP 设计模式 >

第三章 工厂模式(The Factory Pattern)

优质
小牛编辑
127浏览
2023-12-01

在面向对象编程中, 最通常的方法是一个new操作符产生一个对象实例,new操作符就是用来构造对象实例的。但是在一些情况下, new操作符直接生成对象会带来一些问题。举例来说, 许多类型对象的创造需要一系列的步骤: 你可能需要计算或取得对象的初始设置; 选择生成哪个子对象实例; 或在生成你需要的对象之前必须先生成一些辅助功能的对象。 在这些情况, 新对象的建立就是一个 “过程”,不仅是一个操作,像一部大机器中的一个齿轮传动。

问题

你如何能轻松方便地建立这么" 复杂 " 的对象即操作中不需要粘贴复制呢?

解决方法

建立一个工厂(一个函数或一个类方法)来制造新的对象。为了理解工厂的用处, 试想以下的不同之处……

代码:

$connection =& new MySqlConnection($user, $password, $database);

……使你的代码可扩展和更简洁……

$connection =& create_connection();

后者的代码片断集中在和数据库连接的create_connect()工厂上 ,就像刚才说的一样,使创造数据库连接的过程成为一个简单的操作—就像new操作一样。工厂模式的优点就在创建对象上。 它的任务就是把对象的创建过程都封装起来,然后返回一个所需要的新类。

想改变对象的结构和建立对象的方式吗? 你只需选择对象工厂,对代码的改变只需要一次就够了。( 工厂模式的功能是如此强大, 它处于是应用的底层, 所以在许多其余的复杂模式和应用中它会不停地出现。)

样本代码

工厂模式封装了对象的建立过程。 你可以在对象本身创建对象工厂或者是一个额外的工厂类——这要看你具体的应用。让我们看一个工厂对象的例子。

我们发现下面代码中,数据库连接的那部分屡次出现:

// PHP4

class Product {

function getList() { $db =& new MysqlConnection(DB_USER, DB_PW, DB_NAME);

//...

}

function getByName($name) { $db =& new MysqlConnection(DB_USER, DB_PW, DB_NAME);

//...

}

//...

}

为什么这样做不好? 数据库连接的参数出现的地方太多了,当你把这些参数设成常量,意味着你统一定义并对他们进行赋值,显然这种做法不是很妥当:

1. 你可以轻松地改变连接数据库的参数,但你不能增加或改变这些参数地顺序,除非你把所有连接代码都改了。

2. 你不能轻松的实例化一个新类去连接另一种数据库,比如说PostgresqlConnection。

3. 这样很难单独测试和证实连接对象的状态。

使用工厂设计模式,代码将得到很大的改进:

class Product {

function getList() {

$db =& $this->_getConnection();

//...

}

function &_getConnection() {

return new MysqlConnection(DB_USER, DB_PW, DB_NAME);

}

}

先前的类中存在很多调用new MysqlConnection(DB_USER, DB_PW, DB_NAME)的方法,现在都被集中到的_getConnection()方法上。

下面是工厂的另一种变化,你静态地调用了一个工厂类:

class Product {

function getList() {

$db =& DbConnectionBroker::getConnection();

//...

}

}

class DbConnectionBroker {

function &getConnection() {

return new MysqlConnection(DB_USER, DB_PW, DB_NAME);

}

}

这里DbConnectionBroker::getConnection()产生的效果和前面的一样 ,但这样却很有好处: 我们不必在每个需要连接数据库的类中加入调用new MysqlConnection(DB_USER , DB_PW, DB_NAME)的方法。

当然另一种变化就是引用一个外部工厂对象的资源,和这个对象定义了数据库连接的参数:

class Product {

var $_db_maker;

function setDbFactory(&$connection_factory) {

$this->_db_maker =& $connection_factory;

}

function getList() {

$db =& $this->_db_maker->getConnection();

//...

}

}

最后,一个工厂可以用一个函数合理的组织,然后实现:

function &make_db_conn() {

return new MysqlConnection(DB_USER, DB_PW, DB_NAME);

}

class Product {

function getList() {

$bar =& make_db_conn();

//...

}

}

下面是一个工厂的理想化执行的 UML 类图:

function TestInstantiate() {

$this->assertIsA($color = new Color, ‘Color’);

$this->assertTrue(method_exists($color, ‘getRgb’));

}

为了满足这个测试,你可以设计这样一个类。测试看起来有点像伪代码:

class Color {

function getRgb() {}

}

( 这个Color类也许看起来像处于娃娃阶段, 但是 TDD是一个反复的过程。 代码非常少,当需要的时候:你开始有了新的想法或者想精确的执行代码时,可以增加的。)

接下来, 当颜色对象被建立时,getRgb() 方法应该返回以红色,绿色,和蓝色的十六进制字符串。 用一个测试说明一下:

function TestGetRgbWhite() {

$white =& new Color(255,255,255);

$this->assertEqual(‘#FFFFFF’, $white->getRgb());

}

每个 TDD, 你写最简单的可行的代码来满足测试, 并不需要满足人的审美观或者代码的正确执行。

下面是最简单的,能够通过测试的代码:

class Color {

function getRgb() { return ‘#FFFFFF’; }

}

这个Color类不是令人十分满意, 但是它确实表现了逐渐增加的过程。

下一步,让我们增加一个额外的测试使这个Color类的的确确地执行并返回正确的信息:

function TestGetRgbRed() {

$red =& new Color(255,0,0);

$this->assertEqual(‘#FF0000’, $red->getRgb());

}

这个Color类必须改变什么呢? 首先必须把红色,绿色,和蓝色的值储存在三个变量里,然后在调用一个方法把十进制数转化为十六进制数。按照这个要求执行的代码可以写作:

class Color {

var $r=0;

var $g=0;

var $b=0;

function Color($red=0, $green=0, $blue=0)

{

$this->r =$red;

$this->g = $green;

$this->b = $blue;

}

function getRgb() {

return sprintf(‘#%02X%02X%02X’, $this->r, $this->g, $this->b);

}

}

这个构造非常简单: 先收集红色,绿色,和蓝色的数值,然后储存在变量中, getRgb() 方法使用 sprintf() 函数将十进制数转换成十六进制数。

为了对代码充满更多的信心, 你可以用较多的数值来测试它。 这一个测试可以用以下代码实现:

function TestGetRgbRandom() {

$color =& new Color(rand(0,255), rand(0,255), rand(0,255));

$this->assertWantedPattern(

‘/^#[0-9A-F]{6}$/’,

$color->getRgb());

$color2 =& new Color($t = rand(0,255), $t, $t);

$this->assertWantedPattern(

‘/^#([0-9A-F]{2})\1\1$/’,

$color2->getRgb());

}

注:assertWantedPattern

assertWantedPattern() 作用是:使它的第二个叁数匹配第一个参数,第一个参数是正则表达式。如果匹配,这个测试就通过; 否则不通过。

由于assertWantedPattern()具有进行正确的正则表达式匹配的功能,所以常被用作测试。

所有这些测试Color类功能的行为都在正常和期望的环境下实现的。但是每一个设计精良的类都必须考虑边界情况。例如, 被送入构造器执行的数值如果是负数,或者大于255的数值,或者根本不是数值,结果会出现什么呢?一个好的类定义应该适应测试中的多种边界情况。

function testColorBoundaries() {

$color =& new Color(-1);

$this->assertErrorPattern(‘/out.*0.*255/i’);

$color =& new Color(1111);

$this->assertErrorPattern(‘/out.*0.*255/i’);

}

注:assertErrorPattern

assertErrorPattern() 作用是:将产生的php错误进行正确的正则表达式匹配。如果这个错误不匹配指定的模式, 将不通过测试。

在那些测试的基础上,Color类将得到更进一步改进:

class Color {

var $r=0;

var $g=0;

var $b=0;

function Color($red=0, $green=0, $blue=0) {

$red = (int)$red;

if ($red < 0 || $red > 255) {

trigger_error(�color ‘$color’ out of bounds, ?/span>

.�please specify a number between 0 and 255?;

}

$this->r = $red;

$green = (int)$green;

if ($green < 0 || $green > 255) {

trigger_error(�color ‘$color’ out of bounds, ?/span>

.�please specify a number between 0 and 255?;

}

$this->g = $green;

$blue = (int)$blue;

if ($blue < 0 || $blue > 255) {

trigger_error(�color ‘$color’ out of bounds, ?/span>

.�please specify a number between 0 and 255?;

}

$this->b = $blue;

}

function getRgb() {

return sprintf(‘#%02X%02X%02X’, $this->r, $this->g, $this->b);

}

}

这个代码通过了测试, 但是这种 " 剪切和粘贴 " 的风格有点使人厌倦。 在 TDD,

一个经验法则就是将编码最简单的实现,如果你两次需要相同的代码,可以将其改进,但不要复制代码。 然而你往往需要三次或三次以上的一样代码。 因此我们可以提取一个方法即重构实现这个工作。

注:重构 - 提取方法

当你的代码中有两个或者两个以上部分的代码相似的时候, 可以将它们提取出来成为一个独立的方法,并按它的用途命名。当你的类的方法代码中频频出现相同的成分,提取代码作为一个方法是非常有用的。

class Color {

var $r=0;

var $g=0;

var $b=0;

function Color($red=0, $green=0, $blue=0) {

$this->r = $this->validateColor($red);

$this->g = $this->validateColor($green);

$this->b = $this->validateColor($blue);

}

function validateColor($color) {

$check = (int)$color;

if ($check < 0 || $check > 255) {

trigger_error(�color ‘$color’ out of bounds, ?/span>

.�please specify a number between 0 and 255?;

} else {

return $check;

}

}

function getRgb() {

return sprintf(‘#%02X%02X%02X’, $this->r, $this->g, $this->b);

}

}

创建工厂来简化对象的创建过程

让我们为Color类增加一个工厂,使得建立新的实例更简单。增加一个可以命名颜色的方法,这样就可以不记颜色数值,只需要记住自己喜欢的颜色名字。

工厂对象或函数不一定都要被命名为 “工厂”。 当你读代码时,工厂是显而易见的。 相反的,它的名字最好取得有意义,这样可以反映出它解决了什么问题。

在这个代码例子中, 我要叫它CrayonBox颜色工厂。静态的方法CrayonBox::getColor()引入命名颜色的字符串后,返回一个带有相应颜色属性的Color类。

下面的例子就可以测试这一点:

function TestGetColor() {

$this->assertIsA($o =& CrayonBox::getColor(‘red’), ‘Color’);

$this->assertEqual(‘#FF0000’, $o->getRgb());

$this->assertIsA($o =& CrayonBox::getColor(‘LIME’), ‘Color’);

$this->assertEqual(‘#00FF00’, $o->getRgb());

}

通过这个测试,我们发现每个返回的对象都是一个实例化的Color类,getRgb() 方法也返回了正确的结果。第一种情况是以“red”都是小写测试,第二种情况是以“LIME”都是大写测试,这样可以测试代码的通用性。

保险起见, 我们再对其进行另外的测试,探究那些不合法的边界情况。TestBadColor() 方法的作用是:用一个不存在的颜色名字引发一个包含这个颜色名字的php错误,并返回黑色。

function TestBadColor() {

$this->assertIsA($o =& CrayonBox::getColor(‘Lemon’), ‘Color’);

$this->assertErrorPattern(‘/lemon/i’);

// got black instead

$this->assertEqual(‘#000000’, $o->getRgb());

}

以下是一个可以满足测试的CrayonBox类:

class CrayonBox {

/**

* Return valid colors as color name => array(red, green, blue)

*

* Note the array is returned from function call

* because we want to have getColor able to be called statically

* so we can’t have instance variables to store the array

* @return array

*/

function colorList() {

return array(

‘black’ => array(0, 0, 0)

,’green’ => array(0, 128, 0)

// the rest of the colors ...

,’aqua’ => array(0, 255, 255)

);

}

/**

* Factory method to return a Color

* @param string $color_name the name of the desired color

* @return Color

*/

function &getColor($color_name) {

$color_name = strtolower($color_name);

if (array_key_exists($color_name,

$colors = CrayonBox::colorList())) {

$color = $colors[$color_name];

return new Color($color[0], $color[1], $color[2]);

}

trigger_error(�No color ‘$color_name’ available?;

// default to black

return new Color;

}

}

这显然地是一个非常简单的工厂, 它确实制造了单一化的对象(使用了颜色名字,而不是RGB数值) ,它展示了在新的对象被调用之前,是如何建立一个内部对象的。

“工厂”促进多态

控制被送回对象的内在状态固然重要, 但是如果促进多态即返回相同的接口多种类的对象,可以使得工厂模式的功能更为强大。

让我们再次看一下Monopoly的例子,然后执行购买游戏中的道具的行为。在游戏中,你的任务就是买道具,包括一些基本动作。更进一步说, 有三种不同的道具: Street,RailRoad和Utility。所有三个类型的道具有一些共同点: 每个道具都被一个玩家拥有; 每个都有价格;而且每个都能为它的拥有者产生租金只要其他的玩家在它上面登陆。但道具之间还是存在差异的,举例来说, 计算租金的多少就取决于道具的类型。

下列的代码展示了一个Property的基本类:

// PHP5

abstract class Property {

protected $name;

protected $price;

protected $game;

function __construct($game, $name, $price) {

$this->game = $game;

$this->name = $name;

$this->price = new Dollar($price);

}

abstract protected function calcRent();

public function purchase($player) {

$player->pay($this->price);

$this->owner = $player;

}

public function rent($player) {

if ($this->owner

&& $this->owner != $player) {

$this->owner->collect(

$player($this->calcRent())

);

}

}

}

这里, Property类和CalcRent() 方法都被声明为基类。

注:术语 – 基类

一个基类就是不能被直接实例化的类。 一个基础的类包含一个或更多的基础方法,这些方法必须在子类被覆盖。一旦所有的抽象方法被覆盖了, 子类也就产生了。

基类为许多相似的类创造了好的原型。

CalcRent() 方法必须在子类被覆盖,从而形成一个具体的类。因此, 每个子类包括:Street,RailRoad和Utility,和必须定义的calcRent() 方法。

为实现以上的情况,这三个类可以定义为:

class Street extends Property {

protected $base_rent;

public $color;

public function setRent($rent) {

$this->base_rent = new Dollar($rent);

}

protected function calcRent() {

if ($this->game->hasMonopoly($this->owner, $this->color)) {

return $this->base_rent->add($this->base_rent);

}

return $this->base_rent;

}

}

class RailRoad extends Property {

protected function calcRent() {

switch($this->game->railRoadCount($this->owner)) {

case 1: return new Dollar(25);

case 2: return new Dollar(50);

case 3: return new Dollar(100);

case 4: return new Dollar(200);

default: return new Dollar;

}

}

}

class Utility extends Property {

protected function calcRent() {

switch ($this->game->utilityCount($this->owner)) {

case 1: return new Dollar(4*$this->game->lastRoll());

case 2: return new Dollar(10*$this->game->lastRoll());

default: return new Dollar;

}

}

}

每个子类都继承了Property类,而且包括它自己的protected ClacRent() 方法。随着所有的基础方法都被定义, 每个子类都被实例化了。

为了开始游戏, 所有的Monopoly道具必须被创建起来。因为这章是介绍工厂模式的,所有Property的类型存在很多共性,你应该想到多态性,从而建立所有需要的对象。

我们还是以道具工厂类开始。 在我住的地方,政府的Assessor(定税人)掌握了税务和契约, 因此我命名它为的道具定税工厂。下一步,这个工厂将制造全部的专有道具。在真正应用时,所有的Monopoly道具的数值可能都取自于一个数据库或者一个文本, 但是对于这一个例子来说, 可以仅仅用一个数组来代替:

class Assessor {

protected $prop_info = array(

// streets

‘Mediterranean Ave.’ => array(‘Street’, 60, ‘Purple’, 2)

,’Baltic Ave.’ => array(‘Street’, 60, ‘Purple’, 2)

//more of the streets...

,’Boardwalk’ => array(‘Street’, 400, ‘Blue’, 50)

// railroads

,’Short Line R.R.’ => array(‘RailRoad’, 200)

//the rest of the railroads...

// utilities

,’Electric Company’ => array(‘Utility’, 150)

,’Water Works’ => array(‘Utility’, 150)

);

}

Property子类需要实例化Monopoly道具。现在,我们只是简单的用一个函数定义实例化变量$game,那么再把它加入Assessor类好了。

class Assessor {

protected $game;

public function setGame($game) { $this->game = $game; }

protected $prop_info = array(/* ... */);

}

也许你会偏向于选择使用数据库记录数据,不会用数组, 因为有一大堆的参数不可避免地要被罗列。如果是这样的话,可以考虑使用" 引入叁数对象 " 进行重构。

注:重构-引入叁数对象

方法中如果有很多参数,常常变得很复杂,而且容易导致错误。你可以引入一个封装参数的对象来替代一大堆的参数。举例来说,“start date” and “end date” 叁数可以用一个 DateRange 对象一起代替。

在Monopoly这个例子中,这个参数对象应该是什么呢?PropertyInfo,怎样?它的目的是使每个道具参数数组引入 PropertyInfo 类的构造器中,然后返回一个新对象。目的就意味着设计, 依照 TDD, 那意味着一个测试情形。

下面一个测试代码就是测试 PropertyInfo 类的:

function testPropertyInfo() {

$list = array(‘type’,’price’,’color’,’rent’);

$this->assertIsA(

$testprop = new PropertyInfo($list), ‘PropertyInfo’);

foreach($list as $prop) {

$this->assertEqual($prop, $testprop->$prop);

}

}

这个测试证明:每个PropertyInfo类都有四个公共属性,而且具有按精确次序排列的叁数。

但是因为实例中 RailRoad 和 Utility 类并不需要颜色或者租用数据, 所以我们需要测试PropertyInfo 也能引入少量的参 数而实例化为RailRoad 和 Utility 类对象:

function testPropertyInfoMissingColorRent() {

$list = array(‘type’,’price’);

$this->assertIsA(

$testprop = new PropertyInfo($list), ‘PropertyInfo’);

$this->assertNoErrors();

foreach($list as $prop) {

$this->assertEqual($prop, $testprop->$prop);

}

$this->assertNull($testprop->color);

$this->assertNull($testprop->rent);

}

注:assertNoErrors()

assertNoErrors() 方法的作用是:证实没有PHP 错误发生。如果有错误, 将不通过测试。

assertNull()

assertNull()方法的作用是:测试第一个参数是否为空。 如果第一个参数不为空, 将不通过测试。像大多数其他测试方法一样,, 你可以选择是否使用第二个叁数定义失败信息。

为了满足前面的测试,PropertyInfo 类定义为:

class PropertyInfo {

const TYPE_KEY = 0;

const PRICE_KEY = 1;

const COLOR_KEY = 2;

const RENT_KEY = 3;

public $type;

public $price;

public $color;

public $rent;

public function __construct($props) {

$this->type =

$this->propValue($props, ‘type’, self::TYPE_KEY);

$this->price =

$this->propValue($props, ‘price’, self::PRICE_KEY);

$this->color =

$this->propValue($props, ‘color’, self::COLOR_KEY);

$this->rent =

$this->propValue($props, ‘rent’, self::RENT_KEY);

}

protected function propValue($props, $prop, $key) {

if (array_key_exists($key, $props)) {

return $this->$prop = $props[$key];

}

}

}

现在PropertyInfo 类可以构造各种不同的Property参数了。同时Assessor类可以提供数据来建立正确的PropertyInfo对象。

现在以Assessor->$prop_info数组提供的数据为基础,新建一个实例化 PropertyInfo 的类。

这样的代码可以是:

class Assessor {

protected $game;

public function setGame($game) { $this->game = $game; }

public function getProperty($name) {

$prop_info = new PropertyInfo($this->prop_info[$name]);

switch($prop_info->type) {

case ‘Street’:

$prop = new Street($this->game, $name, $prop_info->price);

$prop->color = $prop_info->color;

$prop->setRent($prop_info->rent);

return $prop;

case ‘RailRoad’:

return new RailRoad($this->game, $name, $prop_info->price);

break;

case ‘Utility’:

return new Utility($this->game, $name, $prop_info->price);

break;

default: //should not be able to get here

}

}

protected $prop_info = array(/* ... */);

}

这段代码实现了上述功能, 但却非常脆弱。如果代入的值是$this->prop_info数组中没有的值,结果会怎样呢?因为 PropertyInfo 已经被实例化并被加入到Assessor代码中, 没有有效的方法测试被产生的对象。比较好的解决就是:产生一个工厂方法使 PropertyInfo 对象更容易建立。 因此, 下一步将是写一个测试来实现Assessor类中的PropertyInfo方法。

但是,有一个问题: 这个方法不应该是Assessor类的公共接口(API)的一个部份。它能被测试吗?

这里有两个方法, 可以探究任何要求的合理数量的测试。简单的说, 你可以运行黑匣子测试或白匣子测试。

注:黑匣子测试(Black Box Testing)

黑匣子测试就是:把被测试的对象当成" 黑匣子 " ,我们只知道它提供的应用接口(API),但不知道其到底执行了什么。它主要测试对象公共方法的输入和输出。

白匣子测试(White Box Testing)

白匣子测试和黑匣子测试恰恰相反, 它假定知道测试对象中的所有代码信息。这种形式的测试是为了完善代码和减少错误。

关于白匣子测试的详细说明请见:http:// c 2.com/cgi/wiki?WhiteBoxTesting 。

别把话题扯远了。那么如何才在黑匣子和白匣子之间找到折中办法来实现TDD呢呢?一种选择就是使原来的类中的私有方法变为公有,并且在发布的时候变回私有。但这并不是十分令人满意的方式,所以我们建立一个子类,同时使子类中的方法可以从外部访问:

下面就是一个子类的例子:

class TestableAssessor extends Assessor {

public function getPropInfo($name) {

return Assessor::getPropInfo($name);

}

}

这样做的好处是你可以得到正确的Assessor公有接口(API), 但通过 TestableAssessor 类我们就可以来测试Assessor类了。另外, 你用于测试的代码也不会影响到Assessor类。

缺点是:外加的类会带来更多的问题,从而使测试变得更复杂。而且如果你在对象中的一些内部接口作出一些改动, 你的测试将随着你的重构而再次失效。

比较了它的优点和缺点,让我们来看看它的测试方法:

function testGetPropInfoReturn() {

$assessor = new TestableAssessor;

$this->assertIsA(

$assessor->getPropInfo(‘Boardwalk’), ‘PropertyInfo’);

}

为了要保证所有代码的正确执行, 我们可以使用异常处理。 SimpleTest的目前是基于PHP4 搭建的测试的结构,所以不具备异常处理能力。但是你还是可以在测试中使用如下。

function testBadPropNameReturnsException() {

$assessor = new TestableAssessor;

$exception_caught = false;

try { $assessor->getPropInfo(‘Main Street’); }

catch (InvalidPropertyNameException $e) {

$exception_caught = true;

}

$this->assertTrue($exception_caught);

$this->assertNoErrors();

}

最后, Assessor类的执行部分完成了:

class Assessor {

protected $game;

public function setGame($game) { $this->game = $game; }

public function getProperty($name) {

$prop_info = $this->getPropInfo($name);

switch($prop_info->type) {

case ‘Street’:

$prop = new Street($this->game, $name, $prop_info->price);

$prop->color = $prop_info->color;

$prop->setRent($prop_info->rent);

return $prop;

case ‘RailRoad’:

return new RailRoad($this->game, $name, $prop_info->price);

break;

case ‘Utility’:

return new Utility($this->game, $name, $prop_info->price);

break;

default: //should not be able to get here

}

}

protected $prop_info = array(/* ... */);

protected function getPropInfo($name) {

if (!array_key_exists($name, $this->prop_info)) {

throw new InvalidPropertyNameException($name);

}

return new PropertyInfo($this->prop_info[$name]);

}

}

Assessor::getPropInfo()方法从逻辑上说明 PropertyInfo工厂类是作为了Assessor类的一个私有的方法。而Assessor::getProperty() 方法是用来返回三个Property子类的一个,至于返回哪一个子类这要看property的名字。

迟加载(Lazy Loading)的工厂

使用工厂的另一个好处就是它具有迟加载的能力。这种情况常被用在:一个工厂中包括很多子类,这些子类被定义在单独的PHP文件内。

注:术语 - 迟加载

在迟加载模式中是不预加载所有的操作(像包含PHP文件或者执行数据库查询语句),除非脚本中声明要加载。

用一个脚本可以有效地控制多个网页的输出,这是Web常用的方法了。比如一个博客程序,一些入口就有不同的页面来实现,一个简单的评论入口就有:发布评论的页面,一个导航的页面,一个管理员编辑的页面等。 你可以把所有的功能放入一个单独的类中,使用工厂来加载他们。每一个功能类可以单独放在一个文件里,再把这些文件都放在“pages”这个子文件夹里,这样可以方便调用。

实现迟加载的页面工厂(page factory)的代码可以写作:

class PageFactory {

function &getPage() {

$page = (array_key_exists(‘page’, $_REQUEST))

? strtolower($_REQUEST[‘page’])

: ‘’;

switch ($page) {

case ‘entry’: $pageclass = ‘Detail’; break;

case ‘edit’: $pageclass = ‘Edit’; break;

case ‘comment’: $pageclass = ‘Comment’; break;

default:

$pageclass = ‘Index’;

}

if (!class_exists($pageclass)) {

require_once ‘pages/’.$pageclass.’.php’;

}

return new $pageclass;

}

}

你可以利用 PHP 的动态加载性质,然后使用实时的运行需求(run-time)来给你要建立的类命名。在这情况下, 根据一个 HTTP 请求叁数就能确定哪个页面被加载。你可以使用迟加载,这样只要当你需要建立新对象时才载入相应的类,不需要你载入所有可能用到的“page”类。在上述例子中就用了 require_once来实现这一点。这个技术对于一个装有PHP加速器的系统来说并不重要,因为包含一个外加的文件使用的时间对它来说可以忽略。 但对于大多数典型的PHP服务器来说,这样做是很有好处的。

要想了解更多的关于迟加载的知识,请看第 11 章-代理模式。

小节

工厂模式是非常简单而且非常有用。如果你已经有很多关于工厂模式的例子代码,你会发现更多的东西。《GoF》这本书就介绍了一些关于构建的模式:AbstractFactory and Builder。 AbstractFactory用来处理一些相关组件,Builder模式则是使建立复杂对象更为容易。

在这章的多数例子里, 参数是通过工厂方法引入的(例如 CrayonBox::getColor(‘红色’);)。《GoF》中则称为“参数化工厂”(parameterized factory),它是PHP网页设计中典型的工厂方法。

你现在已经了解工厂模式了, 它是一种代码中建立新对象的管理技术。 你可以看到工厂模式是可以把复杂对象的建立集中起来,甚至用不同的类代替不同的对象。最后,工厂模式支持OOP技术中的多态也是很重要的。