适配器模式 (Adapter)

未匹配的标注

手握设计模式宝典 - 适配器模式 Adapter

Design-Pattern - Structural - Adapter

  • Title: 《手握设计模式宝典》 之 适配器模式 adapter
  • Tag: Design-PatternStructuralAdapter适配器模式设计模式
  • Author: Tacks
  • Create-Date: 2023-08-09
  • Update-Date: 2023-08-10

大纲

[toc]

0、REF

1、5W1H

1.1 WHAT 什么是适配器模式?

1.1.1 概念

适配器模式 Adapter 是一种 结构型 设计模式。 目的是将一个类的接口通过适配器转化成客户端所期望的另一个接口,类似一种包装 Wrap 的手法,从而使得让两个原本不兼容的类可以一起工作。

适配器 Adapter 内部会持有一个适配者 Adaptee(被适配的类)的实例,通过适配器的方法调用适配者的方法,实现接口的转换和适配,最终得到客户端希望的目标接口 Target

客户端通过统一的接口调用与适配器进行交互,而不需要直接和不兼容的类进行交互。相当于适配器就是个中间人,你不需要关心底层如何实现。

1.1.2 真实世界类比

  • 不同国家的电源插头

不同国家的插头标准不同,无法直接插入其他国家的插座。这时候可以使用一个电源适配器,将原本不兼容的插头转换成目标国家的插头,从而使得设备可以正常连接并工作。

1.2 WHY 为什么有适配器模式?

1.2.1 解决了什么问题

适配器模式主要解决两个问题:

  • 适配:在接口不兼容的情况下,将一个接口转换成客户端所需要的接口,消除接口不兼容的问题
  • 解耦:将适配者 Adaptee 与 客户端目标类 Target 进行解耦, 变化的是适配器,客户端代码无需修改

1.2.2 带来了什么问题

  • 间接性:因为引入了适配器类作为中间层,客户端不再是直接调用具体的实现,而是用适配器提供的统一接口
  • 复杂性:毕竟引入了新的接口和类,会增加代码的复杂度

当然,如果只是一个简单两种不同的实现,甚至可以用 if/else 来进行判定来实现不同的调用。

1.3 WHERE 在什么地方使用适配器模式?

适配器模式可以在开发过程中的任何时候使用,当需要解决接口不匹配的问题时

  • 适配不同格式的数据

  • 适配不同的数据库操作

  • 适配不同的文件上传方式

  • 1.4 WHEN 什么时间使用适配器?

  • 当你希望使用某个类,其接口与其他代码不兼容时,可以使用适配器类

    • 此时适配器充当一个中间层,进行转化接口

1.5 WHO 谁负责调用适配器?

在适配器模式中,客户端负责调用适配器。客户端使用统一的接口与适配器进行交互,而不需要直接与不兼容的类进行交互。

1.6 HOW 如何去实现适配器?

  • 确保至少有两个类的接口不兼容
    • 新的服务:比如现在引入一个第三方组件库, 也就是被适配者 Adatee
    • 当前服务:一个现有的客户端类 Client
  • 定义客户端接口 TargetInterface
  • 适配器类 Adapter
    • 定义属性,用来保存调用新接口对象的引用,可以通过构造函数或普通方法,进行成员变量初始化
    • 实现客户端接口 TargetInterface
    • 实现客户端接口所有方法,通过适配器将其委托给新的服务对象
  • 客户端调用
    • 客户端必须通过适配器,完成对应方法的调用,从而可以不影响客户端代码下进行扩展更多的适配器类。

1.6.1 类适配器

  • 实现
    • 通过类继承的方式
    • 适配器类 Adapter 实现 implements 目标接口 Target ,并同时继承 extends 适配者类 Adaptee ,从而实现接口的转换和适配
  • 使用感受
    • 它通过继承可以重用现有代码,同时也符合新的接口规范
    • 但受限于 PHP 是单继承,如果被适配者有多个,却无法多继承实现适配器
    • 不太常用
class Adaptee
{
    public function specificRequest()
    {
    }
}

interface Target
{
    public function request();
}

class Adapter extends Adaptee implements Target
{
    public function request()
    {
        $this->specificRequest();
    }
}

1.6.2 对象适配器

  • 实现
    • 通过对象组合的方式;在面向对象中,通常来说,组合的耦合度低于继承的,用组合代替继承实现会更好一些。
    • 适配器类 Adapter 持有一个适配者类 Adaptee 的实例,并实现 implements 目标接口 Target 的方法
  • 使用感受
    • 适配器实现目标接口,然后利用组合的方式,将被适配者对象进行封装,即可根据需求随意调用
    • Nice
class Adaptee
{
    public function specificRequest()
    {
    }
}

interface Target
{
    public function request();
}

class Adapter implements Target
{
    private $adaptee;

    public function __construct(Adaptee $adaptee)
    {
        $this->adaptee = $adaptee;
    }

