第十二章 装饰器模式
若你从事过面向对象的php开发,即使很短的时间或者仅仅通过本书了解了一些,你会知道,你可以 通过继承改变或者增加一个类的功能,这是所有面向对象语言的一个基本特性。如果已经存在的一个php类缺少某些方法,或者须要给方法添加更多的功能(魅力),你也许会仅仅继承这个类来产生一个新类—这建立在额外的代码上。
但是产生子类并不总是可能或是合适的。如果 你希望改变一个已经初始化的对象的行为,你怎么办?或者,你希望继承许多类的行为,改怎么办?前一个,只能在于运行时完成,后者显然时可能的,但是可能会导致产生大量的不同的类—可怕的事情。
问题
你如何组织你的代码使其可以容易的添加基本的或者一些很少用到的 特性,而不是直接不额外的代码写在你的类的内部?
解决方案
装饰器模式提供了改变子类的灵活方案。装饰器模式允许你在不引起子类数量爆炸的情况下动态的修饰对象,添加特性。
当用于一组子类时,装饰器模式更加有用。如果你拥有一族子类(从一个父类派生而来),你需要在与子类独立使用情况下添加额外的特性,你可以使用装饰器模式,以避免代码重复和具体子类数量的增加。看看以下例子,你可以更好的理解这种观点。考虑一个建立在组件概念上的“form”表单库,在那里你需要为每一个你想要表现的表单控制类型建立一个类。这种类图可以如下所示:
Select and TextInput类是组件类的子类。假如你想要增加一个“labeled”带标签的组件—一个输入表单告诉你要输入的内容。因为任何一个表单都可能需要被标记,你可能会象这样继承每一个具体的组件:
<form action=”formpage.php” method=”post”>
<b>First Name:</b> <input type=”text” name=”fname” value=””><br>
<b>Last Name:</b> <input type=”text” name=”lname” value=””><br>
<b>Email:</b> <input type=”text” name=”email” value=””><br>
<input type=”submit” value=”Submit”>
</form>
增加一些css样式后,表单渲染出来如下图所示:
class Widget {
function paint() {
return $this->_asHtml();
}
}
让我们以一个基本的text输入组件开始。它(组件)必须要包含输入区域的名字(name)而且输入内容可以以HTML的方式渲染。
class TextInput extends Widget {
var $name;
var $value;
function TextInput($name, $value=’’) {
$this->name = $name;
$this->value = $value;
}
function _asHtml() {
return ‘<input type=”text” name=”’.$this->name.’” value=”’
.$this->value.’”>’;
}
}
一个基本的测试可以验证HTML代码是否正确——作为参数传入给构造函数的名字,值(内容)是否传递到渲染后的输出中:
class WidgetTestCase extends UnitTestCase {
function testTextInput() {
$text =& new TextInput(‘foo’, ‘bar’);
$output = $text->paint();
$this->assertWantedPattern(
‘~^<input type=”text”[^>]*>$~i’, $output);
$this->assertWantedPattern(‘~name=”foo”~i’, $output);
$this->assertWantedPattern(‘~value=”bar”~i’, $output);
}
}
TextInput组件工作正常,但是它的用户接口非常糟糕,它缺少友好的描述,如“First Name” 或者 “Email Address.” 。因此,下一个增加到组件类的合理的特性就是一个描述。我们进入有能够统一增加(一些特性)能力的装饰器模式。
作为开始,我们建立一个普通的可以被扩展产生具体的特定装饰器的WidgetDecorator类。至少WidgetDecorator类应该能够在它的构造函数中接受一个组件,并复制公共方法paint()。
class WidgetDecorator {
var $widget;
function WidgetDecorator(&$widget) {
$this->widget =& $widget;
}
function paint() {
return $this->widget->paint();
}
}
为建立一个标签(lable),需要传入lable的内容,以及原始的组件:
class Labeled extends WidgetDecorator {
var $label;
function Labeled($label, &$widget) {
$this->label = $label;
$this->WidgetDecorator($widget);
}
}
有标签的组件也需要复制paint()方法,并将标签信息增加到输出中:
class Labeled extends WidgetDecorator {
var $label;
function Labeled($label, &$widget) {
$this->label = $label;
$this->WidgetDecorator($widget);
}
function paint() {
return ‘<b>’.$this->label.’:</b> ‘.$this->widget->paint();
}
}
你可以用一个测试检验它:
class WidgetTestCase extends UnitTestCase {
function testLabeled() {
$text =& new Labeled(
‘Email’
,new TextInput(‘email’));
$output = $text->paint();
208 The Decorator Pattern
$this->assertWantedPattern(‘~^<b>Email:</b> <input~i’, $output);
}
}
我们已经看到TextInput和Labeled类的能力,你可以装配一个类整体来管理表单(form)。
FormHandler类有一个静态的build()方法从表单的各种元素创建一个部件的数组。
class FormHandlerTestCase extends UnitTestCase {
function testBuild() {
$this->assertIsA($form = FormHandler::build(new Post), ‘Array’);
$this->assertEqual(3, count($form));
$this->assertIsA($form[1], ‘Labeled’);
$this->assertWantedPattern(‘~email~i’, $form[2]->paint());
}
}
实现FormHandler 的代码:
class FormHandler {
function build() {
return array(
new Labeled(‘First Name’, new TextInput(‘fname’))
,new Labeled(‘Last Name’, new TextInput(‘lname’))
,new Labeled(‘Email’, new TextInput(‘email’))
);
}
}
现在,这段代码并不能工作—没有通过$_post提交的数据。因为这段代码必须要使用一个MockObject对象 (参见第6章)测试,现在我们可以将$_post数据包装在一个类似哈希的对象中—与Registry(参见第五章)类似,或者模仿WACT的DataSource从Specification pattern
class Post {
var $store = array();
function get($key) {
if (array_key_exists($key, $this->store))
return $this->store[$key];
}
function set($key, $val) {
$this->store[$key] = $val;
}
}
想更方便的话,你可以使用Factory模式或者自动填充的方法来从$_POST里面提取关键字。
class Post {
// ...
function &autoFill() {
$ret =& new Post;
foreach($_POST as $key => $value) {
$ret->set($key, $value);
}
return $ret;
}
}
使用这个Post类,你可以编辑你的FormHandler::build() 方法,默认使用已经存在的$_post数据:
class FormHandler {
function build(&$post) {
return array(
new Labeled(‘First Name’
, new TextInput(‘fname’, $post->get(‘fname’)))
,new Labeled(‘Last Name’
, new TextInput(‘lname’, $post->get(‘lname’)))
,new Labeled(‘Email’
, new TextInput(‘email’, $post->get(‘email’)))
);
}
}
现在你可以创建一个php脚本使用FormHandler类来产生HTML表单:
<form action=”formpage.php” method=”post”>
<?php
$post =& Post::autoFill();
$form = FormHandler::build($post);
foreach($form as $widget) {
echo $widget->paint(), “<br>\n”;
}
?>
<input type=”submit” value=”Submit”>
</form>
现在,你已经拥有了一个提交给它自身并且能保持posted数据的表单处理(form handler) 类。
现在。我们继续为表单添加一些验证机制。方法是编辑另一个组件装饰器类来表达一个“invalid”状态并扩展FormHandler类增加一个validate()方法以处理组件示例数组。如果组件非法(“invalid”),我们通过一个“invalid”类将它包装在<span>元素中。这里是一个证明这个目标的测试
class WidgetTestCase extends UnitTestCase {
// ...
function testInvalid() {
$text =& new Invalid(new TextInput(‘email’));
$output = $text->paint();
$this->assertWantedPattern(
‘~^<span class=”invalid”><input[^>]+></span>$~i’, $output);
}
}
这里是Invalid WidgetDecorator子类:
//代码Here’s the Invalid WidgetDecorator subclass:
class Invalid extends WidgetDecorator {
function paint() {
return ‘<span class=”invalid”>’.$this->widget->paint().’</span>’;
}
}
装饰器的一个有点是你可以将他们串在一起(使用)。Invalid装饰器仅仅知道:它正在包装一个组件:它不必关心组件是否是一个TextInput, Select,或者是一个有标签的被装饰版本的组件 。
这导致了下一个合理的测试用例:
class WidgetTestCase extends UnitTestCase {
// ...
function testInvalidLabeled() {
$text =& new Invalid(
new Labeled(
‘Email’
,new TextInput(‘email’)));
$output = $text->paint();
$this->assertWantedPattern(‘~<b>Email:</b> <input~i’, $output);
$this->assertWantedPattern(
‘~^<span class=”invalid”>.*</span>$~i’, $output);
}
}
有了Invalid装饰器,我们来处理FormHandler::validate() 方法:
class FormHandlerTestCase extends UnitTestCase {
// ...
function testValidateMissingName() {
$post =& new Post;
$post->set(‘fname’, ‘Jason’);
$post->set(‘email’, ‘jsweat_php@yahoo.com’);
$form = FormHandler::build($post);
$this->assertFalse(FormHandler::validate($form, $post));
$this->assertNoUnwantedPattern(‘/invalid/i’, $form[0]->paint());
$this->assertWantedPattern(‘/invalid/i’, $form[1]->paint());
$this->assertNoUnwantedPattern(‘/invalid/i’, $form[2]->paint());
}
}
这个测试捕获(包含)了所有的基本方面:建立一个Post实例的存根,使用它建立一个组件集合,然后将集合传送给validate方法。
class FormHandler {
function validate(&$form, &$post) {
// first name required
if (!strlen($post->get(‘fname’))) {
$form[0] =& new Invalid($form[0]);}
// last name required
if (!strlen($post->get(‘lname’))) {
$form[1] =& new Invalid($form[1]);
}
}
}
不协调的代码
当我看这段代码时,我发现了两个不协调之处:通过数字索引访问表单元素,需要传递$_post数组。给validation方法。在以后的重构中,最好是创建一个组件集合用一个以表单元素名字索引的关联数组表示或者用一个Registry模式作为更合理的一步。你也可以给类Widget增加一个方法返回它的
当前数值,取消需要传递$_Post实例给Widget集合的构造函数。所有这些都超出了这个例子目的的范围。
为了验证目的,我们继续增加一个简单的 正则方法(regex)来验证email地址:
class FormHandlerTestCase extends UnitTestCase {
// ...
function testValidateBadEmail() {
$post =& new Post;
$post->set(‘fname’, ‘Jason’);
$post->set(‘lname’, ‘Sweat’);
$post->set(‘email’, ‘jsweat_php AT yahoo DOT com’);
$form = FormHandler::build($post);
$this->assertFalse(FormHandler::validate($form, $post));
$this->assertNoUnwantedPattern(‘/invalid/i’, $form[0]->paint());
$this->assertNoUnwantedPattern(‘/invalid/i’, $form[1]->paint());
$this->assertWantedPattern(‘/invalid/i’, $form[2]->paint());
}
}
实现这个简单的email验证的代码如下:
//代码
class FormHandler {
function validate(&$form, &$post) {
// first name required
if (!strlen($post->get(‘fname’))) {
$form[0] =& new Invalid($form[0]);}
// last name required
if (!strlen($post->get(‘lname’))) {
$form[1] =& new Invalid($form[1]);
}
// email has to look real
if (!preg_match(‘~\w+@(\w+\.)+\w+~’
,$post->get(‘email’))) {
$form[2] =& new Invalid($form[2]);
}
}
}
你也可以创建一个测试用例以验证form表单何时有效://代码
class FormHandlerTestCase extends UnitTestCase {
// ...
function testValidate() {
$post =& new Post;
$post->set(‘fname’, ‘Jason’);
$post->set(‘lname’, ‘Sweat’);
$post->set(‘email’, ‘jsweat_php@yahoo.com’);
$form = FormHandler::build($post);
$this->assertTrue(FormHandler::validate($form, $post));
$this->assertNoUnwantedPattern(‘/invalid/i’, $form[0]->paint());
$this->assertNoUnwantedPattern(‘/invalid/i’, $form[1]->paint());
$this->assertNoUnwantedPattern(‘/invalid/i’, $form[2]->paint());
}
}
这又提出了在本方法内追踪任何验证失败的需求,因此它可以返回true如果所有的都合格。
//代码
class FormHandler {
// ...
function validate(&$form, &$post) {
$valid = true;
// first name required
if (!strlen($post->get(‘fname’))) {
$form[0] =& new Invalid($form[0]);
$valid = false;
}
// last name required
if (!strlen($post->get(‘lname’))) {
$form[1] =& new Invalid($form[1]);
$valid = false;}
// email has to look real
if (!preg_match(‘~\w+@(\w+\.)+\w+~’
,$post->get(‘email’))) {
$form[2] =& new Invalid($form[2]);
$valid = false;
}
return $valid;
}
}
那些就是所有需要为页面添加验证的building blocks 。这里是本游戏(章)结尾的一个截图。以及产生它的页面代码:
//代码
<html>
<head>
<title>Decorator Example</title>
<style type=”text/css”>
.invalid {color: red; }
.invalid input { background-color: red; color: yellow; }
#myform input { position: absolute; left: 110px; width: 250px; font-weight: bold;}
</style>
</head>
<body>
<form action=”<?php echo $_SERVER[‘PHP_SELF’]; ?>” method=”post”>
<div id=”myform”>
<?php error_reporting(E_ALL);
require_once ‘widgets.inc.php’;
$post =& Post::autoFill();
$form = FormHandler::build($post);
if ($_POST) { FormHandler::validate($form, $post);
}
foreach($form as $widget) {
echo $widget->paint(), “<br>\n”;
}
?>
</div>
<input type=”submit” value=”Submit”>
</form>
</body>
</html>
总结
装饰器模式是对你产生影响的那些模式中的另一个,当你使用他们工作一段时间以后。装饰器模式允许你可以简单的通过严格的继承问题。你可以这样认为装饰器:在运行时可以有效地改变对象的类或者甚至多次—当你在你的脚本不同的场合使用这个类。
也许装饰器模式最重要的一个方面是它的超过继承的能力。“问题”部分展现了一个使用继承的子类爆炸。基于装饰器模式的解决方案,UML类图展现了这个简洁灵活的解决方案。