通俗易懂的談談裝飾器模式

JeffreyC發表於2017-09-24

前言

在編碼的時候,我們為了擴充套件一個類經常是使用繼承方式來實現,隨著擴充套件功能的增多,子類會越來越膨脹,使系統變得不靈活。

裝飾器模式( Decorator Pattern )允許向一個現有的物件新增新的功能,同時又不改變其結構。它能讓我們在擴充套件類的時候讓系統較好的保持靈活性。

那麼裝飾器模式具體是什麼樣的呢?

從一個情景開始

我們有一塊地,在這塊地上,我們要蓋一棟有好幾間房間的別墅,每間房間的裝修費用都不同,現在,我們要對蓋別墅的費用進行計算。

先定義一個 Land 類,表示這塊地,Land 類定義了在這塊地上蓋別墅需要花錢這個規則。

abstract class Land
{
    abstract public function cost();
}

Land 已經定義好了在這塊地上蓋房需要花錢的這個規則了,但是蓋一間房間具體花多少錢呢?

此時我們再定義一個 Room 類,這個類具體的定義了一個房間建造的基本費用(一個最簡單房間,裡面啥也沒有的)。

class Room extends Land
{
    private $money = 1000;
    public function cost()
    {
        return $this->money;
    }
}

然後開始建造房間,我們建了兩個房間,分別是客廳和餐廳,用 LivingRoom 類和 DiningRoom 類來表示

class LivingRoom extends Room
{
    public function cost()
    {
        return parent::cost()+200; //客廳的建造費用在房屋建造費用的基礎上多200,比如要買沙發,電視
    }

}

class DiningRoom extends Room
{
    public function cost()
    {
        return parent::cost()+100; //餐廳的建造費用在房屋建造費用的基礎上多100,比如買餐桌
    }
}

現在,我們很容易就能得到建造一間客廳所需的花費

$livingRoomCost = new LivingRoom();
echo $livingRoomCost->cost();

問題的產生

不過,這樣的結構並不具備靈活性,雖然我們可以很容易的分別得出建造一間客廳和建造一間餐廳的費用,但是,如果我買的地比較小,只能把餐廳和客廳建在同一個房間裡,那要怎麼去計算費用?難道還要很麻煩的去建立一個包含客廳和餐廳的 LivingDiningRoom 類?這樣做的話除了麻煩,還會使程式碼產生重複。

解決問題

為了更好的解決這個問題,我們得做一些調整,同樣先宣告 Land 類和 Room 類,不同的是,引入了一個房間的裝飾類 RoomDecorator,它繼承了 Land 類,因為沒有實現 Land 類的 cost() 方法,所以需將它宣告為抽象類,並且定義了一個以 Land 類的物件為引數的構造方法,傳入的物件會儲存在 $land 屬性中,該屬性宣告為 protected ,以便子類訪問。具體如下。

abstract class RoomDecorator extends Land
{
    protected $land;
    public function __construct(Land $land)
    {
        $this->land = $land;
    }
}

然後我們再重新定義客廳類和餐廳類

class LivingRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+200;
    }
}

class DiningRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+100;
    }
}

這兩個類都擴充套件自 RoomDecorator 類,這意味著它們都擁有指向 Land 物件的引用。當它們的 cost() 方法被呼叫時,都會先呼叫所引用的 Land 類物件的 cost() 方法,然後再執行自己特有的操作。

所以這時候,建造一間客廳所需的費用是這樣計算

$livingRoomCost = new LivingRoom(new Room());
echo $livingRoomCost->cost(); //輸出1200

建造一間餐廳所需的費用是這樣計算

$diningRoomCost = new DiningRoom(new Room());
echo $diningRoomCost->cost(); //輸出1100

回到剛才的問題,如果我們需計算建造一間包含客廳餐廳的房間所需費用,程式碼如下

$livingRoom = new DiningRoom(new LivingRoom(new Room()));
echo $livingRoom->cost(); //輸出1300

看,我們現在計算建造費用的思路是:計算出基礎房間的費用 --> 在基礎房間上裝飾成客廳的費用 --> 在客廳的基礎上加裝飾餐廳的費用 --> 得到包含客廳餐廳的房間費用。已經不需要麻煩的通過建立一個 LivingDiningRoom 類來計算包含客廳餐廳的房間建造費用了。

這便是裝飾模式,通過一層一層的裝飾,我們可以靈活的得到我們想要的結果。可以輕鬆的新增新的裝飾器類或者新的元件來建立靈活的結構。

完整程式碼

<?php

abstract class Land
{
    abstract function cost();
}

class Room extends Land
{
    private $money = 1000;
    public function cost()
    {
        return $this->money;
    }
}

//裝飾器
abstract class RoomDecorator extends Land
{
    protected $land;
    public function __construct(Land $land)
    {
        $this->land = $land;
    }
}

class LivingRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+200;
    }
}

class DiningRoom extends RoomDecorator
{
    public function cost()
    {
        return $this->land->cost()+100;
    }
}

$livingRoomCost = new LivingRoom(new Room());
echo $livingRoomCost->cost(); //輸出1200

$diningRoomCost = new DiningRoom(new Room());
echo $diningRoomCost->cost(); //輸出1100

$livingDining = new DiningRoom(new LivingRoom(new Room()));
echo $livingDining->cost(); //輸出1300

the end.

happy coding! ^_^

本作品採用《CC 協議》,轉載必須註明作者和本文連結

You can not connect the dots looking forward, you can only connect them looking backwards.

相關文章