    public function request()
    {
        $this->adaptee->specificRequest();
    }
}

1.6.3 接口适配器

  • 实现
    • Abstract Class Adapter 接口适配器通过在适配器类中提供默认实现
    • 客户端调用的时候,利用 Anonymous Class 匿名类 继承 extends 抽象适配器类,重写目标接口中的方法
    • 客户端,并且实例化匿名类才能实现调用具体的方法
  • 用途
    • 暂时没有见到框架中这么使用的,感觉略微多余,还要匿名类才能实例化调用适配器,不然抽象类的适配器无法直接调用
    • 不太常用
interface Target
{
    public function method1();
    public function method2();
    public function method3();
}

abstract class Adapter implements Target
{
    public function method1()
    {
        // 默认实现
    }

    public function method2()
    {
        // 默认实现
    }

    public function method3()
    {
        // 默认实现
    }
}

class ConcreteAdapter extends Adapter
{
    // 根据需要选择性地重写感兴趣的方法
    public function method1()
    {
        // 重写实现
    }
}

2、Code

2.1 类适配器

举一个手机充电的例子,充电器将 220v 电压转换成 手机需要的 5v 。

2.1.1 目标接口与被适配者之间不能直接使用

/**
 * Adaptee 被适配者
 * 
 * 外部组件库(国家电网提供电源)
 */
class Power220vAdaptee {
    public function output220v() {
        return 220;
    }
}

/**
 * Target 目标接口
 * 
 * 目前业务使用的接口(手机目前只支持 5v)
 */
interface ElectronicDeviceTargetInterface {
    public function use5V();
}

/**
 * 业务使用
 */
class ClientPhone {
    public function charge(ElectronicDeviceTargetInterface $dest) {
        if ($dest->use5V() == 5) {
            echo sprintf("[%s] %s:%s 手机使用到5v电压,准备充电", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__). PHP_EOL ;
        } else {
            echo sprintf("[%s] %s:%s 电压不匹配手机,无法充电", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__). PHP_EOL ;
        }
    }
}

2.1.2 引入类适配器

/**
 * Adapter 编写适配器
 * 
 * 电源对象适配器:将220v转化成设备可用的5v
 */
class Power220vAdapter extends Power220vAdaptee implements ElectronicDeviceTargetInterface {
    public function __construct()
    {
        echo sprintf("[%s] %s:%s 我是电源适配器", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__). PHP_EOL ;
    }

    public function use5V() {
        $src = $this->output220v();
        echo sprintf("[%s] %s:%s 我收到了%s电压", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__, $src). PHP_EOL ;

        $dest= $src / 44;
        echo sprintf("[%s] %s:%s 我转化成%s电压", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__, $dest). PHP_EOL ;

        return $dest;
    }
}

2.1.3 客户端调用

// 适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作


/*
`ClientPhone` 客户端类
本来调用充电的时候需要用 charge(5v),但是现在只有 220v ,于是需要用到适配器

原因:
目前有 Power220vAdaptee::output220v() 
但是 ClientPhone 客户希望用 ElectronicDeviceTargetInterface::use5V() 接口

所以:
实现 Power220vAdapter 适配器,将 Power220vAdaptee::output220v() 转化为客户能用的 ElectronicDeviceTargetInterface::use5V() 

 */
$myPhone = new ClientPhone();


/*
`Power220vAdapter` 对象适配器

通过继承的方式,接收 `Power220vAdaptee` 被适配者,成为了子类,相当于有了 220v 的能力
通过依赖目标接口 `ElectronicDeviceTargetInterface` ,在适配器内实现 `use5v()` 目标方法,将 220v 转化 5v
 */
$myPhone->charge(new Power220vAdapter());

2.2 对象适配器

举一个手机充电的例子,充电器将 220v 电压转换成 手机需要的 5v 。

2.2.1 目标接口与被适配者之间不能直接使用

/**
 * Adaptee 被适配者
 * 
 * 外部组件库(国家电网提供电源)
 */
class Power220vAdaptee
{
    public function output220v() :int {
        return 220;
    }
}


/**
 * Target 目标接口
 * 
 * 目前业务使用的接口(手机目前只支持 5v)
 */
interface ElectronicDeviceTargetInterface {
    public function use5V();
}


/**
 * Client
 * 
 * 业务使用
 */
class ClientPhone {
    /**
     * 充电
     *
     * @param ElectronicDeviceTargetInterface $dest
     * @return void
     */
    public function charge(ElectronicDeviceTargetInterface $dest) {
        if ($dest->use5V() == 5) {
            echo sprintf("[%s] %s:%s 手机使用到5v电压,准备充电", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__). PHP_EOL ;
        } else {
            echo sprintf("[%s] %s:%s 电压不匹配手机,无法充电", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__). PHP_EOL ;
        }
    }
}

