TL;DR
- メソッドの抽出とクラスの抽出によって、コードを「短いメソッド」と「小さなクラス」に整理する。これが、変更を楽で安全にする設計の基本である。
- メソッドやクラスの名前を業務の関心事や業務で使われている言葉と一致させ、どこに何が書いてあるかを分かりやすくする。
内容
変更のしづらさは設計の問題
ソフトウェアの変更がしづらいのは、設計に問題があるからである。設計に必要なのは、きれいなクラス図や詳細な仕様書ではなくソースコードを整理整頓し、どこに何が書いてあるのかを分かりやすくすることである。設計の良し悪しは、ソフトウェアを変更するときに分かる。
変更が大変なプログラムの特徴
変更が大変なプログラムには、3 つの特徴がある。1 つ目はメソッドが長いことで、特にネストしたブロックがあると理解するのが大変になる。2 つ目はクラスが大きいことで、関心事を詰め込みすぎているため、変更に必要な箇所との関係が把握しづらい。3 つ目は引数が多いことで、これも関心事の詰め込みすぎであり、変更に必要な箇所と引数の関係が把握しづらくなる。引数が多いとメソッドが長くなり、メソッドが長いとクラスが大きくなるという負のスパイラルに陥る。
こうした状態は、ちょっとしたコードの修正や機能の拡張を繰り返した結果として生まれる。最初はすっきりしていた構造も、修正を重ねるうちに入り組み、全体のバランスが崩れていく。こうして、変更するたびに変更が大変になっていく。
変更を楽にする書き方
まず、わかりやすい名前を使う。短い名前や略語はいろいろな解釈ができるので、正しい意味を推測するのに時間がかかる。
長いメソッドは、意味が異なる場所で空行を入れて「段落」に分けると読みやすくなる。
int price = quantity * unitPrice;
if (price < 3000) {
price += 500;
}
price *= taxRate();次に、目的ごとに変数を用意する。これはリファクタリングの基本テクニックの 1 つである「説明用変数の導入」にあたる。変更の影響範囲を狭くでき、破壊的代入もなくなるので、予想外の副作用の原因を取り除ける。各ステップの目的も明確になり、段落どうしの結びつきが弱まって独立性が高まる。
int basePrice = quantity * unitPrice;
int shippingCost = 0;
if (basePrice < 1000) {
shippingCost = 500;
}
int itemPrice = (basePrice + shippingCost) * taxRate();さらに、段落をメソッドの抽出で独立したメソッドにする。段落に分けるのは見た目の工夫で、可読性は上がるが構造は変わらない。メソッドとして切り出すとプログラムの構造が変わり、変更の影響をそのメソッドの中に閉じ込めやすくなる。元のコードもシンプルになり、メソッド名からコードの意図を理解しやすくなる。再利用もしやすくなり、重複も防げる(コードの重複 vs コードの柔軟性)。
int shippingCost(int basePrice) {
if (basePrice < 1000) return 500;
return 0;
}異なるクラスの重複したコードをなくす
異なるクラスに同じコードがあるときは、次の手順で重複をなくす。
- それぞれのクラスの該当する段落を、メソッドに抽出する
- 2 つのクラスに参照関係があれば、参照する側で抽出したメソッドの呼び出しを、参照先のオブジェクトのメソッド呼び出しに置き換える
- 参照関係がなければ、共通メソッドの置き場所として新しいクラスを作り、抽出したメソッドを移す
- 元の 2 つのクラスを、新しいクラスの共通メソッドを使うように書き換える
たとえば、Order クラスと UpdateOrder クラスの両方で送料の計算が必要なら、ShippingCost クラスを作って送料のロジックを集める。
class ShippingCost
MINIMUM_FOR_FREE = 3000
COST = 500
attr_reader :base_price
def initialize(base_price)
@base_price = base_price
end
def amount
return COST if base_price < MINIMUM_FOR_FREE
0
end
endShippingCost は、送料の対象になる金額(base_price)ごとに別のオブジェクトを作る。計算の対象となる値が異なれば別のオブジェクトに分けるのが、オブジェクト指向らしい設計である。こうすると、対象金額が複数あるときに、誤って別の金額を使ってしまう不具合を防げる。送料の計算が必要なときは ShippingCost を再利用すればよい。これは ShippingCost が狭い関心事に特化したクラスだからこそできる。
狭い関心事に特化したクラス
特定の関心事に特化した小さなクラスは、コードの見通しを良くし、変更をしやすくする。複数の関心事が混ざっていると、気が散る要素が多くて理解しにくい。小さなクラスなら変更の影響もそのクラスの中に留められるので、保守性も高い。
「送料」と ShippingCost のように、業務で使われる用語に対応するクラスをドメインオブジェクトと呼ぶ。業務の用語と直接対応するドメインオブジェクトを用意することが、変更しやすいアプリケーションを作るオブジェクト指向らしいアプローチである。要求を分析して見つけた業務の関心事の単位を、そのままクラスとして表現する。分析で見つけた業務の構造とプログラムの構造が一致していれば、修正や拡張が必要になったときにどこに何が書いてあるかを特定しやすく、変更も安全で楽になる。
メソッドは短く、クラスは小さく
メソッドを短く、クラスを小さくすると、次のメリットがある。
- メソッドやクラスの名前が、変更する箇所を特定する足がかりになる
- コードの重複がなくなり、修正箇所が 1 か所になる
- コードの変更による影響を、メソッドやクラスの中に閉じ込めやすい
キーワード
- 設計
- リファクタリング
- メソッドの抽出
- クラスの抽出
- ドメインオブジェクト
- オブジェクト指向プログラミング
自分の理解
設計はソースコードを整理整頓することだという話は、コードがドキュメントであるということだと思う。
名前については、ブロックが限られていて意味も明確な場合は、不必要に長くする必要はないと思う。for ループの index などがその例である。特に Go はその意識が強い(Go に入っては Go に従え)。
メソッドとして切り出すときは、命名が大事になる。
異なるクラスの重複をなくす手順は、リファクタリングのテクニックにも似たものがあったはずである。クラスの抽出だったと思う。なお、本の例は Java だが、書くのが楽しくないので Ruby で書いた。