裝飾模式 (Decorator Pattern)

kevinyan發表於2018-05-24

裝飾模式能夠實現動態的為物件新增功能,是從一個物件外部來給物件新增功能。通常有兩種方式可以實現給一個類或物件增加行為:

  • 繼承機制,使用繼承機制是給現有類新增功能的一種有效途徑,通過繼承一個現有類可以使得子類在擁有自身方法的同時還擁有父類的方法。但是這種方法是靜態的,使用者不能控制增加行為的方式和時機。
  • 組合機制,即將一個類的物件嵌入另一個物件中,由另一個物件來決定是否呼叫嵌入物件的行為以便擴充套件自己的行為,我們稱這個嵌入的物件為裝飾器(Decorator)

顯然,為了擴充套件物件功能頻繁修改父類或者派生子類這種方式並不可取。在物件導向的設計中,我們應該儘量使用物件組合,而不是物件繼承來擴充套件和複用功能。裝飾器模式就是基於物件組合的方式,可以很靈活的給物件新增所需要的功能。裝飾器模式的本質就是動態組合。動態是手段,組合才是目的。總之,裝飾模式是通過把複雜的功能簡單化,分散化,然後在執行期間,根據需要來動態組合的這樣一個模式。

裝飾模式定義

裝飾模式(Decorator Pattern) :動態地給一個物件增加一些額外的職責(Responsibility),就增加物件功能來說,裝飾模式比生成子類實現更為靈活。其別名也可以稱為包裝器(Wrapper),與介面卡模式的別名相同,但它們適用於不同的場合。根據翻譯的不同,裝飾模式也有人稱之為“油漆工模式”,它是一種物件結構型模式。

裝飾模式的優點

  • 裝飾模式與繼承關係的目的都是要擴充套件物件的功能,但是裝飾模式可以提供比繼承更多的靈活性。
  • 可以通過一種動態的方式來擴充套件一個物件的功能,通過配置檔案可以在執行時選擇不同的裝飾器,從而實現不同的行為。
  • 通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行為的組合。可以使用多個具體裝飾類來裝飾同一物件,得到功能更為強大的物件。

模式結構和說明

裝飾器模式UML

  • 聚合關係用一條帶空心菱形箭頭的直線表示,上圖表示Component聚合到Decorator上,或者說Decorator由Component組成。
  • 繼承關係用一條帶空心箭頭的直接表示
  • 看懂UML類圖請看這個文件

Component:元件物件的介面,可以給這些物件動態的新增職責;

ConcreteComponent:具體的元件物件,實現了元件介面。該物件通常就是被裝飾器裝飾的原始物件,可以給這個物件新增職責;

Decorator:所有裝飾器的父類,需要定義一個與Component介面一致的介面(主要是為了實現裝飾器功能的複用,即具體的裝飾器A可以裝飾另外一個具體的裝飾器B,因為裝飾器類也是一個Component),並持有一個Component物件,該物件其實就是被裝飾的物件。如果不繼承Component介面類,則只能為某個元件新增單一的功能,即裝飾器物件不能再裝飾其他的裝飾器物件。

ConcreteDecorator:具體的裝飾器類,實現具體要向被裝飾物件新增的功能。用來裝飾具體的元件物件或者另外一個具體的裝飾器物件。

裝飾器的示例程式碼

1.Component抽象類, 可以給這些物件動態的新增職責

abstract class Component
{
	abstract public function operation();
}
複製程式碼

2.Component的實現類

class ConcreteComponent extends Component
{
	public function operation()
	{
		echo __CLASS__ .  '|' . __METHOD__ . "\r\n";
	}
}
複製程式碼

3.裝飾器的抽象類,維持一個指向元件物件的介面物件, 並定義一個與元件介面一致的介面

abstract class Decorator extends Component
{
	/**
	 * 持有Component的物件
	 */
	protected $component;

	/**
	 * 構造方法傳入
	 */
	public function __construct(Component $component)
	{
		$this->component = $component;
	}

	abstract public function operation();
}
複製程式碼

4.裝飾器的具體實現類,向元件物件新增職責,beforeOperation(),afterOperation()為前後新增的職責。

class ConcreteDecoratorA extends Decorator
{
	//在呼叫父類的operation方法的前置操作
	public function beforeOperation()
	{
		echo __CLASS__ . '|' . __METHOD__ . "\r\n";
	}

	//在呼叫父類的operation方法的後置操作
	public function afterOperation()
	{
		echo __CLASS__ . '|' . __METHOD__ . "\r\n";
	}

	public function operation()
	{
		$this->beforeOperation();
		$this->component->operation();//這裡可以選擇性的呼叫父類的方法,如果不呼叫則相當於完全改寫了方法,實現了新的功能
		$this->afterOperation();
	}
}

class ConcreteDecoratorB extends Decorator
{
	//在呼叫父類的operation方法的前置操作
	public function beforeOperation()
	{
		echo __CLASS__ . '|' . __METHOD__ . "\r\n";
	}

	//在呼叫父類的operation方法的後置操作
	public function afterOperation()
	{
		echo __CLASS__ . '|' . __METHOD__ . "\r\n";
	}

	public function operation()
	{
		$this->beforeOperation();
		$this->component->operation();//這裡可以選擇性的呼叫父類的方法,如果不呼叫則相當於完全改寫了方法,實現了新的功能
		$this->afterOperation();
	}
}
複製程式碼

5.客戶端使用裝飾器

class Client
{
	public function main()
	{
		$component = new ConcreteComponent();
		$decoratorA = new ConcreteDecoratorA($component);
		$decoratorB = new ConcreteDecoratorB($decoratorA);
		$decoratorB->operation();
	}
}

$client = new Client();
$client->main();
複製程式碼

6.執行結果

oncreteDecoratorB|ConcreteDecoratorB::beforeOperation
ConcreteDecoratorA|ConcreteDecoratorA::beforeOperation
ConcreteComponent|ConcreteComponent::operation
ConcreteDecoratorA|ConcreteDecoratorA::afterOperation
ConcreteDecoratorB|ConcreteDecoratorB::afterOperation
複製程式碼

裝飾模式需要注意的問題

  • 一個裝飾類的介面必須與被裝飾類的介面保持相同,對於客戶端來說無論是裝飾之前的物件還是裝飾之後的物件都可以一致對待。
  • 儘量保持具體元件類ConcreteComponent的輕量,不要把主邏輯之外的輔助邏輯和狀態放在具體元件類中,可以通過裝飾類對其進行擴充套件。 如果只有一個具體元件類而沒有抽象元件類,那麼抽象裝飾類可以作為具體元件類的直接子類。

適用環境

  • 需要在不影響元件物件的情況下,以動態、透明的方式給物件新增職責。
  • 當不能採用繼承的方式對系統進行擴充或者採用繼承不利於系統擴充套件和維護時可以考慮使用裝飾類。

本文已經收錄在系列文章Laravel原始碼學習裡,歡迎訪問閱讀。

相關文章