1. 程式人生 > >Golang 實現設計模式 —— 裝飾模式

Golang 實現設計模式 —— 裝飾模式

概念


“用於代替繼承的技術,無需通過繼承增加子類就能擴充套件物件的新功能”

“動態地給一個物件新增一些額外的職責,就增加功能來說,裝飾模式比生成子類更為靈活”

何時用


需要擴充套件一個類的功能,或給一個類增加附加責任

需要動態的給一個物件增加功能,且可以動態地撤銷它

需要增加一些基本功能的排列組合而產生的大量的功能,而使得繼承變得非常困難的時候

實現構件


抽象構件(Component)
表示“被”裝飾的本體的抽象定義,這個定義通常是一個介面(Interface),定義了若干方法(能力),這些方法可以用來在被具體裝飾角色(ConcreteDecorator)實現時改變原有構件本體的方法(能力),如原來本體傷害輸出是 10,裝飾角色把它再增加 10 而不會影響本體的原有邏輯(程式碼)。

具體構件(ConcreteComponent)
表示實現了抽象構件(Component)的物件,即將要接受附加能力的物件,本文中比喻的“本體”。

抽象裝飾(Decorator)
持有一個抽象構件的例項(即具體構件),並定義(實現)與抽象構件介面一致的介面。抽象裝飾的(部分)作用應該是應用了依賴倒置原則,在結構上使具體裝飾(ConcreteDecorator)不要直接依賴於抽象構件,因為二者作用性質也不同,直接依賴灰常奇怪,就好像都是50歲的男子也不能把隔壁老王當成爸爸一樣。

具體裝飾(ConcreteDecorator)
實現了抽象裝飾的定義,負責給構件物件新增附加能力的物件。具體裝飾通過實現抽象裝飾定義的介面,擁有了和具體構件一樣的“能力”(方法/函式/屬性),再通過抽象裝飾定義中所持有的抽象構件的例項而獲得對該例項“相同”能力的結果,並在結果上進行一些裝飾。

實現步驟


  • 定義抽象構件,提供抽象介面
  • 定義具體構件並實現抽象構件,構造後的具體構件即理解為“本體”,被裝飾的物件
  • 定義抽象裝飾,它要做兩件事,實現抽象構件和儲存一個抽象構件物件
  • 定義具體裝飾,具體裝飾要實現抽象裝飾,並在實現的介面方法中對構件進行具體裝飾操作
  • 之後,要增加“本體”就建立具體構件,要增加裝飾物,就建立具體裝飾
  • 使用時,把本體“傳遞進”裝飾物件,在裝飾物件(同樣繼承自抽象構件)的方法裡去使用本體的方法和結果,加工它,並輸出進行了“調整”的結果

原理與程式碼


用 Golang 描述程式碼結構(程式碼模仿自 github,但不好意思忘記來自哪位作者了)

package component

/*
抽象構件(Component)介面
 */
type Beverage interface {
    // 計算價格
    Cost() int
    // 返回描述
    Me() string
}

上面定義了抽象構件介面“飲料”介面,它包含了兩個方法,輸出價格和描述自己,飲料介面作為最底層介面是所有飲料都必須要實現的(能力)。

package component

type Tea struct {
    Beverage // 作用?
    name string
    price int
}

func (self *Tea) Me() string {
    return self.name
}

func (self *Tea) Cost() int {
    return self.price
}

有了飲料這個概念,就在此基礎之上建立第一款具體的產品:“茶”。茶是飲料,因此它要繼承飲料的特性(實現介面)。如何表達茶實現了飲料介面,使得上層呼叫茶時可以訪問茶的介面呢?按照 Golang 的語法特性先定義一個 Tea 結構(類),先有了茶。

Golang 中實現介面無需宣告,實現該介面所有方法即為(自動)實現介面,因此 Tea 類要通過實現 MeCost 兩個具體方法來實現對介面的實現(這話說的)。與普通方法不同的是在 funcMe 中間要增加 (self *Tea) ,這種語法糖的作用簡單說就是當前這個 Me 方法被 Tea 這個類包含(實現)了,以後可以 Tea.Me() 這麼用了。

