《PHP设计模式介绍》第十二章 装饰器模式

若你从事过面向对象的php开发,即使很短的时间或者仅仅通过本书了解了一些,你会知道,你可以通过继承改变或者增加一个类的功能,这是所有面向对象语言的一个基本特性。如果已经存在的一个php类缺少某些方法,或者须要给方法添加更多的功能(魅力),你也许会仅仅继承这个类来产生一个新类—这建立在额外的代码上。

但是产生子类并不总是可能或是合适的。如果 你希望改变一个已经初始化的对象的行为,你怎么办?或者,你希望继承许多类的行为,改怎么办?前一个,只能在于运行时完成,后者显然时可能的,但是可能会导致产生大量的不同的类—可怕的事情。

问题

你如何组织你的代码使其可以容易的添加基本的或者一些很少用到的 特性,而不是直接不额外的代码写在你的类的内部?

解决方案

装饰器模式提供了改变子类的灵活方案。装饰器模式允许你在不引起子类数量爆炸的情况下动态的修饰对象,添加特性。

当用于一组子类时,装饰器模式更加有用。如果你拥有一族子类(从一个父类派生而来),你需要在与子类独立使用情况下添加额外的特性,你可以使用装饰器模式,以避免代码重复和具体子类数量的增加。看看以下例子,你可以更好的理解这种观点。考虑一个建立在组件概念上的 “form”表单库,在那里你需要为每一个你想要表现的表单控制类型建立一个类。这种类图可以如下所示:

Select and TextInput类是组件类的子类。假如你想要增加一个“labeled”带标签的组件—一个输入表单告诉你要输入的内容。因为任何一个表单都可能需要被标记,你可能会象这样继承每一个具体的组件:
01.gif

2009-4-21 17:39 上传
下载附件 (2.52 KB)
 



上面的类图看起来并不怎么坏,下面让我们再增加一些特性。表单验证阶段,你希望能够指出一个表单控制是否合法。你为非法控制使用的代码又一次继承其它组件,因此又需要产生大量的子类:
02.gif

2009-4-21 17:39 上传
下载附件 (4.43 KB)
 



这个类看起来并不是太坏,所以让我们增加一些新的功能。在结构有效性确认中你需要指出结构是否是有效的。你需要让你检验有效性的代码也可以应用到其它部件,这样不用再更多的子类上进行有效性验证。
03.gif

2009-4-21 17:39 上传
下载附件 (13.65 KB)
 



这里子类溢出并不是唯一的问题。想一想那些重复的代码,你需要重新设计你的整个类层次。有没有更好的方法!确实,装饰器模式是避免这种情况的好方法。

装饰器模式结构上类似与代理模式(参见第2章)。一个装饰器对象保留有对对象的引用,而且忠实的重新建立被装饰对象的公共接口。装饰器也可以增加方法,扩展被装饰对象的接口,任意重载方法,甚至可以在脚本执行期间有条件的重载方法。

为了探究装饰器模式,让我们以前面讨论过的表单组件库为例,并且用装饰器模式而不是继承,实现“lable”和“invalidation”两个特性。

样本代码:

组件库包含哪些特性?

  • 容易创建表单元素
  • 将表单元素以html方式输出
  • 在每个元素上实现简单的验证


本例中,我们创建一个包含姓,名,邮件地址,输入项的表单。所有的区域都是必须的,而且E-mail必须看起来是有效的E—mail地址。用HTML语言表示,表单的代码象下面所示:

  1. <form action=”formpage.php” method=”post”>
  2. <b>First Name:</b> <input type=”text” name=”fname” value=””><br>
  3. <b>Last Name:</b> <input type=”text” name=”lname” value=””><br>
  4. <b>Email:</b> <input type=”text” name=”email” value=””><br>
  5. <input type=”submit” value=”Submit”>
  6. </form>
复制代码

增加一些css样式后,表单渲染出来如下图所示:
04.gif

