設計模式之:適配器模式

WBOY
發布: 2016-07-30 13:29:25
原創
785 人瀏覽過

      適配器很容易理解, 大多數人家庭都有手機適配器, 用來為移動電話充電,這就是一種適配器. 如果只有USB接頭, 就無法將移動電話插到標準插座上. 實際上, 必須使用一個轉接器, 一端接USB插頭, 一端接插座. 當然, 你可以拿出電氣工具,改裝USB連接頭, 或者重新安裝插座, 不過這樣會帶來很多額外的工作, 而且可能會把連接頭或插座弄壞. 所以, 最可取的方法就是找一個適配器. 軟體開發也是如此.

類適配器模式(使用繼承)

      類適配器相匹配器模式很簡單, 不過與物件配器相比模式的彈性較弱, 類別適配器簡單的原因在於, 適配器(Adapter)會從被適配器(Adaptee)繼承功能, 所以適配模式中需要編寫的程式碼比較少.

      由於類別適配器模式包含雙重介面繼承, 但是PHP並不支援雙重繼承, 不過幸運的是,PHP可以用介面來模擬雙重繼承, 下面是一個正確的結構, 不僅繼承了一個類別, 同時還繼承了一個介面

class ChildClass extends ParentClass implements ISomeAdapter
{
    
}
登入後複製

實現類適配器模式時, 參與者必須包括一個PHP接口

下面以一個貨幣兌換為例來演示:

      假設有一個企業網站在同時銷售軟體服務和軟體產品, 目前交易在美國進行, 所以完全可以用美元來完成所有計算.現在開發人員希望能有一個轉換器能處理美元和歐元的兌換, 而不改變原來按美元交易額的類.通過增加一個適配器, 現在程序即可以用美元計算也可以用歐元計算.

DollarCalc.php

<?php
class DollarCalc
{
    private $dollar;
    private $product;
    private $service;
    public $rate = 1;
    public function requestCalc($productNow, $serviceNow)
    {
        $this->product = $productNow;
        $this->service = $serviceNow;
        $this->dollar = $this->product + $this->service;
        return $this->requestTotal();
    }
    public function requestTotal()
    {
        $this->dollar *= $this->rate;
        return $this->dollar;
    }
}
登入後複製

查看這個類別,可以看到其中有一個屬性$rate,requestTotal()方法使用$rate計算一次交易的金額.在這個版本中, 這個值設定為1,實際上總金額無需再乖以兌換率, 不過如果要為客戶提供折扣或者要增加額外服務或產品的附加費, $rate變量會很方便. 這個類並不是適合器模式的一部分, 不過這是一個起點.

需求變化了

現在客戶的公司要向歐洲發展,所以需要開發一個應用, 能夠用歐元完成同樣的計算. 你希望這個歐元計算能夠像DollarCalc一樣, 要做的就是改變變數名稱.

EuroCalc.php

<?php
class EuroCalc
{
    private $euro;
    private $product;
    private $service;
    public $rate = 1;
    public function requestCalc($productNow, $serviceNow)
    {
        $this->product = $productNow;
        $this->service = $serviceNow;
        $this->euro = $this->product + $this->service;
        return $this->requestTotal();
    }
    public function requestTotal()
    {
        $this->euro *= $this->rate;
        return $this->euro;
    }
}
登入後複製

接下來, 再把應用的其餘部分插入到EuroCalc類中. 不過,因為客戶的所有數據都是按美元計算的.換句話說, 如果不重新開發整個程式, 就無法在系統中"插入"這個歐元計算. 但是你不想這麼做. 為了加入EuroCalc, 你需要一個適配器: 就像找一個適配器來適應歐洲的插座一樣, 可以創建一個適配器, 使你的系統能夠使用歐元. 幸運的是, 類適配器正是為這樣的情況設計的.首先需要創建一個接口. 在這個類圖中, 這個接口名為ITarget. 它只有一個方法requester (). requester()是一個抽象方法, 要由介面的具體實作來實作這個方法.

ITarget.php

<?php
interface ITarget
{
    public function requester();
}
登入後複製

現在方法美元.

在使用繼承的適配器設計模式中, 適配器(Adapter)參與都既實現ITarget接口,還實現了具體類EuroCalc. 創建EuroAdapter不需要做太多工作, 因為大部分工作已經在EuroCal類中完成.現在要做的就是實現request()方法, 使它能把美元值轉換為歐元值.