在兩個方法裡需要實現具體的邏輯,要輸出對自身的描述和價格,那值從哪兒來,於是給 Tea 定義了兩個私有欄位 nameprice,以便在構造類例項時對其賦值。

Tea 中還包含了一個 Beverage,意思是通過組合的方式讓類有了 Beverage 物件,但個人理解在本例中沒有起到實質作用,因為 Tea 已經是 Beverage 的具體實現了,除非再創建出茶下面的紅茶、綠茶繼承自茶,它可用被用做標記上層結構是誰,否則在本例中只有茶一種飲品,或建立咖啡這種與茶是平級關係的構件,那這個內部的 Beverage 就沒有作用了。

package component

type Moli struct {
    *Tea
}

func NewMoli() *Moli {
    return &Moli{&Tea{name: "茉莉", price: 48}}
}

func (self *Moli) Me() string{
    return self.name
}

func (self *Moli) Cost() int {
    return self.price
}
package component

type Puer struct {
    *Tea
}

func NewPuer() *Puer {
    return &Puer{&Tea{name: "普洱", price: 38}}
}

func (self *Puer) Me() string{
    return self.name
}

func (self *Puer) Cost() int {
    return self.price
}

上面建立兩種具體的茶,茉莉和普洱。可以看到兩種茶實際是一種結構,為了表達裝飾模式的特性這樣寫更為清晰。類中只包含了一個物件,就是指向茶的指標,也就是“指向某個茶的指標”。普洱類就像個殼,名字叫普洱,殼裡邊只有一種(個)物件就是茶。

NewPuer的語法可以幫助我們方便的例項化一個普洱,它的返回值是指標,內在的邏輯是返回一個袋子,這種袋子叫 Puer,它裡面(只)有一種(個)東西名叫普洱價格是38元的茶。茉莉邏輯與此相同。

到此,完成了抽象構件和具體構件的設計和建立,實際可以喝茶了,沏上兩杯試一下

package main

func main() {

    moli := component.NewMoli()
    puer := component.NewPuer()

    fmt.Printf("第 %v 杯是 %s 售價 %v 元\n", 1, moli.Me(), moli.Cost())
    fmt.Printf("第 %v 杯是 %s 售價 %v 元\n", 2, puer.Me(),puer.Cost())

    fmt.Printf("好喝嗎,歡迎再來 ^_^ ")
}

上面程式碼會輸出兩杯茶的資訊

第 1 杯是 茉莉 售價 48 元
第 2 杯是 普洱 售價 38 元

下面該裝飾了,我要建立一些輔料,比如糖和冰,並希望能自由的放進想放的飲料中而不會和某種飲料硬性繫結,最終實現的邏輯是點一杯加糖的茉莉而不是點一杯茉莉自己再買一包糖倒裡邊。按照原理先定義出一個抽象裝飾,它要同樣實現抽象構件 Beverage 介面,並(最好)還能保持對構件的引用,因為要有“本體”才能裝飾,不然對誰做裝飾呢。

package decorator

import "golang-design-pattern/decorator/component"

type Condiment struct {
    *component.Tea //作用?
    beverage component.Beverage
    name string
    price int
}

func (self *Condiment) Me() string {
    return self.name
}

func (self *Condiment) Cost() int {
    return self.price
}

上面是抽象裝飾 Condiment,與 Tea 一樣實現了兩個具體的方法,並擁有兩個方法要使用到的私有欄位。beverage component.Beverage 讓它能夠儲存一個符合抽象構件介面要求的物件,即只要是滿足 Beverage 介面定義的物件我就能儲存著以後用。

package decorator

import "golang-design-pattern/decorator/component"

type Sugar struct {
    *Condiment
}

func NewSugar(beverage component.Beverage) *Sugar {
    return &Sugar{ &Condiment{beverage:beverage, name:"糖", price:3 }}
}

func (self *Sugar) Me() string{
    return self.beverage.Me() + " 加點 " + self.name
}

