程序员阿沛
发布于 2026-06-27 / 0 阅读
0
0

面向对象和设计模式十六适配器模式让不匹配的类完美牵手的神奇魔法

面向对象和设计模式(十六)适配器模式:让不匹配的类完美牵手的神奇魔法

适配器模式是一种可以将不具有某些接口的类或者具有某些接口但需要对其功能做一些修饰和扩展的类转为具有目标接口或功能的类的一种设计模式。

最常见的场景就是一个业务类A依赖具有B方法的对象。类C具有业务类A所需要依赖的功能,但由于类C没有B方法,类A就无法使用类C。

class A{  // A依赖$obj的formatData()方法    public function run($obj){        //...        $obj->formatData($data);        //...    }}
class C{    // C没有formatData()方法,但是有format方法    public $data;    public function format(){        // ... 做一些对 $this->data 格式化的工作    }}

这种情况下,A如果要使用C的功能就需要一个适配器将 C 的 format()方法转成 formatData()方法。

class CAdaptor extends C{    // CAdaptor作为C的适配器    public function formatData($data){        $this->data = $data;
        // 做一些修饰或者扩展的工作,也可以不做,要看具体需求        return $this->format();        }}

02 适配器模式的实现方式

适配器模式的类图如下:

适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

1. 类适配器(继承)

<?php  // 类适配器: 基于继承  interface ITarget {    public function f1();    public function f2();    public function fc();  }    class Adaptee {    public function fa() { /*...*/ }    public function fb() { /*...*/ }    public function fc() { /*...*/ }  }    class Adaptor extends Adaptee implements ITarget {    public function f1() {      parent::fa();    }        public function f2() {      //...重新实现f2()...    }        // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点  }

可以看到,在适配之前 Adaptee 类是不满足 ITarget 接口的。适配了之后,Adaptor类就能够满足ITarget接口了。

2. 对象适配器(组合)

<?php    // 对象适配器:基于组合  interface ITarget {    public function f1();    public function f2();    public function fc();  }    class Adaptee {    public function fa() { /*...*/ }    public function fb() { /*...*/ }    public function fc() { /*...*/ }  }    class Adaptor implements ITarget {    private $adaptee;        public function __construct(Adaptee $adaptee) {        $this->adaptee = $adaptee;    }        public function f1() {      $this->adaptee->fa(); //委托给Adaptee    }        public function f2() {      //...重新实现f2()...    }        public function fc() {      $this->adaptee->fc();    }  }

什么情况下使用继承方式的适配器,什么情况使用组合方式的适配器呢?

继承方式和组合方式的区别在于,组合方式的适配器必须重新实现原始类的所有方法,而继承方式则可以复用父类的部分代码,无需所有方法都重新实现(例如上述代码中的fc())。

因此判断使用继承还是组合的标准主要有两个,一个是 Adaptee 类的接口个数(即要适配的方法个数),另一个是 Adaptee 和 ITarget
的契合程度(两者之间的已适配的方法个数)。

如果 Adaptee 接口并不多,那两种实现方式都可以。

如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那我们推荐使用类适配器(继承),因为 Adaptor
作为子类复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量(编码的工作量)要少很多。

如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。

03 适配器模式的使用场景

首先声明,适配器模式本质是一种“补偿模式”用来补救设计上的缺陷。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。

适配器模式的应用场景是“接口不兼容”,而接口不兼容大致来说包含以下场景:

1、依赖的外部类在接口设计方面有缺陷

所谓的缺陷具体是指,如包含大量的静态方法(而目标接口规定的是对象方法),方法命名不规范、方法的参数过多、方法的性能有问题等。

<?php

class CD { //这个类来自外部sdk,我们无权修改它的代码 // 静态方法,而ITarget要的是非静态方法 public static function staticFunction1() { // } // 函数名不合适 public function uglyNamingFunction2() { // } // 方法过多参数 public function tooManyParamsFunction3($paramA, $paramB, …) { // } // 方法性能有问题 public function lowPerformanceFunction4() { // } } // 使用适配器模式进行重构 interface ITarget { public function function1(); public function function2(); public function fucntion3(ParamsWrapperDefinition $paramsWrapper); public function function4(); //… }
// 注意:适配器类的命名不一定非得末尾带Adaptor class CDAdaptor extends CD implements ITarget { //… public function function1() { parent::staticFunction1(); } public function function2() { parent::uglyNamingFucntion2(); } public function function3(ParamsWrapperDefinition $paramsWrapper) { parent::tooManyParamsFunction3($paramsWrapper->getParamA(), …); } public function function4() { //…reimplement it… } }

2、多个类具有同一类型的功能,但是方法名各异,需要适配器将方法名进行统一

假设我们要对用户输入的文本内容做敏感词过滤,我们引入了多款第三方敏感词过滤系统,依次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。

但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们上层调用就可以通过适配器的接口用到不同的敏感词过滤类。

未使用适配器:

<?phpclass ASensitiveWordsFilter { // A敏感词过滤系统提供的接口  //text是原始文本,函数输出用***替换敏感词之后的文本  public function filterSexyWords($text) {    // ...  }    public function filterPoliticalWords($text) {    // ...  } }
class BSensitiveWordsFilter  { // B敏感词过滤系统提供的接口  public function filter($text) {    //...  }}
class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口  public function filter($text, $mask) {    //...  }}
// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好class RiskManagement {  private $aFilter;  private $bFilter = new BSensitiveWordsFilter();  private $cFilter = new CSensitiveWordsFilter();    public function __construct(){      $this->aFilter = new ASensitiveWordsFilter();      $this->bFilter = new BSensitiveWordsFilter();      $this->cFilter = new CSensitiveWordsFilter();  }
  public function filterSensitiveWords($text) {    $maskedText = $this->aFilter.filterSexyWords(text);    $maskedText = $this->aFilter.filterPoliticalWords($maskedText);    $maskedText = $this->bFilter.filter($maskedText);    $maskedText = $this->cFilter.filter($maskedText, "***");    return $maskedText;  }}

使用了适配器后:

// 使用适配器模式进行改造interface ISensitiveWordsFilter { // 统一接口定义  public function filter($text);}
// A过滤器的适配器class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {  private $aFilter;  public function filter($text) {    $maskedText = $this->aFilter->filterSexyWords($text);    $maskedText = $this->aFilter->filterPoliticalWords($maskedText);    return $maskedText;  }}//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...
// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。class RiskManagement {   private $filters = [];   public function addSensitiveWordsFilter(ISensitiveWordsFilter $filter) {    $this->filters[] = $filter;  }    public function filterSensitiveWords($text) {    $maskedText = $text;    foreach($this->filter as $filter) {      $maskedText = $filter->filter($maskedText);    }    return $maskedText;  }}

3、将外部依赖从一个类替换为另一个类

当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。

<?php

interface IA { //… function fa(); } // 外部系统A class A implements IA { //… public function fa() { // } }
// 在我们的项目中,外部系统A的使用示例 class Demo { private $a; public function __construct(IA $a) { $this->a = $a; } //… } $demo = new Demo(new A()); // 将外部系统A替换成外部系统B class BAdaptor implemnts IA { private $b; public function __construct(B $b) { $this->b= $b; } public function fa() { //… $this->b->fb(); } }
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动, // 只需要将BAdaptor如下注入到Demo即可。 $demo = new Demo(new BAdaptor(new B()));

最后需要提一点:
适配器模式是一个无法偷懒的模式,如果你有10个原始类要适配某个接口,且适配的逻辑各不相同,你不得不写10个适配器类(无论你使用继承还是组合方式的适配器)。

代理、桥接、装饰器、适配器,这 4 种模式的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

尽管代码结构相似,但这 4 种设计模式要解决的问题、应用场景不同,这也是它们的主要区别。

代理模式:代理模式在不改变原始类接口的条件下,给原始类增加和原始类业务无关的功能,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式:桥接模式的目的是将抽象部分(框架部分)和具体实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。


评论