はじめに
はじめはシンプルだったコードも積み重なる機能追加や変更、バグ修正等などによって、徐々にコードが複雑化し、修正コストの増加や品質低下に繋がります。
これはある程度の規模を持つプロダクトでは至って自然なことであり、そうならないようにするためには意識して設計しなければいけません。
ここではコードを複雑化させないために普段意識している手法やパターンを紹介します。(DDDやClean Architecture成分多め)
また本記事で登場するコードはJavaですが、Javaを知らなくてもある程度は理解できるかなと思います。
あと、割とまとまりもなく幅広い範囲でダラダラと書いてしまい長いです...
以下項目ごとのアンカーリンクとなっておりますので興味がある項目のみどうぞ。
- getter/setterがよくない理由
- デメテルの法則と尋ねるな命じよ(Tell, Don't Ask!)
- 関心の分離 : Separation of Concerns (SoC)
- 依存関係とその方向
- 良いDRYと悪いDRY
- 名前重要
getter/setterがよくない理由
ビジネスロジックが絡むところでのgetter
ここで言うところのgetter
とは外部から参照できるインスタンス変数のことを指します。「ビジネスロジックが絡むところで」と限定しているのはフレームワークの制約などで一概には対応できない場合があるためです。
public class Employee {
private String name; // 外部から参照できないのでOK
private LocalDate contractDate;
public int salary; // publicなインスタンス変数は外部から参照できるので禁止
...
// getterで外部からインスタンス変数を参照できるので禁止
public LocalDate getContractDate() {
return this.contractDate;
}
}
この例の getContractDate
「従業員の契約日を取得する」という処理には業務的な意味を何も持っていません。
従業員の契約日を取得して何がしたいのかが分からないからです。ここで実現したことは、契約日を使った従業員の判別なのか? 契約日による従業員情報の変更なのか?? 等など、これだけでは意味のないものになってしまっています。これは取得したデータの用途は利用側に丸投げしていることと同義であり、設計の先送りです。
では、「設計の先送り」が発生しているクラスを利用する側を見てみます。
int salary;
if (employee.getContractDate().isBefore(LocalDate.now())) {
// 契約済み従業員の給料を取得する (当日を含んでないですが細かいことはあれで...)
salary = employee.salary;
}
上記のようにemployee
を使いたいクラスにビジネスロジックが漏れ出してしまいます。今後も同じ用にemployee
を使いたいクラスが出てくると、同じようなコピペコードが至るところに量産されてしまいます。
それを防ぐためにも以下のようにデータを持つクラスに直接ビジネスロジックを書きます。
public class Employee {
private LocalDate contractDate;
private int salary;
...
public int calculateSalary() {
if (this.contractDate.isBefore(LocalDate.now())) {
return this.salary;
}
return 0; // この返し方はイマイチですが、例ということで...
}
}
int salary = employee.calculateSalary();
getter
をやめて、データを保持しているクラスにビジネスロジックを書くようにすることで、コピペコードの量産を防ぎ、該当クラスを見ただけでビジネスルールを知ることができるようになります。
ビジネスロジックが絡むところでのsetter
ここで言うところのsetter
も同様に外部から値を変更できるインスタンス変数のことを指します。
public class Employee {
public LocalDate contractDate; // publicなインスタンス変数は外部から代入できるので禁止
private int salary;
...
// setterで外部からインスタンス変数を代入できるので禁止
public void setSalary(int salary) {
this.salary = salary;
}
}
こちらでも setSalary
「従業員の給与を変更する」という業務的な意味を持たないロジックになっています。例えば「業績が良かったので上乗せする」や「入力を間違えたので訂正する」など業務上のフローやルールを表現したメソッドにするべきです。
またsetter
によって値が変更されるおそれがあるとということは状態の変更が発生するということなので、安全ではなくなります。特に以下のように処理が深くなるに従って、簡単に人が認識できる状態を超えてくるので、可読性が落ちます。
methodA(Employee employee)
└ methodB(Employee employee)
└ methodC(Employee employee)
└ methodD(Employee employee) <- employeeの状態をここで書き換えられたらもう無理!追いきれない!
└ methodE(Employee employee)
完全コンストラクタとイミュータブルオブジェクトでsetterを避ける
完全コンストラクタ : Complete Constructor
完全コンストラクタとは「コンストラクタで全てのインスタンス変数の値を確定させる」ということです。
特にビジネスルールを扱うクラスではコンストラクタで以下の状態を作り出すことを保証します。
- すべてのインスタンス変数の値が決まる
- ビジネスルールとして矛盾がない
- インスタンス変数の値は範囲やビジネスルールに基づく計算可能な有効値である
簡単な例として「従業員は必ず契約しており(契約日があり)、また入社予定として3ヶ月先までの契約ができる」というビジネスルールを表現するクラスを定義します。
public class Employee {
private LocalDate contractDate;
public Employee(LocalDate contractDate) {
if (contractDate == null) {
throw new IllegalArgumentException("契約日は必須です");
}
LocalDate currentDate = LocalDate.now();
if (contractDate.isAfter(currentDate.plusMonths(3))) {
throw new IllegalArgumentException("3ヶ月先を超える契約日は不正です");
}
this.contractDate = contractDate;
}
}
このように従業員の契約日は必ず必要、かつ3ヶ月先を超える日付は制限されるようなコンストラクタの設計をすることで、このインスタンスを使う際は強制的にビジネスルール上の制約が守られた、安全なインスタンスであることが保証されます。
また、どのような値が来ても安全で予測可能なインスタンスになり、利用側は余計なことを気にすることなく使用できます。
一度インスタンスを生成した後で、状態を変更したり、一部のフィールドを徐々に組み立てていくなどの操作は、安全性や可読性を下げます。インスタンス生成は常にアトミック操作で完結するべきです。
コンストラクタの代わりのstaticファクトリメソッド
コンストラクタに関連して、staticファクトリメソッドを紹介します。
ビジネスルール的にオーバーロードしたコンストラクタを複数作りたいときがあると思います。以下の例のコメントに書いてあるように「○○の場合はこのコンストラクタ」「△△の場合はこのコンストラクタ」という感じにです。
public class SearchDateTime {
private LocalDate date;
private LocalTime time;
// 当日以外の場合はこのコンストラクタを使ってほしい...
public SearchDateTime(LocalDate date, LocalTime time) {
this.date = date;
this.time = time;
}
// 当日の場合のみコンストラクタを使ってほしい...
public SearchDateTime() {
this.date = LocalDate.now();
this.time = LocalTime.now();
// 関係ないけどコンストラクタチェーンしよう!
// this(LocalDate.now(), LocalTime.now());
}
}
上記の用にコンストラクタが複数あった場合、コメントには書いてありますが、利用側は引数の違いだけでどちらを使えばよいのか、わかりにくいものになっています。
これを解決するのが表題の「コンストラクタの代わりのstaticファクトリメソッド」です。
以下のように利用用途ごとに命名した staticなファクトリメソッドを用意します。
public class SearchDateTime {
private LocalDate date;
private LocalTime time;
public static SearchDateTime of(LocalDate date, LocalTime time) {
return new SearchDateTime(date, time);
}
public static SearchDateTime ofToday() {
return new SearchDateTime(LocalDate.now(), LocalTime.now());
}
// コンストラクタは private に可視性を下げて、外部には公開しない
private SearchDateTime(LocalDate date, LocalTime time) {
this.date = date;
this.time = time;
}
}
当初のコンストラクタはprivate
に可視性を下げて、外部から直接利用できないように制限します。
staticなファクトリメソッドの利点は 命名できることにあります。名前を明示的にすることで、曖昧なものにせず利用側に意図を伝えることができます。
また他のユースケース例として、管理者ロールと通常ロールで操作させるオブジェクトを制限させたりなど、誤ってはいけない操作を命名と引数の型によって明示させたりするなども有効だと思います。
イミュータブルオブジェクト
一度決まった値から変更されないことを保証することで、安全性を高めることができます。
インスタンス変数にfinal
修飾子を定義することで、コンストラクタでの初期化後に値が変更されることがなくなります。
public class Employee {
// すべてのインスタンス変数にfinal修飾子をつける
private final String name;
private final LocalDate contractDate;
private final int salary;
public Employee(String name, LocalDate contractDate, int salary) {
this.name = name;
this.contractDate = contractDate;
this.salary = salary;
}
public void addSalary(int salary) {
this.salary += salary // コンパイルエラー!!
}
}
setterをやめて、完全コンストラクタかつイミュータブルなオブジェクトにするのも、一番の目的は状態変更を気にしないでもよくするためです。
人が一時的に覚えておける情報量は少ないので、なるべく気にすることを減らすことが大事です。
これで値を不変にしたイミュータブルオブジェクトができましたが、値の変更を行いたい場合はどうするかというと、新たなインスタンスを生成して返します。あくまでも自分自身の値を変更しません。
public class Employee {
private final String name;
private final LocalDate contractDate;
private final int salary;
...
// 再契約して契約日と給与を変更する
public Employee contractRenewal(int salary) {
return new Employee(this.name, LocalDate.now(), salary);
}
}
// 利用側
Employee employee = new Employee(...);
Employee extendedEmployee = employee.contractRenewal(200000); // 別インスタンスとして扱います。
デメテルの法則と尋ねるな命じよ(Tell, Don't Ask!)
「デメテルの法則」と「尋ねるな命じよ」は両方とも情報隠蔽に関連する設計原則です。
はじめにこの設計原則が守られていない例と守られている例のイメージを見てみます。
設計原則が守れてない例
- Serviceが問い合わせしている
- Serviceが保有しているオブジェクト以外 (ModelB, ModelC) に参照している
設計原則が守れている例
- 命じている
- ServiceはModelBとModelCを参照していない
この設計原則を守るときれいなV字になることがわかります。
次にコードで見てみます。
簡単な例として「クーポン料金を持つ商品の販売価格を求める」という場合のロジックを表しています。
Product product = new Product(something);
int sellingPrice;
if (product.price.couponExpiration.isAfore(LocalDate.now())) {
// クーポンの有効期限が有効なら商品のクーポン価格を返す (こちらも当日を含んでないですが細かいことはあれで...)
sellingPrice = product.price.couponValue;
} else {
// クーポンの有効期限が無効なら製品価格を返す
sellingPrice = product.price.value;
}
上記では、クーポンの有効期限を判定するためproduct.price.couponExpiration
でif文による問い合わせが発生しています。さらにproduct
が保有するprice
にもアクセスしています。
このコードに対して、「デメテルの法則」と「尋ねるな命じよ」を適用して改良されたコードは以下のようになります。
Product product = new Product(something);
int sellingPrice = product.sellingPrice();
上記の「デメテルの法則と尋ねるな命じよが守られた利用側ロジック」では、1行の命令で完結しています。利用側は必要最小限の知識しか与えられておらず、ただ「販売価格を求める」という命令を呼び出すだけで済みます。
この「デメテルの法則と尋ねるな命じよ」が組み込まれたProduct
クラスは以下のような形になります。
public class Product {
private Price price;
...
int sellingPrice() {
return price.sellingPrice();
}
}
class Price {
private int value;
private int couponValue;
private LocalDate couponExpiration;
...
int sellingPrice() {
if expirationDate.isAfore(LocalDate.now()) {
return couponValue;
}
return value;
}
}
元々、利用側にあった分岐等の判断処理はPrice
クラスが担当し、Product
クラス内でコンポジションされています。計算や判定などのビジネスロジックはデータを持つクラスが担当するということです。
さらにPrice
クラスはパッケージプライベートで定義され、sellingPrice
メソッドも同様にパッケージプライベートです。つまり利用側(別パッケージと前提)からはこのクラスやメソッドが見えず、直接実行することが許可されておりません。こうすることで強制的に知識の露出を減らし依存度を下げ疎結合にすることができます。
関心の分離 : Separation of Concerns (SoC)
関心の分離といえば、MVCやレイヤードアーキテクチャなどで語られることが多いと思いますが、どのような粒度にせよ意識したい原則です。
例えば「従業員」を扱うクラスがあるとします。
この「従業員」クラスにおいては、仮に業務エラーが発生した場合、「どう通知するか」については、関心事ではありません。
APIであればJSONのような形式にメッセージを詰めるべきでしょうし、WEBサイトのように画面を表示するものであれば、ユーザが認識しやすいように修飾して出力すべきでしょう。
この「従業員」クラスにおいては、「業務エラーが発生したこと」と「業務エラーの内容(エラーメッセージ)」までが知っているべき関心事になります。それをどのように出力して、どのように修飾するかは、別のクラス(例えばPresentation層)の関心事です。
上記、関心の分離を意識せずに「従業員」クラスに利用側の画面の処理が書かれていると、画面の出力方式に変更があった場合、本来関係ないはずの「従業員」クラスにも修正が発生し、影響範囲が広がります。SOLID原則の一つである、単一責任の原則にも反することになります。
https://qiita.com/UWControl/items/98671f53120ae47ff93a
このように役割に応じて関心を分離し、どこまでの責任を持つかの境界線を決め、線引することで、外部との依存度が少なくなり、疎結合で安全なアプリケーションを設計することが可能です。知識の露出を減らすことにも繋がります。
依存関係とその方向
TL;DR
ちょっと長いので。
- 循環依存が生じないようにする
- より安定したコンポーネントに依存するようにする
- 依存先が少ないほど安定する、または自身が依存されてるほど安定する
- 不安定なコンポーネントが依存されていると変更影響が大きくなるので、 安定したコンポーネントからは依存してはいけない
- それらを解決するために、依存方向を逆転させる
コンポーネントの関連を扱う原則
「コンポーネント」とはモジュールと言ったり、jarやgemの単位だったりするものを指します。
ただコンポーネントに限らず、あらゆるオブジェクトにも適用すべき内容になります。そのためここではクラスを基準に説明します。
非循環依存関係の原則 : Acyclic Dependencies Principle (ADP)
コンポーネントの依存グラフに循環依存があってはならない
という原則があります。
「コンポーネント」に限らず、あらゆる構造において、循環依存が生じないように設計すべきです。
依存は必ず内側に向かって、循環させずに一方向のみに依存するようにします。内側から外側を知ることはなく、外側が持つ知識やルールを内側に持ち込まないようにすることが大事です。
安定依存の原則 : Stable Dependencies Principle (SDP)
安定度の高い方向に依存する
クラスで考えると、依存しているクラスが少ないほど安定します。また自身のクラスが依存されてるほど安定しています。
特に後者(自身のクラスが依存されてるほど安定)の理由に関しては、変更の影響が大きく変更しづらいため、安定せざるを得ないというためです。
(言語よりますが、importやrequire、includeの数は一定の基準になると思います。)
そのため、変更を想定したクラスは、変更しづらいクラスからは依存されてはいけません。
安定度・抽象度等価の原則 : Stable Abstractions Principle (SAP)
コンポーネントの抽象度は、その安定度と同程度でなければいけない
安定度の高いコンポーネントは抽象度も高くあるべきで、安定度の高さが拡張の妨げになってはいけない、という原則です。
逆に言うと、安定度の低いコンポーネントはより具体的であるべき、ということになります。安定度が低いことによってその内部の具体的なコードが変更しやすくなるためだからです。
「安定依存の原則(SDP)」では、安定度の高い方向に依存すべきで、「安定度・抽象度等価の原則(SAP)」では、安定度は抽象度に比例すべきということなので、つまりは 抽象度が高くなる方向に依存すべき ということになります。
依存関係逆転の原則 : Dependency Inversion Principle (DIP)
「安定依存の原則(SDP)」や「安定度・抽象度等価の原則(SAP)」を実現するために依存関係の方向を変更する必要があります。その方法としてインターフェースを導入することで、依存関係の方向を逆転させ、これを実現します。
DIP適用前を見てみましょう。
上記の「変更前」では「安定したクラス」が「修正が多い不安定なクラス」に依存しているため、後者のクラスに変更があった場合、前者のクラスにその影響を与えてします。
具体的には以下のようの例です。
- 「修正が多い不安定なクラス」が外部に公開しているメソッドの戻り値を変更すると「安定したクラス」も修正しなければならない
- 「修正が多い不安定なクラス」がAPIコールしている場合、別のAPIに交換すると「安定したクラス」も修正しなければならない
このような問題に対して、依存関係を逆転させて問題を解消した状態がこちらです。
インターフェースを用いて抽象に依存させたことによって、「修正が多い不安定なクラス」は直接依存されなくなっています。「修正が多い不安定なクラス」をどんなに修正しても、インターフェースを継承している限り「安定したクラス」の修正は不要です。
ここで重要なことは、「安定依存の原則(SDP)」や「安定度・抽象度等価の原則(SAP)」にも書いてありますが、依存される抽象クラスは極めて安定していなければいけません。なので、この部分の設計が重要になります。
依存関係逆転の原則(DIP)を実際に適用する場合について
個人的にはこの依存関係逆転の原則(DIP)を何でもかんでも適用する必要は無いかなと考えています。
特に現状シンプルな構成だった場合、1階層抽象的なオブジェクトが入ることでかえってわかりづらさを生む場合があるからです。
実際の具体例として、依存方向を逆転させる場面としては「このクラスを依存したくないな」と思ってしまうような、以下の場合が多いかなと思います。
- 頻繁な仕様変更、またはバクが多く安定していないクラスがあり、その影響を受けている別のクラスがある
- フレームワークが提供する機能など外部ライブラリを使用している
- 頻繁に仕様が変わったり、または外部など管理外にあるAPIアクセス
これはプロジェクトとして「どこまでフレームワークにロックされてもよいか」など事前にある程度方向性を決めておくべきでしょう。
良いDRYと悪いDRY
一般的にソースコードにおける「重複は悪」とされています。「Don't Repeat Yourself」DRY原則と言われるやつです。
例えば、画面の構成が同じような複数のユースケースがあるとします。作った当初は同じ処理をDRYにするために共通化するとします。
しかし、その時点では問題なかったコードですが、それぞれのユースケースの要求が異なるため、共通ロジックに利用側それぞれの個別の意図を持った場合分けのif分岐が入り、数年後には判断分岐だらけの肥大化した共通だったロジックに変わることがあります。
これを避けるためには、ビジネスが関心を持つ領域は何なのかという、プロダクトが扱う問題領域(ドメイン)を明確にする必要があります。
その上で 偶然に重複しているのか、本当に重複しているのかを慎重に見極める必要があります。前者の場合は偶然に重複しているだけなので、DRYにする必要性はありません。
これはプロダクトが大規模になればなるほど発生しやすい問題です。特にビジネスルールを扱うクラスにcommon
やbase
などという名前が入っていたりするのは要注意です。
名前重要
なんだかんだでプログラミングする上で最重要事項です。
名前に関する内容はさんざん他の記事でも語られているので、概要に留めますが、以下の書籍は大変参考になるかと思います。
-
リーダブルコード
: 基本中の基本を分かりやすく体系立てて言語化してくれています - CODE COMPLETE : 命名について10項目以上を割いて、これほど深堀りした書籍は他にないです(多分)
その上で「ビジネスルールを扱う部分」の命名について、まとめます。
- パッケージ名/モジュール名がドメインが扱う問題領域の名前と一致している
- クラス名が問題領域で扱う関心事と一致している
- メソッド名が実現したい振る舞い、知りたい知識になっている
- 汎用的、曖昧、幅広い意味を持つ名前になっていない
DDDでは「戦略的設計」と「戦術的設計」というのがあります。
- 戦略的設計 : コアドメイン、境界づけられたコンテキスト、コンテキストマップ
- 戦術的設計 : 実際にビジネスルールを実現するための設計パターンやアーキテクチャデザイン
重要なのは「戦略的設計が定まっていないと適切な名前が決められない」ということです。
DRYの項目でも記載しましたので再掲になりますが、自分たちのビジネスが関心を持つ領域は何なのか、更にその領域内で分割できるものはあるのか、などプロダクトが扱う問題領域(ドメイン)を明確にして、それに従ったパッケージ名やクラス名を決めていくという作業が良い命名を行うためには必要です。
最後に
これまで述べてきた手法やパターンは適用した1回で終わりにせず、しっくりこなければ何度でも見返すことが大事です。
その時は良いコードを書いたつもりでも後日に見返すとイマイチなコードになってたりするというのは、あるあるだと思います。ただ、このような設計を意識してきたコードでは以前に比べ修正コストが格段に下がり、変更が容易になっているはずです。常に改善を実施し、小さなリファクタリングを繰り返すことが大事です。