2.2.2 引入对象适配器

/**
 * Adapter 编写适配器
 * 
 * 电源对象适配器:将220v转化成设备可用的5v
 */
class Power220vAdapter implements ElectronicDeviceTargetInterface {
    private $powerObj;

    public function __construct(Power220vAdaptee $powerObj)
    {
        echo sprintf("[%s] %s:%s 我是电源适配器", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__). PHP_EOL ;
        $this->powerObj = $powerObj;
    }

    public function use5V() {
        // 保护措施
        if($this->powerObj === null) {
            return 0;
        }

        $src = $this->powerObj->output220v();
        echo sprintf("[%s] %s:%s 我收到了%s电压", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__, $src). PHP_EOL ;

        $dest= $src / 44;
        echo sprintf("[%s] %s:%s 我转化成%s电压", date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__, $dest). PHP_EOL ;

        return $dest;
    }
}

2.2.3 客户端调用

// 适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作


/*
`ClientPhone` 客户端类
本来调用充电的时候需要用 charge(5v),但是现在只有 220v ,于是需要用到适配器

原因:
目前有 Power220vAdaptee::output220v() 
但是 ClientPhone 客户希望用 ElectronicDeviceTargetInterface::use5V() 接口

所以:
实现 Power220vAdapter 适配器,将 Power220vAdaptee::output220v() 转化为客户能用的 ElectronicDeviceTargetInterface::use5V() 

 */
$myPhone = new ClientPhone();


/*
`Power220vAdapter` 对象适配器

通过组合的方式,接收 `Power220vAdaptee` 被适配者,转化为自身的对象属性,相当于有了 220v 的能力
通过依赖目标接口 `ElectronicDeviceTargetInterface` ,在适配器内实现 `use5v()` 目标方法,将 220v 转化 5v
 */
$myPhone->charge(new Power220vAdapter(new Power220vAdaptee()));

2.3 接口适配器

举一个手机充电的例子,充电器将 220v 电压转换成 手机需要的 5v/10v 。

2.3.1 目标接口与被适配者之间不能直接使用

/**
 *  被适配者:当前电压类(国家电网提供电源)
 */
class Power220v
{
    public function output220v() :int {
        return 220;
    }
}


/**
 * 适配接口:移动手机设备接收的电压 (目前手机设备支持的接口)
 * 
 * MobilePhoneDeviceVoltageInterface
 */
interface MobilePhoneDeviceVoltageInterface {
    public function output() : int;

    /**
     * 普通充电
     *
     * @return integer
     */
    public function outputNormal() : int;

    /**
     * 快速充电
     *
     * @return integer
     */
    public function outputFlash() : int;
}


/**
 * 业务使用:手机客户端类
 */
class ClientPhone {
    const VOLTS = [5, 10];
    const NAME = '手机';

    /**
     * 充电
     *
     * @param MobilePhoneDeviceVoltageInterface $dest 接收电源
     * @return void
     */
    public function charge(MobilePhoneDeviceVoltageInterface $dest) {
        $value = $dest->output();
        if (in_array($value, self::VOLTS)) {
            echo sprintf("[%s] %s:%s %s使用到%sV电压,准备充电", 
                date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__, self::NAME, $value). PHP_EOL ;
        } else {
            echo sprintf("[%s] %s:%s %s应该采用%sV电压,但是实际上是%sV,无法充电", 
                date("Y-m-d H:i:s"), __CLASS__ , __FUNCTION__, self::NAME, json_encode(self::VOLTS), $value). PHP_EOL ;
        }
    }
}

2.3.2 引入抽象类适配器

/**
 * 抽象适配器:抽象类 PhoneAbstractAdapter
 * 
 * 让手机适配 220v 的电压
 */
abstract class PhoneAbstractAdapter extends Power220v implements MobilePhoneDeviceVoltageInterface {
    public function outputNormal(): int
    {
        return $this->output220v() / 44;
    }
    public function outputFlash(): int
    {
        return $this->output220v() / 22;
    }
}

2.3.3 客户端调用


// 适配器模式的主要作用是将一个类的接口转换成客户端所期望的另一个接口
$myPhone = new ClientPhone();


/*
在这个示例中,`PhoneAbstractAdapter` 接口适配器充当了适配器的角色
通过依赖`implements` 移动手机设备电压接口 `MobilePhoneDeviceVoltageInterface` ,从而提供 `output()` 方法
通过继承`extends`    国家电网的电源 `Power220v`,从而提供 `output220v()` 方法
抽象适配器 `PhoneAbstractAdapter` ,将 output220v() 转成 output() ,但是是抽象的,需要具体实现
匿名类 `Class` 继承抽象适配器,完成具体的 output() 方法,来选择具体选择什么电压充电
*/
$myPhone->charge(new Class extends PhoneAbstractAdapter {
    public function output(): int
    {
        return $this->outputNormal();
    }
});

