《PHP设计模式介绍》第十三章 适配器模式

接口的改变,是一个需要程序员们必须(虽然很不情愿)接受和处理的普遍问题。程序提供者们修改他们的代码;系统库被修正;各种程序语言以及相关库的发展和进化。我孩子的无数玩具中有一个简要地描述了这个两难局面:你无法合理安排一个不得其所的人。

问题

你如何避免因外部库的API改变而带来的不便?假如你写了一个库,你能否提供一种方法允许你软件的现有用户进行完美地升级,即使你已经改变了你的API?为了更好地适宜于你的需要,你应该如何改变一个对象的接口?

解决方案

适配器(Adapter)模式为对象提供了一种完全不同的接口。你可以运用适配器(Adapter)来实现一个不同的类的常见接口,同时避免了因升级和拆解客户代码所引起的纠纷。

考虑一下当(不是假设!)一个第三方库的API改变将会发生什么。过去你只能是咬紧牙关修改所有的客户代码,而情况往往还不那么简单。你可能正从事一项新的项目,它要用到新版本的库所带来的特性,但你已经拥有许多旧的应用程序,并且它们与以前旧版本的库交互运行地很好。你将无法证明这些新特性的利用价值,如果这次升级意味着将要涉及到其它应用程序的客户代码。

注:控制体模式
适配器(Adapter)模式是控制体模式的最新范例。一个适配器(Adapter)的结构类似于代理服务器 (Proxy)和修饰器(Decorator),而它们的不同之处在于,适配器(Adapter)的目的是改变封装类的接口,代理服务器(Proxy)和修饰器(Decorator)则是保持接口不变。



样本代码

让我们看看当API改变时,如何保护应用程序不受影响。

假设你费尽心思寻找合适的库,最后终于找到了HwLib,一个(假设的)被设计用来发送信息的代码集。

下面是HwLib类的源代码:

  1. // PHP4
  2. /**
  3. * the HwLib helps programmers everywhere write their first program
  4. * @package HelloWorld
  5. * @version 1
  6. */
  7. class HwLib {
  8. /**
  9. * Say “Hello”
  10. * @deprec this function is going away in the future
  11. * @return string
  12. */
  13. function hello() {
  14. return ‘Hello ‘;
  15. }
  16. /**
  17. * target audience
  18. * @return string
  19. */
  20. function world() {
  21. return ‘World!’;
  22. }
  23. }
复制代码

下面是库运行的范例:

  1. $hw =& new HwLib;
  2. echo $hw->hello(), $hw->world();
复制代码

HwLib有完备的说明文档。在文档中作者已经明确指出hello()方法会在未来的版本中不被支持(甚至淘汰)。
接下来,现在假设第二版的HwLib已经发布。一个全新的greet()方法代替了hello()。
下面是这个库的新版本(注释已被抽取掉):

  1. // version 2
  2. class HwLib {
  3. function greet() {
  4. return ‘Greetings and Salutations ‘;
  5. }
  6. function world() {
  7. return ‘World!’;
  8. }
  9. }
复制代码

为了适应HwLib的不同版本进行编码,先进行一些基于第一版本HwLib接口的测试:

  1. class AdapterTestCase extends UnitTestCase {
  2. function TestOriginalApp() {
  3. $lib =& new HwLib;
  4. $this->assertEqual(
  5. ‘Hello World!’
  6. ,$lib->hello().$lib->world());
  7. }
  8. }
复制代码

你同样可以表明,对这个库的简单升级将造成此应用程序的失效。

  1. class AdapterTestCase extends UnitTestCase {
  2. The Adapter Pattern 221
  3. function TestOriginalAppWouldFail() {
  4. $lib =& new HwLib; // now using HwLib version 2
  5. $this->assertFalse(method_exists($lib, ‘hello’));
  6. }
  7. }
复制代码

(这个测试以method_exists()为例证。如果你简单地更换这个库的第二版本并且以TestOriginalApp()的测试再次运行AdapterTestCase,PHP就会运行失败,同时报告“致命错误:未定义的函数:hello()”)
针对API“升级”的解决办法就是创建一个适配器(Adapter)。
第一步是获得第二版本HwLib的实例的一个引用,并且把它加入到你的Adapter类中。

  1. class HwLibV2ToV1Adapter {
  2. var $libv2;
  3. function HwLibV2ToV1Adapter (&$libv2) {
  4. $this->libv2 =& $libv2;
  5. }
  6. }
复制代码

这个范例展示了将这个实例传递给构造函数的过程,你也可以运用Factory 或 Singleton ,或者其它适合你要求的创建模式,来创建一个新的实例。(通过前两章,你应该对HwLibV2ToV1Adapter的编写用途很熟悉了)

当得到第二版本HwLib的时候,你如何使它在第一版本HwLib的实例中体现?

  1. class HwLibV2ToV1Adapter {
  2. var $libv2;
  3. function HwLibV2ToV1Adapter (&$libv2) {
  4. $this->libv2 =& $libv2;
  5. }
  6. function hello() {
  7. return $this->libv2->greet();
  8. }
  9. function world() {
  10. return $this->libv2->world();
  11. }
  12. }
