こんにちは、KOUKIです。
この記事では、デザインパターンの一つであるObserverパターンについて紹介します。
<目次>
デザインパターンまとめ
シチュエーション
Observer は、「観察者」を意味する英単語です。
Observer パターンとは、インスタンスの変化(例えば、リストが追加されるなど)を観察することを目的としたもので、インスタンスの観察(どちらかというと通知?)を得意とするパターンです。
Observerパターンの実装イメージは、Pub/Subサービスです。Pub/Sub サービスとは、メッセージの送信者とメッセージの受信者を切り離す方式のメッセージ サービスのことを指します。

Observerパターンの適用
簡単なサンプルコードを実装して、Observerパターンについて学習しましょう!
Observer and Observable
Observerインターフェース
いきなりですが、Observerパターンのコアの部分を実装します。
1 2 3 |
type Observer interface { Notify(data interface{}) } |
Observable構造体
Subscriptionを担う構造体を実装します。
1 2 3 4 5 6 7 |
package main import "container/list" type Observable struct { subs *list.List } |
人生で初めて使ったのですが、container/listパッケージのList構造体が結構便利です。
このObservable構造体は、以下のメソッドを持ちます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 登録 func (o *Observable) Subscribe(observer Observer) { o.subs.PushBack(observer) } // 削除 func (o *Observable) Unsubscribe(observer Observer) { // リストの最初の要素を取り出し、nil出ない場合はループ。 // Nextで次のリスト要素を取得する for sub := o.subs.Front(); sub != nil; sub = sub.Next() { // 対象のobserverかチェック(その際に型変換) if sub.Value.(Observer) == observer { o.subs.Remove(sub) } } } // 発火 // どんなデータでも大丈夫なように引数のタイプをinterface func (o *Observable) Fire(data interface{}) { for sub := o.subs.Front(); sub != nil; sub = sub.Next() { sub.Value.(Observer).Notify(data) } } |
Publisher構造体
次に、Publisher構造体を定義しましょう。
1 2 3 4 5 6 7 8 9 10 11 |
type Publisher struct { Observable Title string } func NewPublisher(title string) *Publisher { return &Publisher{ Observable: Observable{subs: new(list.List)}, Title: title, } } |
Observableを含めるところがポイントです。
これで、Subscriptionが使えます。
Publisher構造体には、次のメソッドを実装します。
1 2 3 |
func (p *Publisher) Topic() { p.Fire(p.Title) } |
サービスの実装
Pub/Subサービスで配信するサービスをいくつか実装してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// サービス type BookService struct{} type BlogService struct{} type MovieService struct{} func (b *BookService) Notify(data interface{}) { fmt.Printf("This book title is %s\n", data.(string)) } func (b *BlogService) Notify(data interface{}) { fmt.Printf("This Blog title is %s\n", data.(string)) } func (b *MovieService) Notify(data interface{}) { fmt.Printf("This Movie title is %s\n", data.(string)) } |
Observerインターフェースを実装したサービス構造体を用意することがポイントです。
※Go言語では、インターフェースに定義したパラメータ(Notify)をメソッドとして実装すれば、インターフェースを実装したとみなします
使ってみよう
ここまで実装したコードを使ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func main() { p := NewPublisher("Harry Potter") bookService := &BookService{} blogService := &BlogService{} MovieService := &MovieService{} // 登録 p.Subscribe(bookService) p.Subscribe(blogService) p.Subscribe(MovieService) p.Topic() fmt.Println("-------------") // 解除 p.Unsubscribe(bookService) p.Topic() } |
プログラムを実行します。
1 2 3 4 5 6 7 |
$ go run main.go This book title is Harry Potter This Blog title is Harry Potter This Movie title is Harry Potter ------------- This Blog title is Harry Potter This Movie title is Harry Potter |
OKですね。
Property Observers
ある条件下において、Subscribeしたくない場合もあります。それを制御する構造体を実装しましょう。
PropertyChange構造体
1 2 3 4 |
type PropertyChange struct { Key string Value interface{} } |
このKeyには条件の名称を、Valueには実際の値を格納します。
Publisher構造体の修正
Publisher構造体に、対象年齢を示す「age」プロパティを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type Publisher struct { Observable Title string age int // 対象年齢 } func NewPublisher(title string) *Publisher { return &Publisher{ Observable: Observable{subs: new(list.List)}, Title: title, age: 30, } } |
Publisherに対象年齢を付けました。
この構造体に、Getter/Setterを実装します。
1 2 3 4 5 6 7 8 |
func (p *Publisher) GetAge() int { return p.age } func (p *Publisher) SetAge(age int) { if age == p.age { return } p.age = age p.Fire(PropertyChange{"Age", p.age}) } |
サービスの実装2
以下のサービスを実装しましょう。
1 2 3 |
type AdultService struct { o Observable } |
先ほどと違って、プロパティにObservableを持ちました。
このサービスのNotify処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func (t *AdultService) Notify(data interface{}) { switch data.(type) { case PropertyChange: if pc, ok := data.(PropertyChange); ok { if pc.Value.(int) >= 20 { fmt.Println("this book is reading ok!") } else { fmt.Println("this book is for more than 20 age.") t.o.Unsubscribe(t) } } case string: fmt.Printf("This book title is %s\n", data.(string)) } } |
今回は、TopicメソッドとSetAgeメソッドからFire -> Notifyメソッドを呼ぶことになるので、switch文でcase分けしています。
AdultServiceは、20歳以上の大人限定のサービスなので、もし年齢がそれに満たない場合は、Unsubscribeしています。
使ってみよう2
ここまで実装したコードを使ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
func main() { p := NewPublisher("XXX_YYY_ZZZ") adultService := &AdultService{p.Observable} p.Subscribe(adultService) for age := 22; age > 18; age-- { fmt.Println("Setting the age to", age) p.SetAge(age) p.Topic() fmt.Println() } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ go run main.go Setting the age to 22 this book is reading ok! This book title is XXX_YYY_ZZZ Setting the age to 21 this book is reading ok! This book title is XXX_YYY_ZZZ Setting the age to 20 this book is reading ok! This book title is XXX_YYY_ZZZ Setting the age to 19 this book is for more than 20 age. |
OKですね。
Property Dependencies
PropertyChange構造体は結構便利です。
以下のように実装することもできます。
年齢制限チェク用のメソッドを実装
1 2 3 |
func (p *Publisher) CanSubscribe() bool { return p.age >= 20 } |
SetAgeメソッドの修正
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func (p *Publisher) SetAge(age int) { if age == p.age { return } // 年齢チェク oldCanSubscribe := p.CanSubscribe() p.age = age // p.Fire(PropertyChange{"Age", p.age}) if oldCanSubscribe != p.CanSubscribe() { p.Fire(PropertyChange{"CanSubscribe", p.CanSubscribe()}) } } |
Notifyメソッドを修正
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func (t *AdultService) Notify(data interface{}) { switch data.(type) { case PropertyChange: if pc, ok := data.(PropertyChange); ok { if pc.Key == "CanSubscribe" && pc.Value.(bool) { fmt.Println("this book is reading ok!") } else if pc.Key == "Age" { // do nothing... } else { fmt.Println("this book is for more than 20 age.") t.o.Unsubscribe(t) } } case string: fmt.Printf("This book title is %s\n", data.(string)) } } |
使ってみよう3
1 2 3 4 5 6 7 8 9 10 11 12 |
func main() { p := NewPublisher("XXX_YYY_ZZZ") adultService := &AdultService{p.Observable} p.Subscribe(adultService) for age := 22; age > 18; age-- { fmt.Println("Setting the age to", age) p.SetAge(age) p.Topic() fmt.Println() } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
$ go run main.go Setting the age to 22 This book title is XXX_YYY_ZZZ Setting the age to 21 This book title is XXX_YYY_ZZZ Setting the age to 20 This book title is XXX_YYY_ZZZ Setting the age to 19 this book is for more than 20 age. |
OKですね。
まとめ
Observerパターンは、コンテンツ配信サーバーなどの自作サーバーを作る時に活躍してくれそうなパターンですね。
しかし、正しく理解して実装しないと、けっこうなバグを生み出すプログラムを書きそうで怖くもあります^^;
精進あるのみ! ですね。
それでは、また!
Go言語まとめ
ソースコード
Observer and Observable
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 91 92 |
package main import ( "container/list" "fmt" ) type Observer interface { Notify(data interface{}) } type Observable struct { subs *list.List } // 登録 func (o *Observable) Subscribe(observer Observer) { o.subs.PushBack(observer) } // 削除 func (o *Observable) Unsubscribe(observer Observer) { // リストの最初の要素を取り出し、nil出ない場合はループ。 // Nextで次のリスト要素を取得する for sub := o.subs.Front(); sub != nil; sub = sub.Next() { // 対象のobserverかチェック(その際に型変換) if sub.Value.(Observer) == observer { o.subs.Remove(sub) } } } // 発火 // どんなデータでも大丈夫なように引数のタイプをinterface func (o *Observable) Fire(data interface{}) { for sub := o.subs.Front(); sub != nil; sub = sub.Next() { sub.Value.(Observer).Notify(data) } } type Publisher struct { Observable Title string } func NewPublisher(title string) *Publisher { return &Publisher{ Observable: Observable{subs: new(list.List)}, Title: title, } } func (p *Publisher) Topic() { p.Fire(p.Title) } // サービス type BookService struct{} type BlogService struct{} type MovieService struct{} func (b *BookService) Notify(data interface{}) { fmt.Printf("This book title is %s\n", data.(string)) } func (b *BlogService) Notify(data interface{}) { fmt.Printf("This Blog title is %s\n", data.(string)) } func (b *MovieService) Notify(data interface{}) { fmt.Printf("This Movie title is %s\n", data.(string)) } func main() { p := NewPublisher("Harry Potter") bookService := &BookService{} blogService := &BlogService{} MovieService := &MovieService{} // 登録 p.Subscribe(bookService) p.Subscribe(blogService) p.Subscribe(MovieService) p.Topic() fmt.Println("-------------") // 解除 p.Unsubscribe(bookService) p.Topic() } |
Property Observers
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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
package main import ( "container/list" "fmt" ) type Observer interface { Notify(data interface{}) } type Observable struct { subs *list.List } // 登録 func (o *Observable) Subscribe(observer Observer) { o.subs.PushBack(observer) } // 削除 func (o *Observable) Unsubscribe(observer Observer) { // リストの最初の要素を取り出し、nil出ない場合はループ。 // Nextで次のリスト要素を取得する for sub := o.subs.Front(); sub != nil; sub = sub.Next() { // 対象のobserverかチェック(その際に型変換) if sub.Value.(Observer) == observer { o.subs.Remove(sub) } } } // 発火 // どんなデータでも大丈夫なように引数のタイプをinterface func (o *Observable) Fire(data interface{}) { for sub := o.subs.Front(); sub != nil; sub = sub.Next() { sub.Value.(Observer).Notify(data) } } type PropertyChange struct { Key string Value interface{} } type Publisher struct { Observable Title string age int } func NewPublisher(title string) *Publisher { return &Publisher{ Observable: Observable{subs: new(list.List)}, Title: title, age: 30, } } func (p *Publisher) Topic() { p.Fire(p.Title) } func (p *Publisher) GetAge() int { return p.age } func (p *Publisher) SetAge(age int) { if age == p.age { return } p.age = age p.Fire(PropertyChange{"Age", p.age}) } type AdultService struct { o Observable } func (t *AdultService) Notify(data interface{}) { switch data.(type) { case PropertyChange: if pc, ok := data.(PropertyChange); ok { if pc.Value.(int) >= 20 { fmt.Println("this book is reading ok!") } else { fmt.Println("this book is for more than 20 age.") t.o.Unsubscribe(t) } } case string: fmt.Printf("This book title is %s\n", data.(string)) } } func main() { p := NewPublisher("XXX_YYY_ZZZ") adultService := &AdultService{p.Observable} p.Subscribe(adultService) for age := 22; age > 18; age-- { fmt.Println("Setting the age to", age) p.SetAge(age) p.Topic() fmt.Println() } } |
Property Dependencies
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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
package main import ( "container/list" "fmt" ) type Observer interface { Notify(data interface{}) } type Observable struct { subs *list.List } // 登録 func (o *Observable) Subscribe(observer Observer) { o.subs.PushBack(observer) } // 削除 func (o *Observable) Unsubscribe(observer Observer) { // リストの最初の要素を取り出し、nil出ない場合はループ。 // Nextで次のリスト要素を取得する for sub := o.subs.Front(); sub != nil; sub = sub.Next() { // 対象のobserverかチェック(その際に型変換) if sub.Value.(Observer) == observer { o.subs.Remove(sub) } } } // 発火 // どんなデータでも大丈夫なように引数のタイプをinterface func (o *Observable) Fire(data interface{}) { for sub := o.subs.Front(); sub != nil; sub = sub.Next() { sub.Value.(Observer).Notify(data) } } type PropertyChange struct { Key string Value interface{} } type Publisher struct { Observable Title string age int } func NewPublisher(title string) *Publisher { return &Publisher{ Observable: Observable{subs: new(list.List)}, Title: title, age: 30, } } func (p *Publisher) Topic() { p.Fire(p.Title) } func (p *Publisher) GetAge() int { return p.age } func (p *Publisher) SetAge(age int) { if age == p.age { return } // 年齢チェク oldCanSubscribe := p.CanSubscribe() p.age = age // p.Fire(PropertyChange{"Age", p.age}) if oldCanSubscribe != p.CanSubscribe() { p.Fire(PropertyChange{"CanSubscribe", p.CanSubscribe()}) } } func (p *Publisher) CanSubscribe() bool { return p.age >= 20 } type AdultService struct { o Observable } func (t *AdultService) Notify(data interface{}) { switch data.(type) { case PropertyChange: if pc, ok := data.(PropertyChange); ok { if pc.Key == "CanSubscribe" && pc.Value.(bool) { fmt.Println("this book is reading ok!") } else if pc.Key == "Age" { // do nothing... } else { fmt.Println("this book is for more than 20 age.") t.o.Unsubscribe(t) } } case string: fmt.Printf("This book title is %s\n", data.(string)) } } func main() { p := NewPublisher("XXX_YYY_ZZZ") adultService := &AdultService{p.Observable} p.Subscribe(adultService) for age := 22; age > 18; age-- { fmt.Println("Setting the age to", age) p.SetAge(age) p.Topic() fmt.Println() } } |
コメントを残す
コメントを投稿するにはログインしてください。