2009-4-21 17:39 上传
下载附件 (8.29 KB)
 



为建立统一的API,我们创建一个基本的组件类(如果这是php5的例子,这或许会使用接口)。既然所有的组件(表单元素)都必须渲染一些输出,组建类可以仅仅只有一个paint()方法。

  1. class Widget {
  2. function paint() {
  3. return $this->_asHtml();
  4. }
  5. }
复制代码

让我们以一个基本的text输入组件开始。它(组件)必须要包含输入区域的名字(name)而且输入内容可以以HTML的方式渲染。

  1. class TextInput extends Widget {
  2. var $name;
  3. var $value;
  4. function TextInput($name, $value=’’) {
  5. $this->name = $name;
  6. $this->value = $value;
  7. }
  8. function _asHtml() {
  9. return ‘<input type=”text” name=”’.$this->name.’” value=”’
  10. .$this->value.’”>’;
  11. }
  12. }
复制代码

一个基本的测试可以验证HTML代码是否正确——作为参数传入给构造函数的名字,值(内容)是否传递到渲染后的输出中:

  1. class WidgetTestCase extends UnitTestCase {
  2. function testTextInput() {
  3. $text =& new TextInput(‘foo’, ‘bar’);
  4. $output = $text->paint();
  5. $this->assertWantedPattern(
  6. ‘~^<input type=”text”[^>]*>$~i’, $output);
  7. $this->assertWantedPattern(‘~name=”foo”~i’, $output);
  8. $this->assertWantedPattern(‘~value=”bar”~i’, $output);
  9. }
  10. }
复制代码

TextInput组件工作正常,但是它的用户接口非常糟糕,它缺少友好的描述,如“First Name” 或者 “Email Address.” 。因此,下一个增加到组件类的合理的特性就是一个描述。我们进入有能够统一增加(一些特性)能力的装饰器模式。

作为开始,我们建立一个普通的可以被扩展产生具体的特定装饰器的WidgetDecorator类。至少WidgetDecorator类应该能够在它的构造函数中接受一个组件,并复制公共方法paint()。

  1. class WidgetDecorator {
  2. var $widget;
  3. The Decorator Pattern 207
  4. function WidgetDecorator(&$widget) {
  5. $this->widget =& $widget;
  6. }
  7. function paint() {
  8. return $this->widget->paint();
  9. }
  10. }
复制代码

为建立一个标签(lable),需要传入lable的内容,以及原始的组件:

  1. class Labeled extends WidgetDecorator {
  2. var $label;
  3. function Labeled($label, &$widget) {
  4. $this->label = $label;
  5. $this->WidgetDecorator($widget);
  6. }
  7. }
复制代码

有标签的组件也需要复制paint()方法,并将标签信息增加到输出中:

  1. class Labeled extends WidgetDecorator {
  2. var $label;
  3. function Labeled($label, &$widget) {
  4. $this->label = $label;
  5. $this->WidgetDecorator($widget);
  6. }
  7. function paint() {
  8. return ‘<b>’.$this->label.’:</b> ‘.$this->widget->paint();
  9. }
  10. }
复制代码

你可以用一个测试检验它:

  1. class WidgetTestCase extends UnitTestCase {
  2. function testLabeled() {
  3. $text =& new Labeled(
  4. ‘Email’
  5. ,new TextInput(‘email’));
  6. $output = $text->paint();
  7. 208 The Decorator Pattern
  8. $this->assertWantedPattern(‘~^<b>Email:</b> <input~i’, $output);
  9. }
  10. }
复制代码

我们已经看到TextInput和Labeled类的能力,你可以装配一个类整体来管理表单(form)。
FormHandler类有一个静态的build()方法从表单的各种元素创建一个部件的数组。

  1. class FormHandlerTestCase extends UnitTestCase {
  2. function testBuild() {
  3. $this->assertIsA($form = FormHandler::build(new Post), ‘Array’);
  4. $this->assertEqual(3, count($form));
  5. $this->assertIsA($form[1], ‘Labeled’);
  6. $this->assertWantedPattern(‘~email~i’, $form[2]->paint());
  7. }
  8. }