$myPhone->charge(new Class extends PhoneAbstractAdapter {
    public function output(): int
    {
        return $this->outputFlash();
    }
});


$myPhone->charge(new Class extends PhoneAbstractAdapter {
    public function output(): int
    {
        return $this->output220v();
    }
});

2.4 适配管理器

举一个手机充电线的例子,通过手机类型创建不同的适配器对象,再调用适配器方法 lightning/typec/usb , 用不同的充电线完成充电

通过多个适配器,适配不同的场景,然后用适配管理器统一调度

2.4.1 不同手机充电线接口定义

// 充电线
interface LineInterface
{
    const WHAT = '手机充电线';
}

// Lightning 充电线接口
interface LightningLineInterface extends LineInterface
{
    function lightning();
}

// Typec 充电线接口
interface TypecLineInterface extends LineInterface
{
    function typec();
}

// Typec USB 充电线接口
interface TypecUsbInterface extends LineInterface
{
    function usb();
}

2.4.2 不同手机充电功率

// Apple 手机类,实现 LightningLineInterface
class Apple implements LightningLineInterface
{
    const MAX_POWER = '29W';

    function lightning()
    {
        echo sprintf("[%s] typec %s...", __CLASS__, self::MAX_POWER). PHP_EOL ;
    }
}

// Huawei 手机类,实现 TypecLineInterface 接口
class Huawei implements TypecLineInterface
{
    const MAX_POWER = '100W';

    function typec()
    {
        echo sprintf("[%s] typec %s...", __CLASS__, self::MAX_POWER). PHP_EOL ;
    }
}

// Xiaomi 手机类,实现 TypecLineInterface 接口
class Xiaomi implements TypecLineInterface
{
    const MAX_POWER = '120W';

    function typec()
    {
        echo sprintf("[%s] typec %s...", __CLASS__, self::MAX_POWER) . PHP_EOL ;
    }
}

// Realme 手机类,实现 TypecLineInterface 接口
class Realme implements TypecLineInterface,LineInterface
{
    const MAX_POWER = '240W';

    function typec()
    {
        echo sprintf("[%s] typec %s...", __CLASS__, self::MAX_POWER). PHP_EOL ;
    }
}

// Nokia 手机类,实现 TypecUsbInterface 接口
class Nokia implements TypecUsbInterface
{
    const MAX_POWER = '2.5W';

    function usb()
    {
        echo sprintf("[%s] usb %s...", __CLASS__, self::MAX_POWER). PHP_EOL ;
    }
}

2.4.3 不同充电线不同的适配器

// 手机充电适配器
interface PhoneChargeAdapterInterface
{
    public function charge($phone);
}

// Lightning 适配器类,实现 PhoneChargeAdapterInterface 适配器接口
class LightningAdapter implements PhoneChargeAdapterInterface
{
    public function charge($phone)
    {
        $phone->lightning();
    }
}

// Typec 适配器类,实现 PhoneChargeAdapterInterface 适配器接口
class TypecAdapter implements PhoneChargeAdapterInterface
{
    public function charge($phone)
    {
        $phone->typec();
    }
}

2.4.4 适配管理器

// 适配管理器
class PhoneChargeManage
{
    // 适配器对象
    private $adapterMap = [];

    /**
     * 解析适配器类型返回具体的适配器对象
     *
     * @param string $driverType
     * @return PhoneChargeAdapterInterface
     */
    public function resolve($driverType)
    {
        $key = strtolower($driverType);
        if (in_array($key, $this->adapterMap)) {
            return $this->adapterMap[$driverType];
        }
        return $this->createAdapter($key);
    }


    /**
     * 创建适配器对象
     *
     * @param string $key
     * @return PhoneChargeAdapterInterface
     * @throws Exception
     */
    public function createAdapter($key)
    {
        $driverMethod = 'create' . ucfirst($key) . 'Driver';
        if (!method_exists($this, $driverMethod)) {
            throw new \Exception("PhoneChargeManage [{$key}]'s Driver is not supported.");
        }
        $this->adapterMap[$key] = $this->$driverMethod();

        return $this->adapterMap[$key];
    }

    /**
     * 获取 Lightning 适配器
     *
     * @return PhoneChargeAdapterInterface
     */
    public function createLightningDriver()
    {
        return new LightningAdapter();
    }

    /**
     * 获取 Typec 适配器
     *
     * @return PhoneChargeAdapterInterface
     */
    public function createTypecDriver()
    {
        return new TypecAdapter();
    }
}

2.4.5 目标接口

// 人
class Human
{
    // 手机充电方法
    function phoneChange(LineInterface $phone, array $config)
    {
        // 获取适配器类型
        $driver  = $this->getDriver($phone, $config);

        // 创建适配器管理器
        $manage = new PhoneChargeManage();

        // 根据适配器类型获取适配器对象
        $adapter = $manage->resolve($driver);

        // 使用适配器对象进行手机充电
        $adapter->charge($phone);
    }

