この記事で得られるもの
エンジニア6年目になり、設計について改めて学びたいと思い「現場で役立つシステム設計の原則」を読んでいます。
今回は、現場のソフトウェア開発で、コードをより修正しやすく安全で促進的にするための原則をまとめました。この記事を読めば、次のことが理解できます。
- コードの設計を改善するための基本テクニック
- 変更をより安全にするコードの書き方
目次
なぜソフトウェアの変更は大変なのか
設計 とは
- 「どこに何が書いてあるかわかりやすくし、修正や拡張が楽で安全になるコードを生み出す」ことです
変更するたびに変更が大変になる
アプリケーションを利用者が使い始めると、さまざまな改善要望や不具合の修正が出てきます。
そのたびにメソッドが 数行長くなり、クラスが少し膨らみ、引数がけっこう増えます。その結果、どこに何が書いてあるか、わかりづらくなっていきます。
変更が大変になるのは、こういった「こまごましたコードの修正や機能拡張」が重なったプログラムです。
プログラムの変更が楽になる書き方
目的ごとに変数を用意する
-
説明用の変数の導入
目的ごとに専用のローカル変数を用意し、コードの意図を変数名で説明するやり方です。
そして、既存コードの設計を改善する リファクタリングの基本テクニック です。 -
破壊的代入
1つの変数を使いまわして代入を繰り返す書き方です。
破壊的代入は変更の副作用を起こしやすい書き方です。用途ごとに「説明用の変数」を積極的に導入して破壊的な代入をなくします。
破壊的な代入がなくなればプログラムを変更したときの副作用が減り、コードが安定します。
1つの変数を複数の目的に使いまわす
int price = quantity * unitPrice;
if( price < 3000 )
price += 500; //送料
price = price * taxRate();
目的ごとのローカル変数を使う
int basePrice = quantity * unitPrice;
int shippingCost = 0; // 送料の初期値
if( basePrice < 3000 ) // 3000円未満
shippingCost = 500; // 送料500円
int itemPrice = (basePrice + shippingCost) * taxRate();
メソッドとして独立させる
この独立性の高くなった段落を「メソッド」として独立させると、さらにコードがわかりやすくなり、変更が楽で安全になります。
- 「段落」は見た目の工夫
- 段落をメソッドとして独立させると、プログラムの構造が変化する
送料計算をメソッドにする
int basePrice = quantity * unitPrice;
int shippingCost = shippingCost(basePrice); //送料計算メソッド
int itemPrice = (basePrice + shippingCost) * taxRate();
...
//メソッドに独立させた送料計算のロジック
int shippingCost(int basePrice) {
if( basePrice < 3000 ) return 500;
return 0 ;
}
- 関連性が強いデータとロジックをメソッドに移動すると、ロジックを再利用しやすくなります
狭い関心事に特化したクラスにする
送料クラス
・送料が無料になる注文金額(3000円)
・送料(500円)
・注文金額を判断して、適切な送料を計算するロジック
- 送料計算に関するデータと計算式だけを抜き出して、送料クラスに独立させておけば、変更の対象と影響範囲を送料クラスに限定できる
- 将来、送料に関するルールの変更があっても、修正箇所と影響をこのクラスに限定できる
- 「送料」クラスのように、業務で使われる用語に合わせて、その用語の関心事に対応するクラスを ドメインオブジェクト と呼ぶ
- 業務の用語と、直接対応するドメインオブジェクトを用意することが、業務アプリケーションの変更を容易にするオブジェクト指向らしい設計のアプローチ
- 業務の関心事の単位を、そのままプログラミング単位としてクラスで表現するのが、オブジェクト指向開発のやり方
メソッドは短く、クラスは小さく
- 名前は(略語ではなく)普通の単語を使う
- 「目的別の変数」を使う(1つの変数を使いまわさない)
- 意味のあるコードのまとまり(段落)を「メソッド」として独立させる
- 業務の関心事に対応したクラス(ドメインオブジェクト)を作る
- 「短いメソッド」と「小さなクラス」は変更を楽で安全にする
小さなクラスでわかりやすく安全に
基本データ型の落とし穴
あやしげな数量や金額の宣言
int quantity;
BigDecimal amount;
// intは、マイナス21億からプラス21億の範囲の整数
// BigDecimalは、実質的に無限の範囲の数(小数点も21億桁まで扱える)
- 業務アプリケーションとして、この書き方は危険
- 業務の関心事とはかけ離れた異常な値を扱うことを宣言している
- 文法的には問題ないが、思わぬ障害が混入する原因になる
値の範囲を制限してプログラムをわかりやすく安全にする
- 業務アプリケーションで数量を扱うとき、intのすべての範囲(マイナス21億からプラス21億)が必要になることはない
- Quantityクラスを独自に宣言 して、異常な値を扱わないようにする
- 電話番号をString型で扱うと、実際にこういう不適切なデータが混入する可能性がある
- 電話番号を扱う場合は、必ずTelephone型のオブジェクトとして扱えば、正しいデータであることを保証できる
「値」を扱うための専用のクラスを作る
- 専用の型は業務的に不適切な値が混入するバグを防ぐ
- 値を扱うための専用クラスを作るやり方を 値オブジェクト(Value Object) と呼ぶ
値オブジェクトを使わない場合
- int型やString型だらけになる
- int型やString型だらけだと、そのコードが業務的に何をやっているのか、プログラムを読んだだけでは理解ができない
- 業務ルールの追加や変更がやりにくくなる
- int型やString型が扱える値の範囲は、業務で必要な値の範囲とはかけ離れる
- 業務で扱うデータの種類ごとに値オブジェクトをうまく作って、「業務でやりたいこと」と「プログラムでやっていること」の間の対応を取りやすくする
値オブジェクトは「不変」にする
変数の値の上書きは危険
変数の値を書き換える
Money price = new Money(3000);
price.setValue(2000); // × 値を書き換えている
price = new Money(1000); // × 1つの変数に別の値を代入している
値が異なれば別のオブジェクトにする
Money basePrice = new Money(3000);
Money discounted = basePrice.minus(1000);
Money option = new Money(1000); // 新しくMoneyオブジェクトを作る
値オブジェクトを「不変」にするやり方
- インスタンス変数はコンストラクタでオブジェクトの生成時に設定する
- インスタンス変数を変更するメソッド(setterメソッド)を作らない
- 別の値が必要であれば、別のインスタンス(オブジェクト)を作る
このような設計のやり方を 完全コンストラクタ と呼ぶ - Javaでは、String/BigDecimal/LocalDateなど基本的なデータ型は、「完全コンストラクタ」スタイルの値オブジェクト
###「型」を使ってコードをわかりやすく安全にする
quantityの場所にまちがえてunitPriceを渡した場合、正しい結果を返さない
int amount(int unitPrice, int quantity) {
if(quantity >= discountCriteria)
return discountAmount(unitPrice, quantity)
return unitPrice * quantity ;
}
独自の型を使って意図を明らかにする
Money amount(Money unitPrice, Quantity quantity) {
if(quantity.isDiscountable())
return discount(unitPrice, quantity)
return unitPrice.multiply(quantity.value()) ;
}
- int型の代わりにMoney型とQuantity型を使う
- コードの意図が具体的になる
- intは業務の関心事ではなく、プログラミング言語とコンピュータのしくみに関係する関心事
- QuantityやMoneyは業務の関心事そのもので、妥当な数量とはどのようなもので、数量に対してどのような判断/加工/計算が必要になるかの業務知識を表現した値オブジェクト
複雑さを閉じ込める
コレクション型を扱うロジックを専用クラスに閉じ込める
List型の変数を1つだけ持った「顧客一覧」の専用クラスを独自に宣言します。
コレクション型のインスタンス変数を1つだけ持つ専用クラス
class Customers {
List<Customer> customers;
void add(Customer customer) { ... }
void removeIfExist(Customer customer) { ... }
int count() { ... }
Customers importantCustomers() { ... }
}
- Listを操作するロジックは、すべてこのCustomersクラスに集める
- 顧客の追加や削除、顧客数のカウント、条件付きの顧客の抽出などのロジック
- ロジックを変更するときの影響をCustomersクラスに閉じ込めやすくなる
- このように、コレクション型のデータとロジックを特別扱いにして、コレクションを1つだけ持つ専用クラスを作るやり方を コレクションオブジェクト あるいは ファーストクラスコレクション と呼ぶ
- コレクションを操作するロジックをコレクションオブジェクトに閉じ込めると、コレクションオブジェクトを使う側のコードが単純になる
コレクションオブジェクトを安定させる
- 値オブジェクトと同じようにコレクションオブジェクトも、できるだけ「不変」スタイルで設計する
コレクションへの参照は変更不可にして渡す
class Customers {
List<Customer> customers;
...
List<Customer> asList() {
return Collections.unmodifiableList(customers);
}
}
まとめ
- ソフトウェア設計の良し悪しが変更のしやすさに直結する
- コードの整理の基本は「目的ごとに分けること」
- ドメインに基づいた設計 が変更を容易にする
- 複雑なロジックは 専用クラスに閉じ込める
- 短いメソッド、小さなクラスを心がける
- 「わかりやすさ」と「安全性」を意識して設計する