LoginSignup
8
7

More than 5 years have passed since last update.

Go言語で再考するオープンクローズドの原則

Posted at

オープンクローズドの原則とは?

Software entities should be open for extension, but closed for modification.
–Bertrand Meyer, Object-Oriented Software Construction

オープンクローズドの原則とは
「ソフトウェアの構成要素は拡張に対して開いていて、修正に対して閉じていなければならない」
という原則です。

もう少し噛み砕いて言い換えると
「新しい機能を追加する時に、既存の成果物を変更せずに拡張できるようにし、
修正よってそれ以外の他の成果物に変更を与えてはいけない。」

という意味です。

イメージで捉える悪い例

A-B.jpeg

以下の仕様追加を想定します。
「別のServerを追加し、Clientから使用する」

変更する箇所は使う側と使われる側の2点あります。
・新しくServerを作る
・Clientが新しいServerを呼べるようにIF文を追加

数が増えるほどにIF文が多くなり、修正も拡張もしにくい設計になってます。

イメージで捉える良い例

A-I-B.jpeg

悪いイメージの時と同様に以下の仕様追加を想定します。
「別のServerを追加し、Clientから使用する」

変更する箇所は使われる側の1点のみになります。
・新しくServerを作る

数が増えてもIF文が一つもなく、修正も拡張もしやすい設計になってます。

適用のタイミング

実際に概念を理解したところで、具体的どのようなタイミングで適用していくかの判断基準を説明していきます。

共通の振る舞いを持つが、実装の違いをもつオブジェクトが存在する時

例えば、面積の計算をイメージしてみましょう。
三角形と四角形があり、共通の振る舞いとして、面積の計算があります。
しかし、計算方法(実装の違い)があるとしましょう。
こんな時に適用できます。具体的な実装は以下の章で説明しています。

変更が加わった時

最初に「イメージで捉える悪い例」の図のような設計をすることは問題ではありません。
しかし、新しくサーバを追加するとなった時に、悪い例のようにIF文を使って対応するのではなく、
良い例のよう影響の少ない方法で対応していきましょう。

注意すること

適用するタイミングで一つ注意することがあります。それは未来のための適用です。
まだClientとServerが1つずつの状態の時に、良い例のように設計を適用してしまうと2つの問題が生じます。
・今後追加するかわからないのに設計が複雑になる
・間違った抽象化をすると結局拡張できない

なので、「今」一番良い設計をし、「未来」も一番良い設計をするステップが大切になります。

Go言語で再考してみた

概念的な話を理解したところで、具体的にコードベースでより理解を深めていきます。
Go言語でオープンクローズドの原則を実現する方法は2つあります。
・1.構造体の埋め込み
・2.ダックタイピング

「1.構造体の埋め込み」は継承のようなイメージです。
Go言語には継承という仕組みがありませんが、構造体を入れ子にすることで、継承のような動きを実現できます。

「2.ダックタイピング」はインタフェースを使った拡張のイメージです。
Go言語ではダックタイピングと言われる仕組みがります。
簡単に説明すると、「インタフェースで宣言したメソッドをすべて満たす構造体を作れば、それはそのインタフェースを実装した型として扱える」というものです。
ストラテジーパターンを使って説明されることが多いです。

1.構造体の埋め込み

package main

import "fmt"

type shape struct {
    height int
    bottom int
}

func (s shape) Execute() int {
    return s.height * s.bottom
}

type triangle struct {
    shape
}

func (t triangle) Execute() int {
    return t.height * t.bottom / 2
}

type square struct {
    shape
}

func (m square) Execute() int {
    return m.height * m.bottom
}

// 追加
//type diamond struct {
////    shape
////}
////func (m diamond) Execute() int {
////    return m.height * m.bottom
////}

func main() {
    t := triangle{shape{height: 3, bottom: 8}}
    s := square{shape{height: 2, bottom: 4}}
    // d := diamond{height:shape{height:2, bottom:4}} // 追加
    fmt.Println(t.Execute()) // 12
    fmt.Println(s.Execute()) // 8
    // fmt.Println(d.Execute()) // 追加
}

コードの説明

構造体shapeを構造体triangle/squareに埋め込んでいます。
triangle/squareはshapeのもつheightやbottom、そしてExecuteメソッドを使うことができます。
triangle/squareでExecuteメソッドを再度定義している箇所があります。

func (t triangle) Execute() int {
    return t.height * t.bottom / 2
}

これによりshapeのExecuteメソッドをオーバーライドしたことになります。
また、コメントアウトでdiamondを追加した場合の実装を書いています。
そこからわかるように既存のtriangleやsquare、などに一切変更は加えていません。
このコードは既存の成果物を変更せずに拡張できることがわかります。

また、triangle/square/diamondのメソッドの中身を修正しても、他の箇所に影響を与えることはありません。

2.ダックタイピング

package main

import "fmt"

type IShape interface {
    Execute(height int, bottom int) int
}

type triangle struct {
}
func (t triangle) Execute(height int, bottom int) int {
    return height * bottom / 2
}

type square struct {
}
func (m square) Execute(height int, bottom int) int {
    return height * bottom
}

// 追加
//type diamond struct {
//}
//func (m diamond) Execute(height int, bottom int) int {
//  return height * bottom
//}

type shape struct {
    height int
    bottom int
    s IShape
}
func (c shape) Calculate() int {
    return c.s.Execute(c.height, c.bottom)
}

func main() {
    t := shape{height:3, bottom:8, s: triangle{}}
    s := shape{height:2, bottom:4, s: square{}}
    // d := shape{height:2, bottom:4, s: diamond{}} // 追加
    fmt.Println(t.Calculate()) // 12
    fmt.Println(s.Calculate()) // 8
    // fmt.Println(d.Calculate()) // 追加
    // こんな書き方も出来る
    //shape := []shape{
    //  {height:3, bottom:8, s: triangle{}},
    //  {height:2, bottom:4, s: square{}},
    //  {height:2, bottom:4, s: diamond{}},
    //}
    //for _, s := range shape{
    //  fmt.Println(s.Calculate())
    //}
}

コードの説明

IShapeインタフェースでExecuteメソッドを定義しています。
つまり、このExecuteメソッドを満たす構造体を作れば、それはそのインタフェースを実装した型とすることができます。

構造体であるtriangleとsquareがExecuteメソッドを実装しているので、IShapeインタフェースの型で扱うことが出来るようになりました。

なので以下のようにインタフェースの肩にtriangle{}とsquare{}を定義できています。

t := shape{height:3, bottom:8, s: triangle{}}
s := shape{height:2, bottom:4, s: square{}}

また、コメントアウトでdiamondを追加した場合の実装を書いています。
そこからわかるように既存のtriangleやsquare、IShapeなどに一切変更は加えていません。
このコードは既存の成果物を変更せずに拡張できることがわかります。

また、triangle/square/diamondのメソッドの中身を修正しても、他の箇所に影響を与えることはありません。
※リスコフの置換原則がオープンクローズドの原則を適用する指標になります。

最後に

オープンクローズドの原則を実現する2つの方法を紹介しました。
他にもSOLIDの原則をまとめていきます。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7