SOLID原則 オープンクローズドの原則
はじめに
業務の中で既存のコードを修正しないと改修ができない状況に遭いました。
その時にオープンクローズドの原則が頭によぎり、今回内容をまとめてみました。
ここではオープンクローズドの原則の説明と実際にコードを書いて原則に反しているケースと満たしているケースについて記載しています。
オープンクローズドの原則とは
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
訳すと「クラス、モジュール、ファンクションなどのソフトウエアの構成要素は拡張に対してオープンで修正に対してはクローズドであるべきだ」となります。
ここの「オープン」とは「開く」や「解放する」ではなく、open-mindedの「受け入れやすい」※意味が近しいと考えています。
※英辞郎on the wed 参照
一方で「クローズド」は閉鎖的または排他的※と考えています。
※goo辞書 参照
上記を踏まえるとオープンクローズの原則は「クラス、モジュール、ファンクションは拡張を柔軟に受け入れ、既存のコードの修正に対しては排他的であること」ということなります。
では、ここから原則に反しているケースと原則を満たしているケースを説明します。
オープンクローズドの原則に反しているケース
以下のコードではechoInfoメソッドで学生の情報を入力し、「○年○組 名前」を出力します。
package main
import "fmt"
func main() {
ss := &SchoolServie{}
ss.echoInfo()
}
type SchoolServie struct {
}
func (ss *SchoolServie) echoInfo() {
s := &Student{"1", "A", "鈴木"}
fmt.Printf("%s年%s組 %s\n", s.getGrade(), s.getClass(), s.getName())
}
type Student struct {
Grade string
Class string
Name string
}
func (s *Student) getName() string {
return s.Name
}
func (s *Student) getGrade() string {
return s.Grade
}
func (s *Student) getClass() string {
return s.Class
}
ここで学生の情報だけでなく教員の情報を出力するように改修します。
package main
import "fmt"
func main() {
ss := &SchoolServie{}
ss.echoInfo("student")
ss.echoInfo("teacher")
}
type SchoolServie struct {
}
func (ss *SchoolServie) echoInfo(category string) {
switch category {
case "student":
s := &Student{"1", "A", "鈴木"}
fmt.Printf("%s年%s組 %s\n", s.getGrade(), s.getClass(), s.getName())
case "teacher":
t := &Teacher{"math", "田中"}
fmt.Printf("%s担当%s\n", t.getSubject(), t.getName())
}
}
type Student struct {
Grade string
Class string
Name string
}
func (s *Student) getName() string {
return s.Name
}
func (s *Student) getGrade() string {
return s.Grade
}
func (s *Student) getClass() string {
return s.Class
}
type Teacher struct {
Subject string
Name string
}
func (t *Teacher) getName() string {
return t.Name
}
func (t *Teacher) getSubject() string {
return t.Subject
}
echoInfoメソッドを見ると分岐処理が実装され既存の処理が修正されました。
これでは修正次第でバグが混入し既存の学生情報を出力する処理に影響を与える可能性があります。
また、今後出力する情報を追加するたびに修正が入り既存処理に影響を与えてしまいます。
オープンクローズドの原則を満たしているケース
処理内容は先ほどのコードと変わらず学生の情報を出力しています。
先ほどとの変更点はSchoolMemberIFというインターフェースを作成し実装しています。
また、インターフェースを利用して依存性注入を行なっています。
package main
import "fmt"
func main() {
st := &SchoolServie{NewStudent("1", "A", "鈴木")}
st.echoInfo()
}
type SchoolServie struct {
smif SchoolMemberIF
}
func (ss *SchoolServie) echoInfo() {
ss.smif.getInfo()
}
type SchoolMemberIF interface {
getInfo()
}
func NewStudent(g string, c string, n string) SchoolMemberIF {
return &student{g, c, n}
}
type student struct {
Grade string
Class string
Name string
}
func (s *student) getInfo() {
fmt.Printf("%s年%s組 %s\n", s.Grade, s.Class, s.Name)
}
type teacher struct {
Subject string
Name string
}
func (t *teacher) getInfo() {
fmt.Printf("%s担当 %s\n", t.Subject, t.Name)
}
先ほどの同様に教員の情報を出力する様に改修します。
ackage main
import "fmt"
func main() {
st := &SchoolServie{NewStudent("1", "A", "鈴木")}
st.echoInfo()
te := &SchoolServie{NewTeacher("数学", "田中")}
te.echoInfo()
}
type SchoolServie struct {
smif SchoolMemberIF
}
func (ss *SchoolServie) echoInfo() {
ss.smif.getInfo()
}
type SchoolMemberIF interface {
getInfo()
}
func NewStudent(g string, c string, n string) SchoolMemberIF {
return &student{g, c, n}
}
type student struct {
Grade string
Class string
Name string
}
func (s *student) getInfo() {
fmt.Printf("%s年%s組 %s\n", s.Grade, s.Class, s.Name)
}
func NewTeacher(s string, n string) SchoolMemberIF {
return &teacher{s, n}
}
type teacher struct {
Subject string
Name string
}
func (t *teacher) getInfo() {
fmt.Printf("%s担当 %s\n", t.Subject, t.Name)
}
全体を見ると既存のコードに修正が入らず、教員の情報を追加することができました。
原則を満たしていないケースの時のコードと比較してシンプルで読み易い作りになっています。
こちらの実装ですと、出力する情報が増えても既存コードの修正が入らず機能を拡張することができます。また同時に改修によるバグが発生する可能性も低いです。
今回は依存性注入を使用しましたが、拡張性の高いコードを実装にするには仕様やドメインを理解し今後起こり得る改修を考慮した実装を行うことが必要だと考えています。
まとめ
- オープンクローズドの原則は既存のコードを修正せずに拡張する考えである。
- 既存のコードに修正が入る作りだとコードが複雑になり改修によりデグレードが発生する可能性が高い。
- 拡張できる状態だとコードがシンプルになり改修によるデグレードが発生する可能性が低い。
- コーディングする時は今後の起こり得る改修をイメージして行う必要がある。
参考資料
Discussion