复制代码

实现FormHandler 的代码:

  1. class FormHandler {
  2. function build() {
  3. return array(
  4. new Labeled(‘First Name’, new TextInput(‘fname’))
  5. ,new Labeled(‘Last Name’, new TextInput(‘lname’))
  6. ,new Labeled(‘Email’, new TextInput(‘email’))
  7. );
  8. }
  9. }
复制代码

现在,这段代码并不能工作—没有通过$_post提交的数据。因为这段代码必须要使用一个MockObject对象 (参见第6章)测试,现在我们可以将$_post数据包装在一个类似哈希的对象中—与Registry(参见第五章)类似,或者模仿WACT的 DataSource从Specification pattern

  1. class Post {
  2. var $store = array();
  3. function get($key) {
  4. if (array_key_exists($key, $this->store))
  5. return $this->store[$key];
  6. The Decorator Pattern 209
  7. }
  8. function set($key, $val) {
  9. $this->store[$key] = $val;
  10. }
  11. }
复制代码

想更方便的话,你可以使用Factory模式或者自动填充的方法来从$_POST里面提取关键字。

  1. class Post {
  2. // ...
  3. function &autoFill() {
  4. $ret =& new Post;
  5. foreach($_POST as $key => $value) {
  6. $ret->set($key, $value);
  7. }
  8. return $ret;
  9. }
  10. }
复制代码

使用这个Post类,你可以编辑你的FormHandler::build() 方法,默认使用已经存在的$_post数据:

  1. class FormHandler {
  2. function build(&$post) {
  3. return array(
  4. new Labeled(‘First Name’
  5. , new TextInput(‘fname’, $post->get(‘fname’)))
  6. ,new Labeled(‘Last Name’
  7. , new TextInput(‘lname’, $post->get(‘lname’)))
  8. ,new Labeled(‘Email’
  9. , new TextInput(‘email’, $post->get(‘email’)))
  10. );
  11. }
  12. }
复制代码

现在你可以创建一个php脚本使用FormHandler类来产生HTML表单:

  1. <form action=”formpage.php” method=”post”>
  2. <?php
  3. 210 The Decorator Pattern
  4. $post =& Post::autoFill();
  5. $form = FormHandler::build($post);
  6. foreach($form as $widget) {
  7. echo $widget->paint(), “<br>\n”;
  8. }
  9. ?>
  10. <input type=”submit” value=”Submit”>
  11. </form>
复制代码

现在,你已经拥有了一个提交给它自身并且能保持posted数据的表单处理(form handler) 类。

现在。我们继续为表单添加一些验证机制。方法是编辑另一个组件装饰器类来表达一个“invalid”状态并扩展FormHandler类增加一个 validate()方法以处理组件示例数组。如果组件非法(“invalid”),我们通过一个“invalid”类将它包装在<span>元素中。这里是一个证明这个目标的测试

  1. class WidgetTestCase extends UnitTestCase {
  2. // ...
  3. function testInvalid() {
  4. $text =& new Invalid(
  5. new TextInput(‘email’));
  6. $output = $text->paint();
  7. $this->assertWantedPattern(
  8. ‘~^<span class=”invalid”><input[^>]+></span>$~i’, $output);
  9. }
  10. }
复制代码

这里是Invalid WidgetDecorator子类:

  1. //代码Here’s the Invalid WidgetDecorator subclass:
  2. class Invalid extends WidgetDecorator {
  3. function paint() {
  4. return ‘<span class=”invalid”>’.$this->widget->paint().’</span>’;
  5. }
  6. }
复制代码

