設計模式 in Go: Decorator

結構模式關注代碼、組件以及接口的有效組織與調用,主要解決如對象間的關係管理、提供必要抽象與具體實現相分離,以及將多個不同的庫或框架集成爲一個統一且連貫的系統等問題。

今天我們開始第 4 個結構模式的學習 —— Decorator(裝飾器)。

問題背景:

我們希望在運行時增強或修改特定對象的功能。由於對象存在一定的類繼承關係,所以開發者可能首先想到通過修改基類的功能來實現,以希望這些修改可以同時在所有派生類生效。嗯,確實必逐一修改所有派生類對象要簡單。但是基於繼承的實現還有一些注意事項,你必須瞭解:

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