当前位置: 首页 > 图文教程 > 网络编程 > PHP > PHP系列教程:设计模式介绍Ⅲ工厂模式

PHP
php 多线程上下文中安全写文件实现代码
PHP类的使用 实例代码讲解
用php实现让页面只能被百度gogole蜘蛛访问的方法
php 学习笔记
PHP编程过程中需要了解的this,self,parent的区别
php 操作excel文件的方法小结
使用PHP获取网络文件的实现代码
PHP 巧用数组降低程序的时间复杂度
php下将XML转换为数组
php 文件上传代码(限制jpg文件)
php 无极分类(递归)实现代码
PHP 采集获取指定网址的内容
PHP 将图片按创建时间进行分类存储的实现代码
PHP 存储文本换行实现方法
PHP 批量更新网页内容实现代码
用PHP查询搜索引擎排名位置的代码
用php实现的获取网页中的图片并保存到本地的代码
php实现首页链接查询 友情链接检查的代码
处理php自动反斜杠的函数代码
php实现的遍历文件夹下所有文件,编辑删除

PHP系列教程:设计模式介绍Ⅲ工厂模式


出处:互联网   整理: 软晨网(RuanChen.com)   发布: 2009-11-03   浏览: 314 ::
收藏到网摘: n/a

《PHP设计模式介绍》第三章 工厂模式

在面向对象编程中, 最通常的方法是一个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 类图:

例子:增加一些颜色

让我们更深入工厂模式吧。继续如前,先建立一个能为本章节的其它部分持续举例说明的简单类。 这是一个输出十六进制的HTML RGB Color类,包括了R, G, 和 B三个属性(在构造对象的时候引入)和 getRgb()方法,getRgb()的作用是返回一个十六进制颜色的字符串。

和以前一样,我们按照测试驱动开发(TDD)的方法:写一个测试,再写一段代码满足该测试,如果需要,反复下去。

下面是一个非常简单的起始测试:

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, “
.”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, “
.”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, “
.”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, “
.”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
$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);

最后, 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技术中的多态也是很重要的。

下一节:《PHP设计模式介绍》第四章 单条模式