EuroAdapter.php

<?php
include_once('EuroCalc.php');
include_once('ITarget.php');
class EuroAdapter extends EuroCalc implements ITarget
{
    public function __construct()
    {
        $this->requester();
    }
    public function requester()
    {
        $this->rate = 0.8111;
        return $this->rate;
    }
}
登入後複製

類適配模式中, 一個具體類會繼承另一個具體類, 有這種結構的設計模式很少見, 大多數設計模式中, 幾乎都是繼承一個抽象類, 並由類根據需要實現其抽象方法和屬性. 換句話說, 一般談到繼承時, 都是具體類繼承抽象類別.

由於既實現了一個介面又擴展了一個類別, 所以EuroAdapter類別同時擁有該介面和具體類別的介面. 透過使用requester()方法, EuroAdapter類別可以設定rate值(兌換率), 從而能使用被適配者的功能, 而元而做任何改變.

下面定義一個Client類, 從EuroAdapter和DollarCalc類發出請求. 可以看到,原來的DollarCalc仍能很好地工作, 不過它沒有ITarget介面. 

Client.php

<?php
include_once('EuroAdapter.php');
include_once('DollarCalc.php');
class Client
{
    public function __construct()
    {
        $euro = '&euro;';
        echo "区元: $euro" . $this->makeApapterRequest(new EuroAdapter()) . '<br />';
        echo "美元: $: " . $this->makeDollarRequest(new DollarCalc()) . '<br />';
    }
    private function makeApapterRequest(ITarget $req)
    {
        return $req->requestCalc(40,50);
    }
    private function makeDollarRequest(DollarCalc $req)
    {
        return $req->requestCalc(40,50);
    }
}
$woker = new Client();
登入後複製

運作結果如下:

Euros: €72.999
Dollars: $: 90
登入後複製
之處

可以看到這個模式簡單, 如果是針對更為複雜的計算, 繼承要提供建立類別適配器的Target接口的必要接口和具體實現

使用組合的適配器模式

      对象适配器模式使用组合而不是继承, 不过它也会完成同样的目标. 通过比较这两个版本的适配器模式, 可以看出它们各自的优缺点. 采用类适配器模式时,适配器可以继承它需要的大多数功能, 只是通过接口稍微调. 在对象适配器模式中 适配器(Adapter)参与使用被适配者(Adaptee), 并实现Target接口. 在类适配器模式中, 适配器(Adapter)则是一个被适配者(Adaptee), 并实现Target接口.

示例: 从桌面环境转向移动环境

PHP程序员经常会遇到这样一个问题:需要适应移动环境而做出调整.不久之前,你可能只需要考虑提供一个网站来适应多种不同的桌面环境. 大多数桌面都使用一个布局, 再由设计人员让它更美观. 对于移动设备, 设计人员和开发人员不仅需要重新考虑桌面和移动环境中页面显示的设计元素, 还要考虑如何从一个环境切换到另一个环境.

首先来看桌面端的类Desktop(它将需要一个适配器). 这个类使用了一个简单但很宽松的接口:

IFormat.php

<?php
interface IFormat
{
    public function formatCSS();
    public function formatGraphics();
    public function horizontalLayout();
}
登入後複製

它支持css和图片选择, 不过其中一个方法指示一种水平布局, 我们知道这种布局并不适用小的移动设备.下面给出实现这个接口的Desktop类

Desktop.php

<?php
include_once('IFormat.php');
class Desktop implements IFormat
{
    public function formatCSS()
    {
        echo "引用desktop.css<br />";
    }
    public function formatGraphics()
    {
        echo "引用desktop.png图片<br />";
    }
    public function horizontalLayout()
    {
        echo '桌面:水平布局';
    }
}
登入後複製

问题来了, 这个布局对于小的移动设备来说太宽了. 所以我们的目标是仍采用同样的内容, 但调整为一种移动设计.

下面来看移动端的类Mobile

首先移动端有一个移动端的接口

IMobileFormat

<?php
interface IMobileFormat
{
    public function formatCSS();
    public function formatGraphics();
    public function verticalLayout();
}
登入後複製