    // 获取适配器类型
    public function getDriver(LineInterface $phone, array $config)
    {
        foreach ($config as $keyDriver => $lineItem) {
            if (in_array(get_class($phone), $lineItem)) {
                return $keyDriver;
            }
        }
        return 'default';
    }
}

2.4.6 客户端调用

// 配置信息
$config  = [
    'lightning' => [
        Apple::class,
    ],
    'typec' => [
        Huawei::class,
        Xiaomi::class,
    ],
    'usb' => [
        Nokia::class,
    ],
];

try {
    $tacks = new Human();

    $tacks->phoneChange(new Apple(), $config);

    $tacks->phoneChange(new Huawei(), $config);

    $tacks->phoneChange(new Nokia(), $config);

} catch(\Exception $e) {
    echo sprintf("[Error] message:%s...", $e->getMessage()). PHP_EOL ;
}

3、Application

3.1 Laravel 的 Filesystem 体现出适配器模式

主要从 Laravel 中看 Filesystem 文件管理系统的大致实现。

3.1.1 配置文件入手

// .env 配置
FILESYSTEM_DRIVER=local
// config\filesystems.php 文件系统配置
return [
    'default' => env('FILESYSTEM_DRIVER', 'local'),
    'disks' => [
        // 本地磁盘
        'local' => [
            'driver' => 'local',               // 指定磁盘的驱动程序为local,表示使用本地文件系统进行文件存储。
            'root'   => storage_path('app'),   // 存储目录为 storage/app 目录
        ],
        // 公开磁盘
        'public' => [
            'driver'     => 'local',                      // 指定磁盘的驱动程序为local,表示使用本地文件系统进行文件存储。
            'root'       => storage_path('app/public'),   // 存储目录为 storage/app/public 目录
            'url'        => env('APP_URL').'/storage',    // 可以通过 URL 访问
            'visibility' => 'public',                     // 文件的可见性为public
        ],
        // S3 存储
        's3' => [
            'driver'   => 's3',
            'key'      => env('AWS_ACCESS_KEY_ID'),
            'secret'   => env('AWS_SECRET_ACCESS_KEY'),
            'region'   => env('AWS_DEFAULT_REGION'),
            'bucket'   => env('AWS_BUCKET'),
            'url'      => env('AWS_URL'),
            'endpoint' => env('AWS_ENDPOINT'),
            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
        ],
    ],
    ...
];

3.1.2 对外统一调用方法

  • 不同驱动器,仅需要更改配置,无需修改客户端调用方法
// 生成文件 storage\app\example.txt
\Illuminate\Support\Facades\Storage::disk('local')->put('example.txt', 'Hello World!');


// 生成文件
\Illuminate\Support\Facades\Storage::disk('public')->put('test.txt', 'test');
// 创建符号链接  public/storage => storage/app/public
// php artisan storage:link
// 于是可以通过 http://localhost/storage/test.txt => public/storage => storage/app/public/test.txt

插播一下, 门面模式的使用

  • Facades 门面快捷操作 Storage
    • 所有的门面操作都需要继承抽象类 Facade (vendor\laravel\framework\src\Illuminate\Support\Facades\Facade.php)
    • 抽象类 Facade 中有魔术方法 __callStatic() 来承接所有的门面方法调用
    • 魔术方法 __callStatic() 通过调用 static::getFacadeRoot(); 来获取对应具体的实例 $instance
    • 然后调用具体示例的方法 $instance->method(...$args);
    • 例如: Storage::disk('local') 的操作实际上调用的是 (new \Illuminate\Filesystem\FilesystemManager)->disk('local')
// vendor\laravel\framework\src\Illuminate\Support\Facades\Storage.php
// 表示门面将访问服务容器中绑定的 filesystem 的实例
class Storage extends Facade
{
    // 返回门面对应的服务容器中的键名(即服务名)
    protected static function getFacadeAccessor()
    {
        return 'filesystem';
    }
}

插播一下,如何自动加载这些容器里面的对应实例,主要是在入口文件会加载服务提供者

  • bootstrap\app.php 框架启动加载服务提供者
    • app 中 registerConfiguredProviders() 函数,负责自动加载配置文件 config\app.php 中配置的服务提供者数组 providers
    • 这些服务提供者中就包含 Illuminate\Filesystem\FilesystemServiceProvider::class
// bootstrap\app.php
// 实例化应用
$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

插播一下,服务提供者 Provider 的体现,用于注册和配置文件系统相关的服务和组件

FilesystemServiceProvider 负责将 FilesystemManager 类绑定到 Laravel 的服务容器中,从而方便在容器的其他位置直接使用 实例化的对象

  • Illuminate\Filesystem\FilesystemServiceProvider Filesystem 服务提供者
    • Provider 服务提供者是将服务(Service)和服务容器(Service Container)联系起来的桥梁
    • Provider 它们负责将服务绑定到容器中,以便在整个应用程序中进行访问和使用,所以框架中不会直观的看到 new 一个实例