装饰器的一个优点是你可以将他们串在一起(使用)。Invalid装饰器仅仅知道:它正在包装一个组件:它不必关心组件是否是一个TextInput, Select,或者是一个有标签的被装饰版本的组件 。

这导致了下一个合理的测试用例:

  1. class WidgetTestCase extends UnitTestCase {
  2. // ...
  3. function testInvalidLabeled() {
  4. $text =& new Invalid(
  5. new Labeled(
  6. ‘Email’
  7. ,new TextInput(‘email’)));
  8. $output = $text->paint();
  9. $this->assertWantedPattern(‘~<b>Email:</b> <input~i’, $output);
  10. $this->assertWantedPattern(
  11. ‘~^<span class=”invalid”>.*</span>$~i’, $output);
  12. }
  13. }
复制代码

有了Invalid装饰器,我们来处理FormHandler::validate() 方法:

  1. class FormHandlerTestCase extends UnitTestCase {
  2. // ...
  3. function testValidateMissingName() {
  4. $post =& new Post;
  5. $post->set(‘fname’, ‘Jason’);
  6. $post->set(‘email’, ‘[email protected]’);
  7. $form = FormHandler::build($post);
  8. $this->assertFalse(FormHandler::validate($form, $post));
  9. $this->assertNoUnwantedPattern(‘/invalid/i’, $form[0]->paint());
  10. $this->assertWantedPattern(‘/invalid/i’, $form[1]->paint());
  11. $this->assertNoUnwantedPattern(‘/invalid/i’, $form[2]->paint());
  12. }
  13. }
复制代码

这个测试捕获(包含)了所有的基本方面:建立一个Post实例的存根,使用它建立一个组件集合,然后将集合传送给validate方法。

  1. class FormHandler {
  2. function validate(&$form, &$post) {
  3. // first name required
  4. if (!strlen($post->get(‘fname’))) {
  5. $form[0] =& new Invalid($form[0]);
  6. }
  7. 212 The Decorator Pattern
  8. // last name required
  9. if (!strlen($post->get(‘lname’))) {
  10. $form[1] =& new Invalid($form[1]);
  11. }
  12. }
  13. }
复制代码

不协调的代码

当我看这段代码时,我发现了两个不协调之处:通过数字索引访问表单元素,需要传递$_post数组。给validation方法。在以后的重构中,最好是创建一个组件集合用一个以表单元素名字索引的关联数组表示或者用一个Registry模式作为更合理的一步。你也可以给类Widget增加一个方法返回它的

当前数值,取消需要传递$_Post实例给Widget集合的构造函数。所有这些都超出了这个例子目的的范围。

为了验证目的,我们继续增加一个简单的 正则方法(regex)来验证email地址:

  1. class FormHandlerTestCase extends UnitTestCase {
  2. // ...
  3. function testValidateBadEmail() {
  4. $post =& new Post;
  5. $post->set(‘fname’, ‘Jason’);
  6. $post->set(‘lname’, ‘Sweat’);
  7. $post->set(‘email’, ‘jsweat_php AT yahoo DOT com’);
  8. $form = FormHandler::build($post);
  9. $this->assertFalse(FormHandler::validate($form, $post));
  10. $this->assertNoUnwantedPattern(‘/invalid/i’, $form[0]->paint());
  11. $this->assertNoUnwantedPattern(‘/invalid/i’, $form[1]->paint());
  12. $this->assertWantedPattern(‘/invalid/i’, $form[2]->paint());
  13. }
  14. }
复制代码

实现这个简单的email验证的代码如下:

  1. class FormHandler {
  2. function validate(&$form, &$post) {
  3. // first name required
  4. if (!strlen($post->get(‘fname’))) {
  5. $form[0] =& new Invalid($form[0]);
  6. }
  7. // last name required
  8. if (!strlen($post->get(‘lname’))) {
  9. $form[1] =& new Invalid($form[1]);
  10. }
  11. // email has to look real
  12. if (!preg_match(‘~\w+@(\w+\.)+\w+~’
  13. ,$post->get(‘email’))) {
  14. $form[2] =& new Invalid($form[2]);
  15. }
  16. }
  17. }