可以看到, IMobileFormat接口和IFormat接口是不一样的,也就是不兼容的, 一个包含了方法horizontalLayout(), 另一个包含方法verticalLaout(), 它们的差别很小, 最主要的区别是: 桌面设计可以采用水平的多栏布局, 而移动设计要使用垂直布局,而适配器就是要解决这个问题

下面给出一个实现了IMoibleFormat接口的Mobile类

Mobile.php

<?php
include_once('IMobileFormat.php');
class Mobile implements IMobileFormat
{
    public function formatCSS()
    {
        echo "引用mobile.css<br />";
    }
    public function formatGraphics()
    {
        echo "引用mobile.png图片<br />";
    }
    public function verticalLayout()
    {
        echo '移动端:垂直布局';
    }
}
登入後複製

Mobile类和Desktop类非常相似, 不过是图片和CSS引用不同

接下来,我们需要一个适配器,将Desktop和Mobile类结合在一起

MobileAdapter.php

<?php
include_once('IFormat.php');
include_once('Mobile.php');
class MobileAdapter implements IFormat
{
    private $mobile;
    public function __construct(IMobileFormat $mobileNow)
    {
        $this->mobile = $mobileNow;
    }
    public function formatCSS()
    {
        $this->mobile->formatCSS();
    }
    public function formatGraphics()
    {
        $this->mobile->formatGraphics();
    }
    public function horizontalLayout()
    {
        $this->mobile->verticalLayout();
    }
}
登入後複製

可以看到,MobileAdapter实例化时要提供一个Mobile对象实例.还要注意 ,类型提示中使用了IMobileFormat, 确保参数是一个Mobile对象.有意思的是, Adapter参与者通过实现horizontalLayout()方法来包含verticalLayout()方法.实际上, 所有MobileAdapter方法都包装了一个Mobile方法.碰巧的是, 适配器参与者中的一个方法并不在适配器接口中(verticalLayout());它们可能完全不同, 适配器只是把它们包装在适配器接口(IFormat)的某一方法中.

客户调用(Client)

Client.php

<?php
include_once('Mobile.php');
include_once('MobileAdapter.php');
class Client
{
    private $mobile;
    private $mobileAdapter;
    public function __construct()
    {
        $this->mobile = new Mobile();
        $this->mobileAdapter = new MobileAdapter($this->mobile);
        $this->mobileAdapter->formatCSS();
        $this->mobileAdapter->formatGraphics();
        $this->mobileAdapter->horizontalLayout();
    }
}
$worker = new Client();
登入後複製

适配器模式中的Client类必须包装Adaptee(Mobile)的一个实例, 以便集成到Adapter本身.实例化Adapter时, Client使用Adatee作为参数来完成Adapter的实例化.所以客户必须首先创建一个Adapter对象(new Mobile()), 然后创建一个Adapter((new MobileAdapter($this->mobile)).

Client类的大多数请求都是通过MobileAdapter发出的. 不过这个代码的最后他使用了Mobile类的实例.

适配器和变化

      PHP程序员要即该面对变化.不同版本的PHP会变化, 可能增加新的功能, 另外还可能取消一些功能.而且随着PHP的大大小小的变化,MySQL也在改变.例如, mysql的扩展包升级为mysqli, PHP开发人员需要相应调整, 要改为使用mysqli中的新API.这里适合采用适配器模式吗?可能不适合.适配器可能适用, 可能不适用,这取决于你的程序如何配置.当然可以重写所有连接和交互代码, 不过这可不是适配器模式的本意, 这就像是重新安装USB连接头, 想把它插进标准的墙上插座一样. 不过, 如果所有原来的mysql代码都在模块中, 你可以修改这个模块(类),换入一个有相同接口的新模块.只是要使用mysqli而不是mysql.我不认为交换等同于适配器, 不过道理是一样的, 在适配器模式中, 原来的代码没有任何改变, 有变化的只是适配器.

如果需要结合使用两个不兼容的接口, 这种情况下, 适配器模式最适用.适配器可以完成接口的"联姻".可以把适配器看作是一个婚姻顾问;通过创建一个公共接口来克服双方的差异.利用 这种设计模式, 可以促成二者的合作,而避免完全重写某一部分.

以上就介绍了设计模式之:适配器模式,包括了方面的内容,希望对PHP教程有兴趣的朋友有所帮助。

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!