// vendor\laravel\framework\src\Illuminate\Filesystem\FilesystemServiceProvider.php
namespace Illuminate\Filesystem;

use Illuminate\Support\ServiceProvider;

class FilesystemServiceProvider extends ServiceProvider
{
    // 注册服务提供者
    public function register()
    {
        $this->registerNativeFilesystem();

        $this->registerFlysystem();
    }

    // 注册本地文件系统
    protected function registerNativeFilesystem()
    {
        // 采用了单例模式
        $this->app->singleton('files', function () {
            return new Filesystem;
        });
    }

    // 注册基于驱动程序的文件系统
    protected function registerFlysystem()
    {
        $this->registerManager();

        $this->app->singleton('filesystem.disk', function ($app) {
            return $app['filesystem']->disk($this->getDefaultDriver());
        });

        $this->app->singleton('filesystem.cloud', function ($app) {
            return $app['filesystem']->disk($this->getCloudDriver());
        });
    }

    // 注册文件管理器
    protected function registerManager()
    {
        $this->app->singleton('filesystem', function ($app) {
            return new FilesystemManager($app);
        });
    }

    // 默认驱动
    protected function getDefaultDriver()
    {
        return $this->app['config']['filesystems.default'];
    }

    // 云驱动
    protected function getCloudDriver()
    {
        return $this->app['config']['filesystems.cloud'];
    }
}

3.1.3 文件系统适配管理器类

  • Illuminate\Filesystem\FilesystemManager 文件系统管理器类
    • 提供文件系统操作的统一入口
    • 每一个适配器都会对应一个 Manager 后缀的管理器, 例如 FilesystemManager 对应 filesystem
    • (new FilesystemManager)->disk('local') 返回具体的 Filesystem 实例
// vendor\laravel\framework\src\Illuminate\Filesystem\FilesystemManager.php
class FilesystemManager implements FactoryContract
{
    ...
    // 根据不同的驱动器,返回文件系统实例
    // \Illuminate\Contracts\Filesystem\Filesystem
    public function disk($name = null)
    {
        // 读取默认配置 filesystems.default
        $name = $name ?: $this->getDefaultDriver();

        return $this->disks[$name] = $this->get($name);
    }

    // 获取实例
    protected function get($name)
    {
        return $this->disks[$name] ?? $this->resolve($name);
    }

    // 根据配置加载对应的配置
    protected function resolve($name, $config = null)
    {
        $config = $config ?? $this->getConfig($name);

        if (empty($config['driver'])) {
            throw new InvalidArgumentException("Disk [{$name}] does not have a configured driver.");
        }

        $name = $config['driver'];

        // 自定义
        if (isset($this->customCreators[$name])) {
            return $this->callCustomCreator($config);
        }

        // 系统设定的 例如 createLocalDriver() createS3Driver()
        $driverMethod = 'create'.ucfirst($name).'Driver';

        if (! method_exists($this, $driverMethod)) {
            throw new InvalidArgumentException("Driver [{$name}] is not supported.");
        }

        return $this->{$driverMethod}($config);
    }

    // 创建本地驱动器
    // \Illuminate\Contracts\Filesystem\Filesystem
    public function createLocalDriver(array $config)
    {
        $permissions = $config['permissions'] ?? [];
        $links = ($config['links'] ?? null) === 'skip'
            ? LocalAdapter::SKIP_LINKS
            : LocalAdapter::DISALLOW_LINKS;

        return $this->adapt($this->createFlysystem(new LocalAdapter(
            $config['root'], $config['lock'] ?? LOCK_EX, $links, $permissions
        ), $config));
    }


    // 创建S3驱动器
    //  \Illuminate\Contracts\Filesystem\Cloud
    public function createS3Driver(array $config)
    {
        $s3Config = $this->formatS3Config($config);
        $root = $s3Config['root'] ?? null;
        $options = $config['options'] ?? [];
        $streamReads = $config['stream_reads'] ?? false;

        return $this->adapt($this->createFlysystem(
            new S3Adapter(new S3Client($s3Config), $s3Config['bucket'], $root, $options, $streamReads), $config
        ));
    }

    /**
     * 适配器(将 \League\Flysystem\FilesystemInterface 转化成 \Illuminate\Contracts\Filesystem\Filesystem)
     *
     * @param  \League\Flysystem\FilesystemInterface  $filesystem
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     */
    protected function adapt(FilesystemInterface $filesystem)
    {
        return new FilesystemAdapter($filesystem);
    }

    ...
}