复制代码

你也可以创建一个测试用例以验证form表单何时有效:

  1. class FormHandlerTestCase extends UnitTestCase {
  2. // ...
  3. function testValidate() {
  4. $post =& new Post;
  5. $post->set(‘fname’, ‘Jason’);
  6. $post->set(‘lname’, ‘Sweat’);
  7. $post->set(‘email’, ‘[email protected]’);
  8. $form = FormHandler::build($post);
  9. $this->assertTrue(FormHandler::validate($form, $post));
  10. $this->assertNoUnwantedPattern(‘/invalid/i’, $form[0]->paint());
  11. $this->assertNoUnwantedPattern(‘/invalid/i’, $form[1]->paint());
  12. $this->assertNoUnwantedPattern(‘/invalid/i’, $form[2]->paint());
  13. }
  14. }
复制代码

这又提出了在本方法内追踪任何验证失败的需求,因此它可以返回true如果所有的都合格。

  1. class FormHandler {
  2. // ...
  3. function validate(&$form, &$post) {
  4. $valid = true;
  5. // first name required
  6. if (!strlen($post->get(‘fname’))) {
  7. $form[0] =& new Invalid($form[0]);
  8. $valid = false;
  9. }
  10. // last name required
  11. if (!strlen($post->get(‘lname’))) {
  12. $form[1] =& new Invalid($form[1]);
  13. $valid = false;
  14. }
  15. // email has to look real
  16. if (!preg_match(‘~\w+@(\w+\.)+\w+~’
  17. ,$post->get(‘email’))) {
  18. $form[2] =& new Invalid($form[2]);
  19. $valid = false;
  20. }
  21. return $valid;
  22. }
  23. }
复制代码

那些就是所有需要为页面添加验证的building blocks 。这里是本游戏(章)结尾的一个截图。

05.gif

2009-4-21 17:39 上传
下载附件 (5.95 KB)
 



以及产生它的页面代码:

  1. <html>
  2. <head>
  3. <title>Decorator Example</title>
  4. <style type=”text/css”>
  5. .invalid {color: red; }
  6. .invalid input { background-color: red; color: yellow; }
  7. #myform input { position: absolute; left: 110px; width: 250px; font-weight: bold;}
  8. </style>
  9. </head>
  10. <body>
  11. <form action=”<?php echo $_SERVER[‘PHP_SELF’]; ?>” method=”post”>
  12. <div id=”myform”>
  13. <?php
  14. error_reporting(E_ALL);
  15. require_once ‘widgets.inc.php’;
  16. $post =& Post::autoFill();
  17. $form = FormHandler::build($post);
  18. if ($_POST) {
  19. FormHandler::validate($form, $post);
  20. }
  21. foreach($form as $widget) {
  22. echo $widget->paint(), “<br>\n”;
  23. The Decorator Pattern 215
  24. }
  25. ?>
  26. </div>
  27. <input type=”submit” value=”Submit”>
  28. </form>
  29. </body>
  30. </html>
复制代码

总结

装饰器模式是对你产生影响的那些模式中的另一个,当你使用他们工作一段时间以后。装饰器模式允许你可以简单的通过严格的继承问题。你可以这样认为装饰器:在运行时可以有效地改变对象的类或者甚至多次—当你在你的脚本不同的场合使用这个类。

也许装饰器模式最重要的一个方面是它的超过继承的能力。“问题”部分展现了一个使用继承的子类爆炸。基于装饰器模式的解决方案,UML类图展现了这个简洁灵活的解决方案。

06.gif

2009-4-21 17:39 上传
下载附件 (10.31 KB)
 

你可能感兴趣的:(装饰器模式)