Golang 設計模式之裝飾器模式
1 裝飾器模式
本期和大家交流的是設計模式中的裝飾器模式.
下面聊聊關於裝飾器模式的基本定義:裝飾器模式能夠在不改變原對象結構的基礎上,動態地爲對象增加附屬能力. 在實現思路上,裝飾器模式和 “繼承” 一定的類似之處,但是兩者側重點有所不同,可以把裝飾器模式作爲 “繼承” 的一種補充手段.
這麼幹講概念顯得過於抽象,下面我們通過一個實際案例,來和大傢俱體地剖析一下有關於裝飾器模式的實現思路:
基於以上條件,我們開始烹飪創作:
-
• 比如選用主菜培根搭配上副菜雞蛋,就形成了一碗雞蛋培根蓋澆飯;
-
• 比如選用主菜牛肉搭配上副菜青椒,就形成了一碗青椒牛肉蓋澆飯;
-
• 比如選用主菜雞排搭配上副菜黑椒汁,就形成了一碗黑椒雞排蓋澆飯.
聊到這裏,下面我們嘗試通過編程的方式還原上面的場景問題.
一種常見的實現方式是可以採用繼承的方式進行實現:
-
• 我們構造出一個最基礎的父類:米飯
-
• 在白米飯的基礎上,根據添加的主菜肉食,實現出現對應的幾個一級子類:培根飯、牛肉飯、雞排飯
-
• 在一級子類的基礎上,再搭配上各種副菜,實現對應的幾個二級子類,包括:雞蛋培根飯、青椒培根飯、青椒牛肉飯、黑椒雞排飯等等
在上述 “繼承” 的實現思路中,我們需要對子類的等級以及種類進行枚舉,包括通過加入主菜後形成的一系列一級子類以及加入主菜和副菜後形成的一系列二級子類,這樣一套相對固定的等級架構也暴露出來一些問題:
-
• 在實際場景中,主菜和副菜的結合可以是更加靈活多樣的,比如作爲副菜的雞蛋不僅可以和主菜的培根組合,還可以和牛肉或者雞排搭配;比如實際場景中,後續可能有更多的主菜和副菜類型出現,如果需要對所有的組合進行窮盡,則需要經歷一輪笛卡爾內積,最終子類的數量將會嚴重膨脹無法收斂
-
• 使用主菜和副菜對配菜的類型進行界定顯得過於刻板,主菜和副菜本質上都是菜品而已,可以根據用戶的喜好靈活添加,比如用戶可以只要副菜或者只要主菜,可以只添加雙份甚至三份的雞蛋而不添加培根或者牛肉,也可以選擇要一份主菜搭配多份配菜,比如一份牛肉兩份雞蛋等等. 這樣的話,原本約定好的基於繼承實現的等級架構就不再適用了
結合以上兩點,我們發現在這類 “加料” 的場景中,使用繼承的設計模式未必合適. 我們不妨切換思路,不再聚焦於嘗試對所有組合種類進行一一枚舉,而是把注意力放在 “加料” 的這個過程行爲當中:
-
• 首先, 我們不再區分主菜和副菜,不論是雞蛋還是培根還是青椒,我們都把它們當中一種普通的 “菜品”
-
• 針對於每一種 “菜品”,我們定義出一個裝飾器類
-
• 每次使用一個裝飾器類時,對應的邏輯是會往原本的主食中添加一份對應的 “菜品”
在這種實現的思路下,就誕生出了基於 “裝飾器模式” 的實現架構,如下圖所示:
在裝飾器模式中,一份雞蛋培根蓋澆飯 = 一份白米飯(核心類)+ 一份雞蛋(裝飾器 1)+ 一份培根(裝飾器 2),其中雞蛋和培根對應裝飾器的使用順序是不作限制的. 於是不管後續有多少種新的 “菜品” 加入,我們都只需要聲明其對應的裝飾器類即可,只要 “菜品” 的種類確定,後續用戶想要組裝出何種形式的蓋澆飯,都是 easy case.
比如,雙份雞蛋蓋澆飯 = 一份白米飯(核心類)+ 一份雞蛋(裝飾器 1)+ 一份雞蛋(裝飾器 1);雞蛋火腿青椒蓋澆飯 = 一份白米飯(核心類)+ 一份雞蛋(裝飾器 1)+ 一份青椒(裝飾器 2)+ 一份火腿(裝飾器 3);雙份牛肉青椒蓋澆飯 = 一份白米飯(核心類)+ 一份青椒(裝飾器 4)+ 一份牛肉(裝飾器 5)+ 一份牛肉(裝飾器 5)
到這裏爲止,問題已經得到圓滿解決. 下面,我們再回過頭對裝飾器模式和繼承模式做一個對比總結:
-
• 繼承強調的是等級制度和子類種類,這部分架構需要一開始就明確好
-
• 裝飾器模式強調的是 “裝飾” 的過程,而不強調輸入與輸出,能夠動態地爲對象增加某種特定的附屬能力,相比於繼承模式顯得更加靈活,且符合開閉原則
2 代碼實現
2.1 類型聲明實現
下面我們就進入代碼實戰環節,通過編程的方式實現一個搭配食材的案例,以此來展示裝飾器模式的實現細節.
這個案例非常簡單,我們需要主食的基礎上添加配菜,最終搭配出美味可口的食物套餐. 其中主食包括米飯 rice 和麪條 noodle 兩條,而配菜則包括老乾媽 LaoGanMa(老乾媽拌飯頂呱呱)、火腿腸 HamSausage 和煎蛋 FriedEgg 三類.
事實上如果需要的話,主食和配菜也可以隨時進行擴展,在裝飾器模式中,這種擴展行爲的成本並不高.
下面先展示一下總體的 UML 類圖:
首先是對應於裝飾器模式中核心類的是原始的主食 Food,我們聲明瞭一個 interface,其中包含兩個核心方法,Eat 和 Cost,含義分別爲食用主食以及計算出主食對應的花費.
type Food interface {
// 食用主食
Eat() string
// 計算主食的話費
Cost() float32
}
Food 對應的實現類包括米飯 rice 和麪條 noodle:
type Rice struct {
}
func NewRice() Food {
return &Rice{}
}
func (r *Rice) Eat() string {
return "開動了,一碗香噴噴的米飯..."
}
// 需要花費的金額
func (r *Rice) Cost() float32 {
return 1
}
type Noodle struct {
}
func NewNoodle() Food {
return &Noodle{}
}
func (n *Noodle) Eat() string {
return "嗦面ing...嗦~"
}
// 需要花費的金額
func (n *Noodle) Cost() float32 {
return 1.5
}
接下來是裝飾器部分,我們聲明瞭一個 Decorate interface,它們本身是在強依附於核心類(主食)的基礎上產生的,只能起到錦上添花的作用,因此在構造器函數中,需要傳入對應的主食 Food.
type Decorator Food
func NewDecorator(f Food) Decorator {
return f
}
接下來分別聲明三個裝飾器的具體實現類,對應爲老乾媽 LaoGanMaDecorator、火腿腸 HamSausageDecorator、和煎蛋 FriedEggDecorator.
每個裝飾器類的作用是對食物進行一輪裝飾增強,因此需要在構造器函數中傳入待裝飾的食物,然後通過重寫食物的 Eat 和 Cost 方法,實現對應的增強裝飾效果.
type LaoGanMaDecorator struct {
Decorator
}
func NewLaoGanMaDecorator(d Decorator) Decorator {
return &LaoGanMaDecorator{
Decorator: d,
}
}
func (l *LaoGanMaDecorator) Eat() string {
// 加入老乾媽配料
return "加入一份老乾媽~..." + l.Decorator.Eat()
}
func (l *LaoGanMaDecorator) Cost() float32 {
// 價格增加 0.5 元
return 0.5 + l.Decorator.Cost()
}
type HamSausageDecorator struct {
Decorator
}
func NewHamSausageDecorator(d Decorator) Decorator {
return &HamSausageDecorator{
Decorator: d,
}
}
func (h *HamSausageDecorator) Eat() string {
// 加入火腿腸配料
return "加入一份火腿~..." + h.Decorator.Eat()
}
func (h *HamSausageDecorator) Cost() float32 {
// 價格增加 1.5 元
return 1.5 + h.Decorator.Cost()
}
type FriedEggDecorator struct {
Decorator
}
func NewFriedEggDecorator(d Decorator) Decorator {
return &FriedEggDecorator{
Decorator: d,
}
}
func (f *FriedEggDecorator) Eat() string {
// 加入煎蛋配料
return "加入一份煎蛋~..." + f.Decorator.Eat()
}
func (f *FriedEggDecorator) Cost() float32 {
// 價格增加 1 元
return 1 + f.Decorator.Cost()
}
做好所有的準備工作之後,下面我們通過單測代碼,展示裝飾器模式的使用示例:
func Test_decorator(t *testing.T) {
// 一碗乾淨的米飯
rice := NewRice()
rice.Eat()
// 一碗乾淨的麪條
noodle := NewNoodle()
noodle.Eat()
// 米飯加個煎蛋
rice = NewFriedEggDecorator(rice)
rice.Eat()
// 麪條加份火腿
noodle = NewHamSausageDecorator(noodle)
noodle.Eat()
// 米飯再分別加個煎蛋和一份老乾媽
rice = NewFriedEggDecorator(rice)
rice = NewLaoGanMaDecorator(rice)
rice.Eat()
}
2.2 增強函數實現
下面提供另一種閉包實現裝飾增強函數的實現示例,其實現也是遵循着裝飾器模式的思路,但在形式上會更加簡潔直觀一些:
type handleFunc func(ctx context.Context, param map[string]interface{}) error
func Decorate(fn handleFunc) handleFunc {
return func(ctx context.Context, param map[string]interface{}) error {
// 前處理
fmt.Println("preprocess...")
err := fn(ctx, param)
fmt.Println("postprocess...")
return err
}
}
其中核心的處理方法 handleFunc 對應的是裝飾器模式中的核心類,Decorate 增強方法對應的則是裝飾器類,每次在執行 Decorate 的過程中,都會在 handleFunc 前後增加的一些額外的附屬邏輯.
3 工程案例
爲了進一步加深理解,下面摘出一個實際項目中應用到裝飾器模式的使用案例和大家共同分析探討.
這裏給到的案例是 grpc-go 中對攔截器鏈 chainUnaryInterceptors 的實現.
grpc-go 的開源地址:https://github.com/grpc/grpc-go
下面走讀的源碼版本爲 Release 1.53.0
在 grpc-go 服務端模塊中,每次接收到來自客戶端的 grpc 請求,會根據請求的 path 映射到對應的 service 和 handler 進行執行邏輯的處理,但在真正調用 handler 之前,會先先經歷一輪對攔截器鏈 chainUnaryInterceptors 的遍歷調用,在這裏我們可以把 handler 理解爲裝飾器模式中的核心類,攔截器鏈中的每一個攔截器 unaryInterceptors 可以理解爲一個裝飾器.
下面我們來觀察一下其中具體的源碼細節.
首先,對於攔截器類 UnaryServerInterceptor,本身是一個函數的類型:
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
UnaryServerInterceptor 對應的幾個入參包括:
-
• ctx:golang 請求鏈路中的上下文,不贅述
-
• req:grpc 請求的入參
-
• info:grpc 業務服務 service
-
• handler:核心邏輯執行方法
其中核心邏輯執行方法 handler 對應的類型爲 UnaryHandler,入參爲 context 和 req,出參爲 resp 和 error
type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)
下面是生成攔截器鏈的方法 chainUnaryInterceptors. 該方法的入參是用戶定義好的一系列攔截器 interceptors,內部會按照順序對攔截器進行組裝,最終通過層層裝飾增強的方式,將整個執行鏈路壓縮成一個攔截器 UnaryServerInterceptor 的形式進行方法.
func chainUnaryInterceptors(interceptors []UnaryServerInterceptor) UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (interface{}, error) {
return interceptors[0](ctx, req, info, getChainUnaryHandler(interceptors, 0, info, handler))
}
}
func getChainUnaryHandler(interceptors []UnaryServerInterceptor, curr int, info *UnaryServerInfo, finalHandler UnaryHandler) UnaryHandler {
if curr == len(interceptors)-1 {
return finalHandler
}
return func(ctx context.Context, req interface{}) (interface{}, error) {
return interceptors[curr+1](ctx, req, info, getChainUnaryHandler(interceptors, curr+1, info, finalHandler))
}
}
在 chainUnaryInterceptors 方法中,閉包返回了一個對應於攔截器 UnaryServerInterceptor 類型的函數. 這個閉包函數內部的執行邏輯是,會調用攔截器列表 interceptors 當中的首個攔截器,並通過 getChainUnaryHandler 方法,依次使用下一枚攔截器對核心方法 handler 進行裝飾包裹,封裝形成一個新的 “handler” 供當前的攔截器使用.
在這個過程中,就體現了我們今天討論的裝飾器模式的設計思路. 核心業務處理方法 handler 對應的就是裝飾器模式中的核心類,每一輪通過攔截器 UnaryServerInterceptor 對 handler 進行增強的過程,對應的就是一次 “裝飾” 的步驟.
下面給出一個具體實現的裝飾器的代碼示例,可以看到其中在覈心方法 handler 前後分別執行了對應的附屬邏輯,起到了裝飾的效果.
var myInterceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
// 添加前處理...
fmt.Printf("interceptor preprocess, req: %+v\n", req)
resp, err = handler(ctx, req)
// 添加後處理...
fmt.Printf("interceptor postprocess, req: %+v\n", resp)
return
}
如果各位讀友們想了解更多關於 grpc-go 的內容,可以閱讀我之前發表的相關話題文章:
-
• grpc-go 服務端使用介紹及源碼分析
-
• grpc-go 客戶端源碼走讀
-
• 基於 etcd 實現 grpc 服務註冊與發現
4 總結
本期和大家交流了設計模式中的裝飾器模式. 裝飾器模式能夠動態地爲對象增加某種特定的附屬能力,相比於繼承模式顯得更加靈活,且符合開閉原則,可以作爲繼承模式的一種有效補充手段.
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/XG2G1O67o-p_u_MPj5N0oQ