3.1.4 委托给不同的适配器进行具体实现

  • Illuminate\Contracts\Filesystem\Filesystem Filesystem 抽象接口约束
    • 文件系统操作的方法契约,例如定义 put() 接口,写入文件内容
    • 抽象文件系统操作,提供一致文件系统操作接口,不受具体的文件系统细节限制,比如 local, s3
namespace Illuminate\Contracts\Filesystem;

interface Filesystem
{
    ...
    /**
     * Write the contents of a file.
     *
     * @param  string  $path
     * @param  string|resource  $contents
     * @param  mixed  $options
     * @return bool
     */
    public function put($path, $contents, $options = []);
    ...
}
  • Illuminate\Filesystem\FilesystemAdapter Filesystem 适配器
    • 适配器实现 Illuminate\Contracts\Filesystem\Filesystem Filesystem 接口契约
    • FilesystemAdapter 可以适配不同的文件系统驱动器,例如 本地,AwsS3,等
    • FilesystemAdapter 针对访问不同的存储系统,提供一致的API操作
    • FilesystemAdapter 充当适配器 ,解决连接 Laravel 框架与底层文件系统驱动程序之间的交互,将文件操作委托给底层文件系统驱动程序
// vendor\laravel\framework\src\Illuminate\Filesystem\FilesystemAdapter.php
namespace Illuminate\Filesystem;

use Illuminate\Contracts\Filesystem\Filesystem;
use League\Flysystem\FilesystemInterface;

class FilesystemAdapter implements Filesystem {

    /**
     * The Flysystem filesystem implementation.
     *
     * @var \League\Flysystem\FilesystemInterface
     */
    protected $driver;


     /**
     * Create a new filesystem adapter instance.
     *
     * @param  \League\Flysystem\FilesystemInterface  $driver
     * @return void
     */
    public function __construct(FilesystemInterface $driver)
    {
        $this->driver = $driver;
    }


    /**
     * Write the contents of a file.
     *
     * @param  string  $path
     * @param  \Psr\Http\Message\StreamInterface|\Illuminate\Http\File|\Illuminate\Http\UploadedFile|string|resource  $contents
     * @param  mixed  $options
     * @return bool
     */
    public function put($path, $contents, $options = [])
    {
        $options = is_string($options)
                     ? ['visibility' => $options]
                     : (array) $options;

        // 如果是
        if ($contents instanceof File ||
            $contents instanceof UploadedFile) {
            return $this->putFile($path, $contents, $options);
        }

        if ($contents instanceof StreamInterface) {
            return $this->driver->putStream($path, $contents->detach(), $options);
        }

        return is_resource($contents)
                ? $this->driver->putStream($path, $contents, $options)
                : $this->driver->put($path, $contents, $options);
    }
}
  • league/flysystem 组件, 负责具体文件系统的的实现
    • 抽象
      • League\Flysystem\AdapterInterface 接口,对应 Laravel 文件系统的接口 \Illuminate\Contracts\Filesystem\Filesystem
      • League\Flysystem\Adapter\AbstractAdapter 抽象类,实现接口 League\Flysystem\AdapterInterface
    • 不同驱动器的实现
      • LocalAdapter 继承抽象类 League\Flysystem\Adapter\AbstractAdapter , 完成里面具体的文件操作
      • AwsS3Adapter 继承抽象类 League\Flysystem\Adapter\AbstractAdapter , 里面还会在调用 S3 Client 的组件,完成具体的文件操作
// vendor\league\flysystem\src\Filesystem.php
namespace League\Flysystem;

use League\Flysystem\AdapterInterface;

class Filesystem implements FilesystemInterface
{
    /**
     * @var AdapterInterface
     */
    protected $adapter;

    /**
     * Constructor.
     *
     * @param AdapterInterface $adapter
     * @param Config|array     $config
     */
    public function __construct(AdapterInterface $adapter, $config = null)
    {
        $this->adapter = $adapter;
        $this->setConfig($config);
    }

    /**
     * Get the Adapter.
     *
     * @return AdapterInterface adapter
     */
    public function getAdapter()
    {
        return $this->adapter;
    }

    /**
     * @inheritdoc
     */
    public function put($path, $contents, array $config = [])
    {
        $path = Util::normalizePath($path);
        $config = $this->prepareConfig($config);

        if ( ! $this->getAdapter() instanceof CanOverwriteFiles && $this->has($path)) {
            return (bool) $this->getAdapter()->update($path, $contents, $config);
        }

        return (bool) $this->getAdapter()->write($path, $contents, $config);
    }

    /**
     * @inheritdoc
     */
    public function putStream($path, $resource, array $config = [])
    {
        if ( ! is_resource($resource) || get_resource_type($resource) !== 'stream') {
            throw new InvalidArgumentException(__METHOD__ . ' expects argument #2 to be a valid resource.');
        }

        $path = Util::normalizePath($path);
        $config = $this->prepareConfig($config);
        Util::rewindStream($resource);

        if ( ! $this->getAdapter() instanceof CanOverwriteFiles && $this->has($path)) {
            return (bool) $this->getAdapter()->updateStream($path, $resource, $config);
        }

        return (bool) $this->getAdapter()->writeStream($path, $resource, $config);
    }
}
  • LocalAdapter
