適配器很容易理解, 大多數人家庭都有手機轉接器, 用來爲移動電話充電,這就是一種適配器. 若是隻有USB接頭, 就沒法將移動電話插到標準插座上. 實際上, 必須使用一個適配器, 一端接USB插頭, 一端接插座. 固然, 你能夠拿出電氣工具,改裝USB鏈接頭, 或者從新安裝插座, 不過這樣會帶來不少額外的工做, 並且可能會把鏈接頭或插座弄壞. 因此, 最可取的方法就是找一個適配器. 軟件開發也是如此.php
類適配器模式(使用繼承)
類適配器模式很簡單, 不過與對象適配器模式相比, 類適配器模式的靈活性弱些, 類適配器簡單的緣由在於 , 適配器(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();
}
如今開發人員能夠實現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 = '€';
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.我不認爲交換等同於適配器, 不過道理是同樣的, 在適配器模式中, 原來的代碼沒有任何改變, 有變化的只是適配器.
若是須要結合使用兩個不兼容的接口, 這種狀況下, 適配器模式最適用.適配器能夠完成接口的"聯姻".能夠把適配器看做是一個婚姻顧問;經過建立一個公共接口來克服雙方的差別.利用 這種設計模式, 能夠促成兩者的合做,而避免徹底重寫某一部分.