こんにちは、KOUKIです。
この記事では、デザインパターンの一つであるDecoratorパターンについて紹介します。
ソースコードは、以下のYouTube動画を参考にしています。
<目次>
デザインパターンまとめ
シチュエーション
スポンジケーキの上に、生クリームが乗っていて、さらにその上に美味しそうなイチゴがデコレーションされているショートケーキをイメージしてください。

これをプログラムの観点から観察すると、元々の機能(スポンジケーキ)に新しい機能(生クリームやいちご)が実装されている、と解釈することができると思います。
この様に、既存の機能に何か新しい機能を実装したい場合、Decoratorパターンを検討しましょう。
Decoratorパターン
装飾元となるプログラムを実装してからDecoratorパターンを適用します。
円周率計算プログラム
円周率を計算するプログラムを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package main import ( "fmt" "math" ) func Pi(n int) float64 { ch := make(chan float64) for i := 0; i <= n; i++ { go func(ch chan float64, i float64) { ch <- 4 * math.Pow(-1, i) / (2*i + 1) }(ch, float64(i)) } result := 0.0 for k := 0; k <= n; k++ { result += <-ch } return result } func main() { fmt.Println(Pi(1000)) fmt.Println(Pi(10000)) } |
このPi関数は、ショートケーキの例で言うところのスポンジです。
プログラムを実行してみましょう。
1 2 3 |
$ go run main.go 3.1425916543395416 3.141692643590534 |
Decorator ~ ロガー関数 ~
このプログラムが完了するまでに、どの程度の時間かかったかロガー(Decorator)を実装して算出してみましょう。
typeの定義
まず、typeを定義してください。
1 |
type piFunc func(int) float64 |
これは、Pi関数のパラメータ/戻り値と同じ型です。
1 |
Pi(n int) float64 {...} |
Decoratorパターンを実装する上で必須ではありませんが、typeを定義するとわかりやすいプログラムを実装できます。
デザインパターンの適用
以下に実装する関数がDecoratorです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func wrapLogger(do piFunc, logger *log.Logger) piFunc { return func(n int) float64 { fn := func(n int) (result float64) { defer func(t time.Time) { logger.Printf( "took=%v, n=%v, result=%v", time.Since(t), n, result) }(time.Now()) return do(n) } return fn(n) } } |
引数にはデコレーション元(Pi関数)とロガーを渡し、以下の様に呼び出します。
1 2 3 4 |
func main() { f := wrapLogger(Pi, log.New(os.Stdout, "test ", 1)) f(10000) } |
1 2 |
$ go run main.go test 2021/05/12 took=20.827182ms, n=10000, result=3.1416926435905315 |
変数fには、piFunc typeの
が格納されています。fn := func(n int) (result float64)
そして、この無名関数内で「タイムスタンプ + do(Pi関数)」を定義することで、元々の機能(Pi関数)とロガー機能(wrapLogger)をデコレーションすることができます。
補足ですが、timeスタンプを取るとき、deferキーワードを使っています。こうすれば、do(Pi関数)の実行後にtimestampを取得することができます。
1 2 3 4 5 6 7 |
defer func(t time.Time) { logger.Printf( "took=%v, n=%v, result=%v", time.Since(t), n, result) }(time.Now()) |
Decoratorは、ミドルウェアを実装するとき重宝しそうですね。
Decorator ~ キャッシュ ~
更に理解を深めるために、別のDecoratorを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func wrapCache(do piFunc, cache *sync.Map) piFunc { return func(n int) float64 { fn := func(n int) float64 { key := fmt.Sprintf("n=%d", n) val, ok := cache.Load(key) if ok { return val.(float64) } result := do(n) cache.Store(key, result) return result } return fn(n) } } |
wrapCache関数は、sync.Mapを使ってキャッシュの仕組みを提供しています。
do関数の実行前に、sync.Mapからキャッシュの存在を確認し、存在していない場合は保存 + do関数実行、存在する場合はキャッシュからデータを返します。
do関数の実行 + キャッシュの保存がなくなる分、2回目以降の実行が早くなるはずです。
1 2 3 4 5 6 7 8 9 |
func main() { c := wrapCache(Pi, &sync.Map{}) g := wrapLogger(c, log.New(os.Stdout, "test ", 1)) g(10000) g(333) // キャッシュが残るのでめちゃくちゃ早くなる g(10000) g(333) } |
1 2 3 4 5 6 |
$ go run main.go test 2021/05/13 took=22.271334ms, n=10000, result=3.1416926435905332 test 2021/05/13 took=337.969µs, n=333, result=3.138598648323333 // 2回目 test 2021/05/13 took=999ns, n=10000, result=3.1416926435905332 test 2021/05/13 took=531ns, n=333, result=3.138598648323333 |
だいぶ早くなりましたね。
現場でもプログラムの速度を上げるためにキャッシュを実装するので、テクニックとして覚えておいて損はないと思います。
Decoratorのメリット
Decoratorは、再利用がしやすいです。
例えば、Pi関数と同じパラメータ/戻り値の関数を定義したとします。
1 2 3 |
func Divide(n int) float64 { return float64(n / 2) } |
この関数に、これまで実装したデコレーターを適用することが可能です。
1 2 3 4 5 6 7 8 |
func main() { ... c = wrapCache(Divide, &sync.Map{}) g = wrapLogger(c, log.New(os.Stdout, "divide ", 1)) g(10000) g(333) g(10000) } |
1 2 3 4 5 |
$ go run main.go ... divide 2021/05/12 took=2.217µs, n=10000, result=5000 divide 2021/05/12 took=9.708µs, n=333, result=166 divide 2021/05/12 took=720ns, n=10000, result=5000 |
OKですね。覚えるとめちゃくちゃ便利なパターンです。
まとめ
恐らく、Decoratorパターンを実装する上で大切なのは、型だと思います。
1 |
type piFunc func(int) float64 |
再利用性を考えると、ここに定義する型はなるべく単純なものにしておくべきです。パラメータが簡単だとテストもしやすいですしね^^
Decoratorパターンは、使いこなすと強力なツールになりそうなので、ガンガン実装していきたいと思います!
それでは、また!
Go言語まとめ
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
package main import ( "fmt" "log" "math" "os" "sync" "time" ) // Decoratorの型 type piFunc func(int) float64 // Decorator ~ ロガー ~ func wrapLogger(do piFunc, logger *log.Logger) piFunc { return func(n int) float64 { fn := func(n int) (result float64) { // deferで定義することでdo実行後にtimestampが取得可能 defer func(t time.Time) { logger.Printf( "took=%v, n=%v, result=%v", time.Since(t), n, result) }(time.Now()) return do(n) } // 戻り値をpiFuncにすること return fn(n) } } // Decorator ~ キャッシュ ~ func wrapCache(do piFunc, cache *sync.Map) piFunc { return func(n int) float64 { fn := func(n int) float64 { key := fmt.Sprintf("n=%d", n) val, ok := cache.Load(key) if ok { return val.(float64) } result := do(n) cache.Store(key, result) return result } return fn(n) } } // Pi計算 func Pi(n int) float64 { ch := make(chan float64) for i := 0; i <= n; i++ { go func(ch chan float64, i float64) { ch <- 4 * math.Pow(-1, i) / (2*i + 1) }(ch, float64(i)) } result := 0.0 for k := 0; k <= n; k++ { result += <-ch } return result } // 割り算計算 func Divide(n int) float64 { return float64(n / 2) } func main() { c := wrapCache(Pi, &sync.Map{}) g := wrapLogger(c, log.New(os.Stdout, "test ", 1)) g(10000) g(333) // キャッシュが残るので、2回目以降は早くなる g(10000) g(333) c = wrapCache(Divide, &sync.Map{}) g = wrapLogger(c, log.New(os.Stdout, "divide ", 1)) g(10000) g(333) g(10000) } |
コメントを残す
コメントを投稿するにはログインしてください。