// vendor\league\flysystem\src\Adapter\Local.php
class Local extends AbstractAdapter
{

    /**
     * Constructor.
     *
     * @param string $root
     * @param int    $writeFlags
     * @param int    $linkHandling
     * @param array  $permissions
     *
     * @throws LogicException
     */
    public function __construct($root, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS, array $permissions = [])
    {
        $root = is_link($root) ? realpath($root) : $root;
        $this->permissionMap = array_replace_recursive(static::$permissions, $permissions);
        $this->ensureDirectory($root);

        if ( ! is_dir($root) || ! is_readable($root)) {
            throw new LogicException('The root path ' . $root . ' is not readable.');
        }

        $this->setPathPrefix($root);
        $this->writeFlags = $writeFlags;
        $this->linkHandling = $linkHandling;
    }

    /**
     * @inheritdoc
     */
    public function write($path, $contents, Config $config)
    {
        $location = $this->applyPathPrefix($path);
        $this->ensureDirectory(dirname($location));

        if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
            return false;
        }

        $type = 'file';
        $result = compact('contents', 'type', 'size', 'path');

        if ($visibility = $config->get('visibility')) {
            $result['visibility'] = $visibility;
            $this->setVisibility($path, $visibility);
        }

        return $result;
    }

      /**
     * @inheritdoc
     */
    public function update($path, $contents, Config $config)
    {
        $location = $this->applyPathPrefix($path);
        $size = file_put_contents($location, $contents, $this->writeFlags);

        if ($size === false) {
            return false;
        }

        $type = 'file';

        $result = compact('type', 'path', 'size', 'contents');

        if ($visibility = $config->get('visibility')) {
            $this->setVisibility($path, $visibility);
            $result['visibility'] = $visibility;
        }

        return $result;
    }

}
  • AwsS3Adapter
// league/flysystem-aws-s3-v3 ~1.0 组件
// vendor\league\flysystem-aws-s3-v3\src\AwsS3Adapter.php
namespace League\Flysystem\AwsS3v3;

use Aws\S3\S3ClientInterface;

class AwsS3Adapter extends AbstractAdapter {
    public function __construct(S3ClientInterface $client, $bucket, $prefix = '', array $options = [], $streamReads = true)
    {
        $this->s3Client = $client;
        $this->bucket = $bucket;
        $this->setPathPrefix($prefix);
        $this->options = $options;
        $this->streamReads = $streamReads;
    }
     /**
     * Write a new file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config Config object
     *
     * @return false|array false on failure file meta data on success
     */
    public function write($path, $contents, Config $config)
    {
        return $this->upload($path, $contents, $config);
    }

    /**
     * Update a file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config Config object
     *
     * @return false|array false on failure file meta data on success
     */
    public function update($path, $contents, Config $config)
    {
        return $this->upload($path, $contents, $config);
    }
    ...
}

3.2 其他组件中关于适配器模式的体现

通常有一些共性,就是通过配置文件,设置对应的驱动器配置,以及默认的驱动器,然后通过适配管理器加载配置创建出对应的适配器,但是对用户来说都是统一的调用,仅仅是配置不同。

这里只是根据自己理解,从 Laravel 中看出来的设计模式,不一定完整。

4、 Summary

适配器模式,用于解决两个接口不兼容的问题 ,从而让接口不兼容的对象可以协同工作。

  • 为什么不兼容,因为有一个老的接口,现在提供了一个第三方的接口,那你是否客户端要进行变化?
    • 当然要变化,最简单的就是 if/else ,但是如果后续又要适配新的接口,那是不是又多了一个。
  • 如何使用适配器模式?
    • 通过引入一个适配器类,既满足现有客户端调用,又能复用另一个组件的方法,这就是既要又要。
  • 适配器只能解决两个接口不兼容的问题?
    • 一个适配器可以解决两个接口,那么再引入一个接口,就需要有一个另一个适配器。
  • 多个适配器如何管理?
    • 通过引入 Manager ,负责加载驱动,具体什么配置加载什么适配器。
  • 用适配器的好处怎么体现?
    • 复用:适配器只需要当一个中间人,转化或者说包装一下,把不能用的接口,还给用起来。
    • 解耦:客户端跟适配器交互,不需要跟具体的服务对象对接了。
    • 扩展性增加。
    • 符合开闭原则,客户端的调用无需改动,就能增加新的适配器。
  • 用适配器的不足如何体现?
    • 复杂度增高,直观来看,代码量就会增加不少。
  • 什么地方用适配器?
    • 集成不同的系统
    • 复用已有的代码

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~