設計模式 in Go: Decorator
結構模式關注代碼、組件以及接口的有效組織與調用,主要解決如對象間的關係管理、提供必要抽象與具體實現相分離,以及將多個不同的庫或框架集成爲一個統一且連貫的系統等問題。
今天我們開始第 4 個結構模式的學習 —— Decorator(裝飾器)。
問題背景:
我們希望在運行時增強或修改特定對象的功能。由於對象存在一定的類繼承關係,所以開發者可能首先想到通過修改基類的功能來實現,以希望這些修改可以同時在所有派生類生效。嗯,確實必逐一修改所有派生類對象要簡單。但是基於繼承的實現還有一些注意事項,你必須瞭解:
-
繼承是靜態的。你不能在運行時修改現有對象的行爲(繼承層次是編譯時確定的,運行時不能修改)。你只能用另一個從不同子類創建的對象替換整個子類對象。So,如果希望運行時互換,還是得添加子類實現,當心子類爆炸。
-
子類只有一個父類。大多數語言中,繼承不允許一個類同時繼承多個類的行爲。(C++ 支持多重繼承,而大多數語言不支持)。這在希望組合多個對象的能力時會受限。
-
而且,這些待增強的對象可能不是那麼方便被修改,比如是第三方維護的,或者這些代碼已經漸漸成爲累贅,不再想投入更多人力、時間維護;
ps:這裏必須強調一下,設計模式是爲了降低解決問題複雜度而提煉的經過實踐檢驗的解決方案,絕不是無病呻吟、沒有問題製造問題、讓問題變複雜。如果你們的項目中不存在前述的這些顧慮,則不一定非要生搬硬套某個設計模式。So,首先你得識別,項目的複雜性是什麼,是否要引入對應的解決方案來解決它。
聯想下適配器模式,該模式封裝了一個實現對象的引用,在調用被引用對象的方法之前可以做一些事情。我們仍然可以在這種 “wrapper” 方式的基礎上來解決這個問題。
解決方案:
如果你想在運行時增強或修改對象的功能,可以將它們包裹在裝飾器對象中。被包裹的對象和裝飾器應該具有相同的接口,以便它們可以互換。
定義一個與待增強對象實現了相同接口的裝飾器類,或者定義裝飾器基類,然後每個具體的裝飾器類繼承這個基類。裝飾器類在其方法中添加額外的功能。裝飾器保持對被包裹對象的引用,在執行了自己的行爲(功能增強的動作)之後,將後續操作委託給該對象。
decorator pattern
假設我們在 Medium 平臺上發佈一篇文章,平臺可能會檢查文章中是否有拼寫錯誤、髒話、仇恨言論等。現在有一個人編寫了一個模塊 articleProcessor
來檢查拼寫錯誤和髒話。現在我們發現還需要檢查仇恨言論。但是我們不能直接修改 articleProcessor
代碼。
我們可以定義一個 nohateArticleProcessor,它持有對 articleProcessor 的引用。但在調用 articleProcessor.Process(...) 之前,我們首先調用自己的增強代碼來檢查仇恨言論。
ps: 裝飾器模式也被稱爲包裝器模式。你應該意識到適配器模式也稱爲包裝器模式。不過,wrapper 只是實現的一種方法,你可以通過其要實現的功能或目標來區分這兩種模式。
裝飾器模式變體:
-
多個裝飾器可以包裹一個對象以組合多種功能,它們可以遞歸地應用。
-
裝飾器可以是抽象類而不是具體類,允許在抽象類基礎上的自定義實現。
與其他模式關係:
裝飾器模式是一種擴展目標對象功能的替代方案,無需通過修改或增加子類的方式。它提供了更大的靈活性,因爲可以獨立創建新的裝飾器而不修改原始類。當可能有多個功能擴展時,它可以避免因組合子類而導致的子類數量爆炸問題。
示例代碼:
//////////////////////// processor.go //////////////////////////
package decorator
// Supposing you want to submit an article to media, after submiting
// their system may process your article step by step.
//
// For example, check dirty words, check typo, check hate words, ...
// Here, we define Processor to represent any processing step.
type Processor interface {
Process([]byte) ([]byte, error)
}
type articleProcessor struct {
processors []Processor
opts Options
}
func (p articleProcessor) Process(dat []byte) ([]byte, error) {
for _, pp := range p.processors {
v, err := pp.Process(dat)
if err != nil {
return nil, err
}
dat = v
}
return dat, nil
}
type Options struct {
checkTypo bool
checkDirtyWords bool
}
func NewArticleProcessor(opt Options) Processor {
p := articleProcessor{
processors: []Processor{},
}
if opt.checkTypo {
p.processors = append(p.processors, checkTypoProcessor{})
}
if opt.checkDirtyWords {
p.processors = append(p.processors, checkDirtyWordsProcessor{})
}
return p
}
type checkTypoProcessor struct{}
func (p checkTypoProcessor) Process(dat []byte) ([]byte, error) {
return dat, nil
}
type checkDirtyWordsProcessor struct{}
func (p checkDirtyWordsProcessor) Process(dat []byte) ([]byte, error) {
return dat, nil
}
// well, here there's no check hate words processor, which will be defined
// when we want to decorate our articleProcessor in nohateArticleProcessor.
////////////////////////// processor_nohate.go //////////////////////////
package decorator
import (
"bytes"
"errors"
)
type nohateArticleProcessor struct {
base Processor
}
func NewNoHateArticleProcessor(opts Options) Processor {
return &nohateArticleProcessor{
base: NewArticleProcessor(opts),
}
}
func (p nohateArticleProcessor) Process(dat []byte) ([]byte, error) {
// decorate this normal article processor, we add checking hatewords logic,
//
// nohateArticleProcessor and ArticleProcessor are likely to be defined
// in different modules or packages, we cannot change ArticleProcessor's
// implementation, so we use decorate pattern to augment it's behavior.
if p.hasHateWords(dat) {
return nil, errors.New("contains hate words")
}
return p.base.Process(dat)
}
func (p nohateArticleProcessor) hasHateWords(dat []byte) bool {
if bytes.Contains(dat, []byte("hate")) {
return true
}
if bytes.Contains(dat, []byte("kill")) {
return true
}
return false
}
Here’s the tests:
////////////////////////// decorator_test.go ///////////////////////////
package decorator
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestArticleProcesso(t *testing.T) {
var content = "hello world, some hate words here"
// original processor
opts := Options{true, true}
p1 := NewArticleProcessor(opts)
_, err := p1.Process([]byte(content))
require.Nil(t, err)
// augmented processor
p2 := NewNoHateArticleProcessor(opts)
_, err = p2.Process([]byte(content))
require.NotNil(t, err)
}
ps:此處我們優先考慮可讀性,不會太關注編碼標準,如註釋、camelCase 類型名等。我們將多個文件的代碼組織到一個 codeblock 中僅僅是爲了方便閱讀,如果您想測試可以通過 git 下載源碼 github.com/hitzhangjie/go-patterns。
在這篇文章中,我們描述了何時、爲何以及如何使用裝飾器模式。最關鍵的一點是,裝飾器符合原始對象的接口,因此可以透明地互換。這允許在運行時不用引入額外的複雜性即可添加多種行爲組合,該模式擴展對象而不是靜態類。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/IjmOpkfX9PPfYHNOPJfwiA