Design Pattern in Go
全体通じての参考資料
GolangにおけるDesign Pattern
Design Patternはオブジェクト指向言語をベースに考えられたものだが、GoはOOPではない。
(No Ingeritance, Weak Encapsulation)
そのため、OOPを前提とした言語と比べると少し実装の仕方が異なる。
Solid Principles
Overview
- Robert C.Martinによって導入された設計原則
- 当初はC#を使った書籍で説明されたが、原則自体はユニバーサルで言語に関わらず役に立つもの
- Design Patternの中でSOLID原則が参照されることが多い
Single Responsibility Principles
Type(Object?)は一つの責務だけを持つべきであり、変更される理由は一つだけであるべきというもの。
Separation of Concerns
が関連している。
Separation of Converns
のアンチパターンはGod objectで全て(異なる関心事を扱っている処理)のコードが一つのパッケージに収められていること。
例えば以下のようにJournal(日誌)を管理するアプリケーションを作ったとする。
基本的な機能としてJournalに日誌を登録したり、削除したりする機能をまずは追加する。
var entryCount = 0
type Journal struct {
entries []string
}
func (j *Journal) String() string {
return strings.Join(j.entries, "\n")
}
func (j *Journal) AddEntry(text string) int {
entryCount++
entry := fmt.Sprintf("%d: %s", entryCount, text)
j.entries = append(j.entries, entry)
return entryCount
}
func (j *Journal) RemoveEntry(index int) {
// ...
}
SRPを破るのは非常に簡単。
例えばJournalの内容を永続的に保持したくなったとする。
以下のようにFileに保存したりWeb上から読み込む処理を追加した時、SRPの原則は破られてしまう。
JournalのResponsibilityはJournalの内容を管理することにあり、それを永続的に保持することではない。
永続的に保持する部分を別のComponentとして持つことで、再利用性や変更時の影響範囲を限定することができる?
func (j *Journal) Save(filename string) {
_ = ioutil.WriteFile(filename, []byte(j.String()), 0644)
}
func (j *Journal) Load(filename string) {
//
}
func (j *Journal) LoadFromWeb(url *url.URL) {
//
}
SRPを守った実装
関数として切り出すか、Structで定義を分けることでJournalのSRP原則を守ったまま機能を追加することができる。
// 関数を切り出すパターン
var LineSeparator = "\n"
func SaveToFile(j *Journal, filename string) {
_ = ioutil.WriteFile(filename, []byte(strings.Join(j.entries, LineSeparator)), 0644)
}
// Structで定義するパターン
type Persistence struct {
lineSeparator string
}
func (p *Persistence) SaveToFile(j *Journal, filename string) {
_ = ioutil.WriteFile(filename, []byte(strings.Join(j.entries, p.lineSeparator)), 0644)
}
実際に呼び出すときは実装のパターンによって以下のように呼び出すことができる。
func main() {
j := Journal{}
j.AddEntry("牛乳を買った")
j.AddEntry("ゴルフに行った")
fmt.Println(j.String())
// 関数で切り出す場合
SaveToFile(&j, "journal.txt")
// Structで定義した場合
p := Persistence{"\r\n"}
p.SaveToFile(&j, "journal_p.txt")
}
OCP
Open for extension, closed for modification
という言葉で表される。
Spefication
というEnterprise Patternとも関連がある。
サイズや値段でフィルタリングできるアプリケーションを考える。
基本的な色でフィルタリングする仕組みだけなら以下の実装でできる。
type Color int
const (
red Color = iota
green
blue
)
type Size int
const (
small Size = iota
medium
large
)
type Product struct {
name string
color Color
size Size
}
type Filter struct {
//
}
func (f *Filter) FilterByColor(
products []Product, color Color) []*Product {
result := make([]*Product, 0)
for i, v := range products {
if v.color == color {
result = append(result, &products[i])
}
}
return result
}
func main() {
apple := Product{"Apple", green, small}
tree := Product{"Tree", green, large}
house := Product{"House", blue, large}
products := []Product{apple, tree, house}
fmt.Printf("Green products (old):\n")
f := Filter{}
for _, v := range f.FilterByColor(products, green) {
fmt.Printf(" - %s in green\n", v.name)
}
}
サイズでのフィルタリング、色とサイズでのフィルタリングを新しいメソッドを作って追加すると以下のようになる。そしてこれはOCPに違反している。
func (f *Filter) FilterByColor(
products []Product, color Color) []*Product {
result := make([]*Product, 0)
for i, v := range products {
if v.color == color {
result = append(result, &products[i])
}
}
return result
}
func (f *Filter) FilterBySize(
products []Product, size Size) []*Product {
result := make([]*Product, 0)
for i, v := range products {
if v.size == size {
result = append(result, &products[i])
}
}
return result
}
func (f *Filter) FilterBySizeAndColor(
products []Product, size Size, color Color) []*Product {
result := make([]*Product, 0)
for i, v := range products {
if v.size == size && v.color == color{
result = append(result, &products[i])
}
}
return result
}
Specification Pattern
Specification Patternを使って以下のように実装を変える。
type Specification interface {
IsSatisfied(p *Product) bool
}
type SizeSpecification struct {
size Size
}
func (s SizeSpecification) IsSatisfied(p *Product) bool {
return p.size == s.size
}
type ColorSpecification struct {
color Color
}
func (c ColorSpecification) IsSatisfied(p *Product) bool {
return p.color == c.color
}
type BetterFilter struct{}
func (f *BetterFilter) Filter(
products []Product, spec Specification) []*Product {
result := make([]*Product, 0)
for i, v := range products {
if spec.IsSatisfied(&v) {
result = append(result, &products[i])
}
}
return result
}
呼び出す際にはフィルタリングの条件に応じてFilterにSpecificationを渡す。
以下の例では色でのフィルタリングのみを実装した場合。
func main() {
// 最初に作成したバージョン
apple := Product{"Apple", green, small}
tree := Product{"Tree", green, large}
house := Product{"House", blue, large}
products := []Product{apple, tree, house}
fmt.Printf("Green products (old):\n")
f := Filter{}
for _, v := range f.FilterByColor(products, green) {
fmt.Printf(" - %s in green\n", v.name)
}
// Better Filter
fmt.Printf("Green products (new):\n")
greenSpec := ColorSpecification{green}
bf := BetterFilter{}
for _, v := range bf.Filter(products, greenSpec) {
fmt.Printf(" - %s in green\n", v.name)
}
}
以下の通り全く同じ結果が得られる。
しかし、Better Filterの方が新しいフィルタリングの条件を追加するときにSpecificationを追加するだけでよく、柔軟性が高く開発時の効率性も上がる。(コードの重複も少ない)
% go run ocp.go
Green products (old):
- Apple in green
- Tree in green
Green products (new):
- Apple in green
- Tree in green
OCPの文脈で上記の実装を考えてみると
- Specificationという型は拡張に対しては開かれている(機能追加は自由にできる)が、変更に対しては閉じられている(interfaceの型が変わることはない)。同様にBetterFilterもそう。
複数の条件に対応する方法
Design Patternの一つであるComposite Patternを使う。
以下のように複数のフィルタリング条件を受け付けるSpecificationを定義し、それを受け取るようにする。
type AndSpecification struct {
first, second Specification
}
func (a AndSpecification) IsSatisfied(p *Product) bool {
return a.first.IsSatisfied(p) && a.second.IsSatisfied(p)
}
...
func main() {
...
// Composite
largeSpec := SizeSpecification{large}
lgSpec := AndSpecification{greenSpec, largeSpec}
fmt.Printf("Large green products:\n")
for _, v := range bf.Filter(products, lgSpec) {
fmt.Printf(" - %s is green and large\n", v.name)
}
}
まとめ
- 機能追加するときに既存のメソッドをコピペして少しだけ変えているときはOCPに違反している匂いがする
- 一つの型を定義してそれを拡張していくことで、コードの重複も防げるし既存機能への影響を気にしなくても良くなる?
参考資料
Liskov Substitution Principle
Inheritanceの考えを元にしているのでそのままGoには適用できない。
Baseクラスで動作するならDeriveクラスでも動作するはずというのが基本的な考え。
package main
import "fmt"
type Sized interface {
GetWidth() int
SetWidth(width int)
GetHeight() int
SetHeight(height int)
}
type Rectangle struct {
width, height int
}
func (r *Rectangle) GetWidth() int {
return r.width
}
func (r *Rectangle) SetWidth(width int) {
r.width = width
}
func (r *Rectangle) GetHeight() int {
return r.height
}
func (r *Rectangle) SetHeight(height int) {
r.height = height
}
func UseIt(sized Sized) {
width := sized.GetWidth()
sized.SetHeight(10)
expectedArea := 10 * width
actualArea := sized.GetWidth() * sized.GetHeight()
fmt.Print("Expected an area of ", expectedArea,
", but got ", actualArea, "\n")
}
func main() {
rc := &Rectangle{2, 3}
UseIt(rc). // Expected an area of 20, but got 20
}
問題なし
四角形の計算を加えようとしたとき。
type Square struct {
Rectangle
}
func NewSquare(size int) *Square {
sq := Square{}
sq.width = size
sq.height = size
return &sq
}
func (s *Square) SetWidth(width int) {
s.width = width
s.height = width
}
以下のように想定外の値が出力される。
原因としてはSetHeightを実行したときにHeightだけでなく、Widthも設定しているから。
% go run lsp.go
Expected an area of 20, but got 20
Expected an area of 50, but got 100
Interface Segregation Principle
Interfaceに多くの情報を詰め込みすぎないこと?
type Document struct {
}
type Machine interface {
Print(d Document)
Fax(d Document)
Scan(d Document)
}
type MultiFunctionPrinter struct {
// interfaceを実装すればいい
}
func (m MultiFunctionPrinter) Print(d Document) {
}
func (m MultiFunctionPrinter) Fax(d Document) {
}
func (m MultiFunctionPrinter) Scan(d Document) {
}
しかし、もしFaxやScanの機能がないプリンターを実装する場合、どうすればいいだろうか。
以下のように実際には使い物にならないメソッドを実装する必要が出てきてしまう。
type OldFashionedPrinter struct {
}
func (o OldFashionedPrinter) Print(d Document) {
// ok
}
func (o OldFashionedPrinter) Fax(d Document) {
panic("この操作はサポートしていません")
}
func (o OldFashionedPrinter) Scan(d Document) {
panic("この操作はサポートしていません")
}
ISPの導入
以下のようにインタフェースを分離することで用途に応じたものを定義することができる。
// ISP
type Printer interface {
Print(d Document)
}
type Scanner interface {
Scan(d Document)
}
type MyPrinter struct {
}
func (m MyPrinter) Print(d Document) {
}
// PrinterとScannerの機能を持つ
type Photocopier struct{}
func (p Photocopier) Scan(d Document) {
}
func (p Photocopier) Print(d Document) {
}
複数のものに対応するパターン
type MultiFunctionDevice interface {
Printer
Scanner
}
Decoratorパターン
Decoratorパターンを使って複数の機能を実装することもできる。
// decorator
type MultiFunctionMachine struct {
printer Printer
scanner Scanner
}
func (m MultiFunctionMachine) Print(d Document) {
m.printer.Print(d)
}
Dependency Invesion Principle
type Relationship int
const (
Parent Relationship = iota
Child
Sibling
)
type Person struct {
name string
// other property
}
// 人と人の関係性をモデルとして定義する
type Info struct {
from *Person
relationship Relationship
to *Person
}
// low-level
// store relationship data
type Relationships struct {
relations []Info
}
func (r *Relationships) AddParentAndChild(
parent, child *Person) {
r.relations = append(r.relations, Info{parent, Parent, child})
r.relations = append(r.relations, Info{child, Child, parent})
}
// high-level
type Research struct {
// break DIP
relationships Relationships
}
func (r *Research) Investigate() {
relations := r.relationships.relations
for _, rel := range relations {
if rel.from.name == "John" &&
rel.relationship == Parent {
fmt.Println("John has a child called", rel.to.name)
}
}
}
func main() {
parent := Person{"John"}
child1 := Person{"Chris"}
child2 := Person{"Tom"}
relationships := Relationships{}
relationships.AddParentAndChild(&parent, &child1)
relationships.AddParentAndChild(&parent, &child2)
r := Research{relationships}
r.Investigate()
}
以下の箇所が問題。
Research module
(High-level)がRelationships module
(low-level)の内部要素に依存している。
Relationships module
がデータの保存方法を外部ストレージ(例えばDB)に変更するとき、Research module
に変更が必要になる。(Low-levelの具体に依存しているから)
具体的には以下の例だとforループは使えなくなる。このように具体に抽象することで、変更によりHigh Levelまでコードの変更が必要になる
// high-level
type Research struct {
// break DIP
relationships Relationships
}
func (r *Research) Investigate() {
relations := r.relationships.relations
for _, rel := range relations {
if rel.from.name == "John" &&
rel.relationship == Parent {
fmt.Println("John has a child called", rel.to.name)
}
}
}
DIPでの書き方
Research
でRelationships
に依存するのではなく、RelationshipBrowser
という新しいinterfaceを定義し、そこに依存するようにする。
その上でRelationships
にメソッドを実装する。
...
// low-level
// store relationship data
type RelationshipBrowser interface {
FindAllChildrenOf(name string) []*Person
}
func (rs *Relationships) FindAllChildrenOf(name string) []*Person {
result := make([]*Person, 0)
for i, v := range rs.relations {
if v.relationship == Parent &&
v.from.name == name {
result = append(result, rs.relations[i].to)
}
}
return result
}
type Relationships struct {
relations []Info
}
func (rs *Relationships) AddParentAndChild(
parent, child *Person) {
rs.relations = append(rs.relations, Info{parent, Parent, child})
rs.relations = append(rs.relations, Info{child, Child, parent})
}
// high-level
type Research struct {
// break DIP
// relationships Relationships
browser RelationshipBrowser //
}
func (r *Research) Investigate() {
for _, p := range r.browser.FindAllChildrenOf("John") {
fmt.Println("John has a child called", p.name)
}
}
func main() {
parent := Person{"John"}
child1 := Person{"Chris"}
child2 := Person{"Tom"}
relationships := Relationships{}
relationships.AddParentAndChild(&parent, &child1)
relationships.AddParentAndChild(&parent, &child2)
r := Research{&relationships} // Interfaceに依存するように変更
r.Investigate()
}