[現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法](https://gihyo.jp/book/2017/978-4-7741-9087-7) のメモ 本のサンプルソースはJavaで書かれているが、勉強も兼ねてGo言語にアレンジしている。 # Chapter2 場合分けのロジックを整理する ## プログラムを複雑にする「場合分け」のコード ### 区分や種別がコードを煩雑にする ソフトウェアを複雑にし、変更を厄介にする原因の人るがば場合分けロジック。 - 顧客区分 - 料金種別 - ... 区分ごとのロジックをかき分ける基本手段がif分野switch文。 プログラムのあちこちに同じようなif文/switch文が重複すると、プログラムの変更をやりにくくする。 さらに複数の区分体型を組み合わせて判断する業務ロジックだとif文の分岐がどんどん複雑になる。 このような複雑になりがちな、区分ごとの業務ルールのコードを変更のやりやすいプログラムにするにはどう設計すればよいか? ### 判断や処理のロジックをメソッドに独立させる 区分ごとのコードの整理も、オブジェクト指向の考え方で設計する。 - コードの塊は、メソッドとして独立させる - 関連するデータとロジックは1つのくらすにまとめる コードの塊をメソッドして抽出して独立させる方法。 ```go # bud if customerType.equals("child"){ fee := baseFee * 0.5 } ``` ```go # good if isChild(){ fee = childFee() } // 判断ロジックの詳細 func isChild() bool{ return consumerType.equals("child") } // 計算ロジックの詳細 func childFee()int{ return baseFee * 0.5 } ``` メソッドを絵定義することで、もともとのif文はそれを呼び出すだけのシンプルな表現になる。 メソッド名を読むだけで何をやろうとしているか明確になる。 また、仕様変更が発生した場合でも、メソッドの実装を変えることになるので変更がかんたんで安全。 ### else句をなくすと条件分岐が単純になる else句は、プログラムの構造を複雑にする。 else句をできるだけ書かないほうがプログラムが単純になる。 ```go // bud func fee() Yen{ var result Yen if isChild(){ result = childFee() }else if isSenior(){ result = seniorFee() }else{ result = adultFee() } return result } ``` ローカル変数の計算方法をelse区でかき分けている。 しかし、実際にはローカル変数は必要ない。 ```go // ローカル変数を使わずに判定後、直ちに結果を返す func fee() Yen{ if isChild(){ return childFee() }else if isSenior(){ return seniorFee() }else{ returnadultFee() } } ``` ローカル変数がなくなったことでコードがシンプルでわかりやすくなった。 これが、***早期リターン***という書き方。 早期リターンを使うとelse句が不要になる。 ```go // good func fee() Yen{ if isChild(){ return childFee() } if isSenior(){ return seniorFee() } returnadultFee() } ``` else句を使わずに早期リターンするこの書き方を***ガード節***と呼ぶ。 コードの中にelse句を見つけたら、早期リターンやガード説に書き換えることを検討してみる。 場合分けのコードをすっきりさせ、変更時にバグが紛れ込む可能性が確実に減る。 早期リターンやガード節を検討しやすくするために、予め条件判断の式や条件分の中の式をそれぞれメソッドに抽出しておく。 ### 複文は単文に分ける 前節のelse句を使った最初の書き方は、if文のelse区の中にif文を書いている。if文が入れ子になった複文構造。 「複文」は日本語でもプログラミングでも意図をわかりにくくする。そこで、複文を分解して、単文を並べるシンプルな構造に変える。 複文は文と文が密に結合している。単文は文と文の結合度を下げる。結合度を下げることで変更がやりやすくなる。 ```go func fee() Yen{ // 「幼児」区分を追加 if isBaby(){ return babyFee() } if isChild(){ return childFee() } if isSenior(){ return seniorFee() } returnadultFee() } ``` ### 区分ごとのロジックをクラスに分ける 前節ではメソッドとして独立させた。 もっと独立性を高めるために、メソッドではなく別クラスに分ける。 ```go type AdultFee struct{} func (a AdultFee) fee() Yen{ return NewYen(100) } } func (a AdultFee) label() string{ return "大人" } } type ChildFee struct{} func (a ChildFee) fee() Yen{ return NewYen(50) } } func (a ChildFee) label() string{ return "子供" } } type SeniorFee struct{} func (a SeniorFee) fee() Yen{ return NewYen(80) } } func (a SeniorFee) label() string{ return "シニア" } } ``` 区分に関するロジックを区分ごとにクラスに分けて記述すれば、区分ごとのロジックが整理され、どこに何が書かれているか明確になる。 ### 区分ごとのクラスを同じ「型」として扱う インターフェースを使って異なるクラスを同じ型として扱う。 ```graphviz digraph G { label = "インターフェースを使って異なるクラスを同じ型として扱う" node [fontname = "Bitstream Vera Sans", fontsize = 8, shape = "record"] edge [fontname = "Bitstream Vera Sans", fontsize = 8] Fee [ label = "{\<\<interface\>\>\lAnimal|fee()\llabel()\l}"] AdultFee [label = "{AdultFee|fee()\llabel()\l}"] ChildFee [label = "{ChildFee|fee()\llabel()\l}"] edge [arrowhead = "empty"] AdultFee -> Fee [style = dashed]; ChildFee -> Fee [style = dashed]; } ``` ```go type Fee interface{ fee() Yen label() string } type AdultFee struct{} func newAdultFee() AdultFee { return AdultFee{} } func (a AdultFee) fee() Yen{ return NewYen(100) } func (a AdultFee) label() string{ return "大人" } type ChildFee struct{} func newChildFee() ChildFee { return ChildFee{} } func (a ChildFee) fee() Yen{ return NewYen(50) } func (a ChildFee) label() string{ return "子供" } type SeniorFee struct{} func newSeniorFee() SeniorFee { return SeniorFee{} } func (a SeniorFee) fee() Yen{ return NewYen(80) } func (a SeniorFee) label() string{ return "シニア" } ``` ```go //Fee型を使う側のコード type Charge{ fee Fee } func (c Charge) NewCherge(Fee) *Charge{ return &Charge{Fee: fee} } func (c Charge) yen() Yen{ return fee.yen() } ``` ```go // 子連れ団体の料金計算 type Reservation struct{ fees []Fee } func NewReservation() *Reservation{ return &Reservation{} } func (r Reservation) addFee(){ r.fees.add(fee) } func (r Reservation) feeTotal() Yen{ var total Yen for _,v := range r.fees{ total.add(v.yen()) } return total } ``` インターフェース宣言と、区分ごとの専用クラスを組みわせて、区分ごとに異なるクラスのオブジェクトを「同じ型」として扱う仕組みを***多態***と呼ぶ。 使う側のクラスが、実際にどのような区分があるのか「知らない」ことが重要。 クラスとクラスの関係は、知っていることが多いほど密結合になる。 結合が弱いほど独立性が高くなり、あるクラスの変更が他のクラスに影響することが減る。 ### 区分ごとのクラスのインスタンスを生成する 多態は、利用する側のコードをシンプルにする。 区分ごとのクラスのインスタンスを生成するときにはif文で場合分けが必要なる。 しかしちょっとした工夫でこのif文は不要になる。 ```go // if文を使わずに区分ごとに小向区とを生成するやり方の例 type FeeFactory struct{ types map[string]Fee } type FeeFactory struct { types map[string]Fee } func NewFeeFactory() FeeFactory { t := make(map[string]Fee) t["adult"] = newAdultFee() t["child"] = newChildFee() t["senior"] = newSeniorFee() return FeeFactory{types: t} } func (ff FeeFactory) feeByName(t string) Fee { return ff.types[t] } ``` ```go ff := NewFeeFactory() ff.feeByName("adult").label() ``` ### Javaの列挙型を使えばもっとかんたん 多態には区分の一覧がわかりにくいという問題がある。 区分ごとのクラス一覧を明示的に記述できる。 それが***列挙型(enum)***。 ``` type FeeType int const ( adult FeeType = iota +1 child senior ) // 区分を使う側のクラス type Guest struct{ feeType Type } func (g Guest) isAdult(){ return g.feeType == FeeType.adult } ``` 区分定数の一覧を宣言する列挙型はJava以外の言語でも用意されている。 しかしJavaの列挙型は単純な区分定数ではなく、クラス。 インスタンス変数やメソッドを記述できる。 列挙型を使って、区分ごとののロジックをわかりやすく整理するこの方法を***区分オブジェクト***と呼ぶ。 ### 区分ごとの業務ロジックを区分オブジェクトで分析し整理する 業務アプリケーションには様々な区分/分類/種別が登場する。 それらの区分ごとの業務ルールや、区分を組み合わせた判断ロジックが業務アプリケーションを複雑にする。 区分ごとの業務ロジックを、区分ごとに別クラスに独立させた区分オブジェクトはオブジェクト指向らしいコードの整理のやり方。 区分ごとのロジックをどこに書くべきかわかりやすくなる。 クラスに分けることで、特定の場合のルールや計算方法を変更しても、影響範囲をそのクラスに閉じ込めることができる。 さらに列挙型を使って、業務を扱う区分の一覧を宣言することは、業務ロジックの見通しを抑止、わかりやすく整理する手段。 オブジェクト指向では、業務の関心事や業務ロジックを分析し整理する活動と、クラスを設計する活動は基本的に同じ。 ### 状態遷移のルールをわかりやすく記述する 業務アプリケーションでは、状態の繊維を管理することも重要な関心事の一つ。 列挙型を使うと、状態遷移に関わる制約をif文/switch文を使わずに表現できる。 Javaの列挙型は区分を集合として扱うことができる。 状態の一覧を配列として取得したり、コレクション型として状態の就業を操作できる。 コードで表現するやり方は以下。 - ある状態から遷移可能な状態(複数)をSetで宣言する - 繊維ともの状態を「キー」に、遷移可能な状態のSetのを値(バリュー)にしたMapを宣言する。 このやり方は色々応用が効く。 状態遷移図の矢印はある状態から別の状態に遷移する「イベント」を表している。 状態の列挙型とイベントの列挙型を組み合わせることで、次のルールを宣言的に記述できる。 - あるイベントがその状態で起きて良いイベントか起きてはいけないイベントかの判定 - ある状態で発生しても良いイベントの一覧の提示 Javaの列挙型は振る舞いを持てる。 状態やイベントごとの判断/加工/計算のロジック、をそれぞれの状態区分やイベント区分に持たせることができる。 静的な制約ルールだけでなく、状態に関わる動的なビジネスルールも、列挙型を活用することで、見通しよく整理でき、変更も楽に安全にできる。 状態遷移に関わる業務ロジックを分析生て整理することは業務アプリケーションの中心課題の一つ。 そして、オブジェクト指向の開発では、状態遷移図のような視覚的な表現を使った活動と、列挙型という実装のしくみを直接的に関連付けて分析と設計をすすめる。 ぎゅ無の理解とコードの実装が一致しているほど、プログラムはわかりやすくなり、業務要件の変更や機能追加への対応が楽で安全になる。 ### 第2章のまとめ ###### tags: `ISBN-978-4-7741-9087-7`