こんにちは、KOUKIです。
この記事では、デザインパターンの一つであるFlyweightパターンについて紹介します。
<目次>
デザインパターンまとめ
シチュエーション
Flyweightパターンは、オブジェクトを使い回すことによりオブジェクトの生成コストを下げるパターンです。Goにはオブジェクトが存在しないので、構造体(インスタンス)が対象ですね。
オブジェクトの生成コストを下げる -> メモリの消費量を抑えられるので、良質なプログラムを書くことにつながります。
例えば、DBへのアクセスを例に考えてみましょう。
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 |
package main type DB struct { name string db *DB } func NewDB(name string) *DB { switch name { case "postgres": db, _ := sql.Open("postgres", "xxx") return &DB{"postgres", db} case "mysql": db, _ := sql.Open("mysql", "xxx") return &DB{"mysql", db} case "cache": db, _ := sql.Open("cache", "xxx") return &DB{"mysql", db} default: db, _ := sql.Open("postgres", "xxx") return &DB{"postgres", db} } } func (d *DB) ConnectToDB() bool { // DBのコネクト return true } func (d *DB) showData() { // DBの情報を取得して表示 } |
実際には、DBにアクセスしないので、中身は適当です。
DBにアクセスするためには、DBのインスタンス化が必要です。
1 2 3 4 5 6 7 |
func main() { // インスタンスを生成する psgl := NewDB("postgres") if psgl.ConnectToDB() { psgl.ShowData() } } |
DB構造体には、DBへの接続情報が入っており、たくさんインスタンスを生成するとそれだけメモリを食います。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func main() { // インスタンスを生成する psgl := NewDB("postgres") if psgl.ConnectToDB() { psgl.ShowData() } // インスタンスを生成していくとメモリをたくさん食う psgl2 := NewDB("postgres") psgl3 := NewDB("postgres") psgl4 := NewDB("postgres") psgl5 := NewDB("postgres") } |
このように各インスタンスが接続情報を保持するのではなく、共有化すれば、メモリの消費を抑えられます。
Flyweightパターンの実装
Flyweightパターンのコアは「インスタンスの生成コストを下げる」ことにあり、それによりメモリの消費を下げます。
そのための方法として、インスタンスの共有(再利用)ができるように、先ほどのソースコードを書き換えてみましょう。
poolを用意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import ( "database/sql" "sync" ) var ( pool map[string]*DB ) func init() { pool = make(map[string]*DB) } |
インスタンスを共有するためのpool(格納庫)を用意しました。keyにDB名、valueにコネクション情報を格納します。
インスタンス取得
poolにコネクション情報が存在しない場合は、NewDB(コンストラクタ)を呼び出してインスタンスを生成し、存在する場合は、poolからインスタンスを取得する処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 |
func GetDBInstance(name string) *DB { // poolからインスタンスを取得 if db, ok := pool[name]; ok { return db } // poolにない場合は、インスタンス化 db := NewDB(name) pool[name] = db return db } |
これで、インスタンスを共有することができるようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func main() { // インスタンスを生成する psgl := GetDBInstance("postgres") if psgl.ConnectToDB() { psgl.ShowData() } // poolに格納された情報を取得 psgl2 := GetDBInstance("postgres") psgl3 := GetDBInstance("postgres") psgl4 := GetDBInstance("postgres") psgl5 := GetDBInstance("postgres") } |
新しくインスタンスを生成しないので、メモリの消費を抑えることができます。
まとめ
もしかしたら、Flyweightパターンを実装する機会はあまりないかもしれません。しかし、メモリの消費を意識するプログラミングを行えることは、プログラミング上級者を目指す上で必要なことだと思うので知っていて損はないと思います。
それでは、また!
サンプル
本記事のサンプル
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 |
package main import ( "database/sql" ) var ( pool map[string]*DB ) func init() { pool = make(map[string]*DB) } type DB struct { name string db *sql.DB } func NewDB(name string) *DB { switch name { case "postgres": db, _ := sql.Open("postgres", "xxx") return &DB{"postgres", db} case "mysql": db, _ := sql.Open("mysql", "xxx") return &DB{"mysql", db} case "cache": db, _ := sql.Open("cache", "xxx") return &DB{"mysql", db} default: db, _ := sql.Open("postgres", "xxx") return &DB{"postgres", db} } } func (d *DB) ConnectToDB() bool { // DBのコネクト return true } func (d *DB) ShowData() { // DBの情報を取得して表示 } func GetDBInstance(name string) *DB { // poolからインスタンスを取得 if db, ok := pool[name]; ok { return db } // poolにない場合は、インスタンス化 db := NewDB(name) pool[name] = db return db } func main() { // インスタンスを生成する psgl := GetDBInstance("postgres") if psgl.ConnectToDB() { psgl.ShowData() } // poolに格納された情報を取得 // psgl2 := GetDBInstance("postgres") // psgl3 := GetDBInstance("postgres") // psgl4 := GetDBInstance("postgres") // psgl5 := GetDBInstance("postgres") } |
他にも色々な実装例があるので、参考までに載せておきます。
同一名称を使い回す例
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 |
package main import ( "fmt" "strings" ) type User struct { FullName string } func NewUser(fullName string) *User { return &User{FullName: fullName} } var allNames []string type User2 struct { names []uint8 } func NewUser2(fullName string) *User2 { getOrAdd := func(s string) uint8 { for i := range allNames { if allNames[i] == s { return uint8(i) } } allNames = append(allNames, s) return uint8(len(allNames) - 1) } result := User2{} parts := strings.Split(fullName, " ") for _, p := range parts { result.names = append(result.names, getOrAdd(p)) } return &result } func (u *User2) FullName() string { var parts []string for _, id := range u.names { parts = append(parts, allNames[id]) } return strings.Join(parts, " ") } func main() { john := NewUser("John Doe") jane := NewUser("Jane Doe") alsoJane := NewUser("Jane Smith") fmt.Println("Memory taken by users:", len([]byte(john.FullName))+ len([]byte(alsoJane.FullName))+ len([]byte(jane.FullName))) john2 := NewUser2("John Doe") jane2 := NewUser2("Jane Doe") alsoJane2 := NewUser2("Jane Smith") totalMem := 0 for _, name := range allNames { totalMem += len([]byte(name)) } totalMem += len(john2.names) totalMem += len(jane2.names) totalMem += len(alsoJane2.names) fmt.Println("Memory taken by users:", totalMem) } |
1 2 3 |
$ go run main.go Memory taken by users: 26 Memory taken by users: 22 |
テキスト文字列を使い回す例
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 ( "fmt" "strings" "unicode" ) type FormattedText struct { plainText string capitalize []bool } func (f *FormattedText) String() string { sb := strings.Builder{} for i := 0; i < len(f.plainText); i++ { c := f.plainText[i] if f.capitalize[i] { sb.WriteRune(unicode.ToUpper(rune(c))) } else { sb.WriteRune(rune(c)) } } return sb.String() } func (f *FormattedText) Capitalize(start, end int) { for i := start; i <= end; i++ { f.capitalize[i] = true } } func NewFormattedText(plainText string) *FormattedText { return &FormattedText{ plainText: plainText, capitalize: make([]bool, len(plainText)), } } type TextRange struct { Start, End int Capitalize, Bold, Italic bool } func (t *TextRange) Covers(position int) bool { return position >= t.Start && position <= t.End } type BetterFormattedText struct { plainText string formatting []*TextRange } func (b *BetterFormattedText) String() string { sb := strings.Builder{} for i := 0; i < len(b.plainText); i++ { c := b.plainText[i] for _, r := range b.formatting { if r.Covers(i) && r.Capitalize { c = uint8(unicode.ToUpper(rune(c))) } } sb.WriteRune(rune(c)) } return sb.String() } func NewBetterFormattedText(plainText string) *BetterFormattedText { return &BetterFormattedText{plainText: plainText} } func (b *BetterFormattedText) Range(start, end int) *TextRange { r := &TextRange{start, end, false, false, false} b.formatting = append(b.formatting, r) return r } func main() { text := "This is a brave new world" ft := NewFormattedText(text) ft.Capitalize(10, 15) fmt.Println(ft.String()) fmt.Println("==============================") bft := NewBetterFormattedText(text) bft.Range(16, 19).Capitalize = true fmt.Println(bft.String()) } |
1 2 3 4 |
$ go run sample/main.go This is a BRAVE new world ============================== This is a brave NEW world |
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 |
package main import ( "database/sql" ) var ( pool map[string]*DB ) func init() { pool = make(map[string]*DB) } type DB struct { name string db *sql.DB } func NewDB(name string) *DB { switch name { case "postgres": db, _ := sql.Open("postgres", "xxx") return &DB{"postgres", db} case "mysql": db, _ := sql.Open("mysql", "xxx") return &DB{"mysql", db} case "cache": db, _ := sql.Open("cache", "xxx") return &DB{"mysql", db} default: db, _ := sql.Open("postgres", "xxx") return &DB{"postgres", db} } } func (d *DB) ConnectToDB() bool { // DBのコネクト return true } func (d *DB) ShowData() { // DBの情報を取得して表示 } func GetDBInstance(name string) *DB { // poolからインスタンスを取得 if db, ok := pool[name]; ok { return db } // poolにない場合は、インスタンス化 db := NewDB(name) pool[name] = db return db } func main() { // インスタンスを生成する psgl := GetDBInstance("postgres") if psgl.ConnectToDB() { psgl.ShowData() } // poolに格納された情報を取得 // psgl2 := GetDBInstance("postgres") // psgl3 := GetDBInstance("postgres") // psgl4 := GetDBInstance("postgres") // psgl5 := GetDBInstance("postgres") } |
コメントを残す
コメントを投稿するにはログインしてください。