func (self *Sugar) Cost() int {
    return self.beverage.Cost() + self.price
}
package decorator

import "golang-design-pattern/decorator/component"

type Ice struct {
    *Condiment
}

func NewIce(beverage component.Beverage) *Ice {
    return &Ice{ &Condiment{beverage: beverage, name: "冰", price: 3 }}
}

func (self *Ice) Me() string {
    return "加了" + self.name + "的" + self.beverage.Me()
}

func (self *Ice) Cost() int {
    return self.beverage.Cost() + self.price
}

上面定義兩種輔料,SugarIce。角色是具體裝飾,內部儲存著對 Condiment 的引用,並且它們也要實現 Beverage 介面,是為了履行裝飾模式的特性,即對上層呼叫是透明的,呼叫裝飾件和呼叫具體構件方法一樣,否則就違背或汙染了裝飾模式的優勢。

具體裝飾的介面方法是關鍵,以 Sugar 中的 Cost 方法為例,它的實現是通過將 Sugar (裡)的 Condiment (裡)的 beverage 的價格疊加上 Sugar 自己的價格,作為這一杯“加糖飲料”的價格。

好了,輔料也有了,讓我們來做一杯加糖的茉莉和加冰的普洱吧

package main

import (
    "fmt"
    "golang-design-pattern/decorator/component"
    "golang-design-pattern/decorator/decorator"
)

func main() {

    moli := component.NewMoli()
    puer := component.NewPuer()

    fmt.Printf("第 %v 杯是 %s 售價 %v 元\n", 1, moli.Me(), moli.Cost())
    fmt.Printf("第 %v 杯是 %s 售價 %v 元\n", 2, puer.Me(),puer.Cost())

    fmt.Printf("下面我們給剛才那杯茉莉加點糖...\n")
    sugar := decorator.NewSugar(moli)
    fmt.Printf("剛剛給茉莉加了點糖,現在準備嘗一下\n")
    fmt.Printf("第 %v 杯是 %s 售價 %v 元\n", 3, sugar.Me(), sugar.Cost())

    ice := decorator.NewIce(puer)
    fmt.Printf("來一杯加冰的普洱,現在準備嘗一下\n")
    fmt.Printf("第 %v 杯是 %s 售價 %v 元\n", 4, ice.Me(), ice.Cost())
    fmt.Printf("好喝嗎,歡迎再來 ^_^ ")

首先做了兩杯標準的茶,一杯48元叫做“茉莉”的Moli茶,一杯38元的叫做“普洱”的Puer茶。為了給這杯Moli加點糖,建立了 sugar,並把剛才那杯 moli “傳”給了它。它拿到了 moli 後加了糖(sugar.Me方法),並把價格提高到了 48+3 元,加冰的Puer亦是如此。

感受

為什麼要把本體傳給裝飾,而不是往本體上“新增”裝飾,這個邏輯讓我想不通彆扭了很久,其實到現在也是彆扭。越彆扭越佩服創造邏輯創造模式的聰明人,因為在本體上做動作,一定會增加本體的額外工作,甚至會破壞本體原有的結構,本體會怎麼想,我就是一杯茉莉茶,我為什麼要實現加糖、加醋、加冰這些方法。所以換個角度看,把具體裝飾想象成一個廚師,把本體(具體構件)給TA,TA來做操作就好理解一些了。

單點一杯Moli,再點一個Sugar,把它們加一起也能達成效果,這和裝飾模式有什麼區別?個人理解裝飾模式是“官方組裝”,是對於客戶端而言的。客戶端需要一杯加了糖的茉莉茶,這是一杯經過組合加工的整體產品交付,而不是扔給客戶一杯茶一袋糖,這有本質的區別。在應用中,裝飾模式往往被用來做更有趣的功能擴充套件,核心優點是通過“組合”而不是“繼承”的方式,在不改變本體的情況下,改變結果。

一定要儘量理解邏輯本身的邏輯,而不能僅從文字字面意思理解,中文英文字身意思就差距甚遠,更何況中文自己又那麼博大精深。

參考與引用


CSDN Shulin

部落格園 灌水樂