复制代码

HwLibV2ToV1Adapter::hello()方法代表了$libv2对象的greet()方法。
接着,你该如何在程序中使用它?

  1. class AdapterTestCase extends UnitTestCase {
  2. function TestOriginalAppWithAdapter() {
  3. $lib =& new HwLibV2ToV1Adapter(new HwLib);
  4. $this->assertEqual(
  5. ‘Greetings and Salutations World!’
  6. ,$lib->hello().$lib->world());
  7. }
  8. }
复制代码

刚才的程序测试和现在的程序代码都有一定程度的脆弱性。有没有方法在长久的使用中让它们都更易于维护呢?当然是有的!

回忆一下(第三章)Factory是如何提供一个更灵活的方法来创建类的实例的。为了更好地在将来检验这些代码,就从一个简单的Factory函数开始:

  1. function &HwLibInstance() {
  2. return new HwLib;
  3. }
复制代码

为了测试Factory,直接调用它而不创建它的实例:

  1. class AdapterTestCase extends UnitTestCase {
  2. function TestAppWithFactory() {
  3. $lib =& HwLibInstance();
  4. $this->assertWantedPattern(
  5. ‘/\w+ World!$/’
  6. ,$lib->hello().$lib->world());
  7. }
  8. }
复制代码

有两个方面需要注意:Factory创建了对象,而用于确认的assertEqual()函数被修改为更灵活的assertWantedPattern()。你现在可以用一个正则表达式来捕获你在库中所要查找的“核心”,但可能会使这个测试本身变得脆弱。
接下来,升级HwLib库。当安装了HwLib第二版,你就可以修改HwLibInstance()函数来适应新的版本。

  1. function &HwLibInstance($ver=false) {
  2. switch ($ver) {
  3. case ‘V2’:
  4. return new HwLib;
  5. default:
  6. return new HwLibV2ToV1Adapter(new HwLib);
  7. }
  8. }
复制代码

现在重新运行AdapterTestCase。测试通过!(绿色进度条正常。)因为原始的程序没有传递一个参数,HwLibInstance会默认返回封装在HwLibV2toV1Adapter中的HwLib的一个实例。尽管如此,如果你编写了新的代码,你可以传递进一个“V2”的参数让这个函数自动选择HwLib的新版本而不用去调整它。
以后,如果你选择升级HwLib的第三版,应该将Factory做如下的调整:

  1. function &HwLibInstance($ver=false) {
  2. switch ($ver) {
  3. case ‘V3’:
  4. return new HwLib;
  5. case ‘V2’:
  6. return new HwLibV3ToV2Adapter(new HwLib);
  7. default:
  8. return new HwLibV2ToV1Adapter(
  9. new HwLibV3ToV2Adapter(new HwLib));
  10. }
  11. }
复制代码

总结

如例中代码所示,你可以运用适配器(Adapter)模式来避免因外部库改变所带来的不便——倘若向上兼容。作为某个库的开发者,你应该独立编写适配器,使你的用户更简便地使用新版本的库,而不用去修改他们现有的全部代码。

GoF书中提出的适配器(Adapter)模式更倾向于运用继承而不是组成。这在强类型语言中是有利的,因为适配器(Adapter)事实上是一个目标类的子类,因而能更好地与类中方法相结合。

下面是HwLib适配器运用继承的范例:

  1. class HwLibGofAdapter extends HwLib { // extending version 2.0
  2. function hello() {
  3. return parent::greet();
  4. }
  5. }
复制代码

world()方法没有在类中提到,因为运用了继承,它已经是子类的一部分。

  1. class AdapterTestCase extends UnitTestCase {
  2. function TestHwLibGofAdapter() {
  3. $lib =& new HwLibGofAdapter;
  4. $this->assertEqual(
  5. ‘Greetings and Salutations World!’
  6. ,$lib->hello().$lib->world());
  7. }
  8. }
复制代码

为了更好的灵活性,我个人比较倾向于组成的方法(特别是在结合了依赖性倒置的情况下);尽管如此,继承的方法提供两种版本的接口,或许在你的实际运用中反而是一个提高灵活性的关键。

注:依赖性倒置原理

依赖性倒置原理(首先在 http://www.objectmentor.com/resources/articles/dip.pdf中由Robert C. Martin提出)是一个面向对象编程的准则,它表明:高层次的模块不应该依赖于低层次的模块,而应依赖于抽取。一个简单的与适配器(Adapter)模式相结合的依赖性倒置原理范例可以在以下地址中找到: http://www.phplondon.org/wiki/DependencyInversion

适配器模式的重点是改变一个单独类的API。有一个与之相关的设计模式(本书中没有涵盖),称作正面(Facade)模式。正面 (Facade)的目的是给由许多对象构成的整个子系统,提供更为简洁的接口——反过来就是封装一个单独类——可能是一个值得研究的模式,如果你正设法把你的代码与第三方库隔离开来的话。

你可能感兴趣的:(适配器模式)