現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法
を読んだので学んだことをアウトプットしていこうと思います。
こちらの記事に細かく書かれていたので、細かいことは記載しておりません。
+個人的な意見も含まれております。
第1章 小さくまとめてわかりやすくする
①値オブジェクト(Value Object)
値を扱う専用クラスを作成する。
まず、↓この書き方は怪しい。
int quantity;
BigDecimal amount;
なぜなら実務でint の「マイナス21億からプラス21億の範囲の整数」を扱うことは少ないから。たとえば、quantityが商品数量を表しているなら、「0から100」ぐらいが現実的だと思われる。
そのため、専用クラスとしてはこのような記載にするほうが良い。
class Quantity{
//0未満、100より大きい値が記載されたら例外として扱う
}
専用クラスを使って、宣言はこのような形にする。
Quantity quantity;
②完全コンストラクタ
・インスタンス変数はコンストラクタでオブジェクトの生成時に設定する
・インスタンス変数を変更するメソッド(setterメソッド)を作らない
・別の値が必要であれば、別のインスタンス(オブジェクト)を作る
⚫︎悪い例(変数の値を書き換える)
Money price = new Money(3000);
price.setValue(2000); // 値を書き換えている
price = new Money(1000); //1つの変数に別の値を代入している
⚫︎良い例(値が異なれば別のオブジェクトにする)
Money basePrice =new Money(3000);
Money discounted = basePrice.minus(1000); //minusは別オブジェクトを作成して返す
Money option = new Money(1000); // 新しくMoneyオブジェクトを作る
③ファーストコレクション/コレクションオブジェクト
・コレクション操作のロジックをコレクションオブジェクトに移動する
・コレクション操作の結果も同じ型のコレクションオブジェクトとして返す
・コレクションを「不変」にして外部に渡す(Collection.unmodifiableListを使用する等)
⚫︎悪い例(コレクションの参照をそのまま渡すメソッド)
class Customers{
List<Customer> customers;
...
List<Customer> getList(){
return customers;
}
}
⚫︎良い例(コレクション操作の結果を同じ型のコレクションオブジェクトを作って返す)
class Customers{
List<Customer> customers;
...
Customers add(Customer customer){
List<Customer> result = new ArrayList<>(customers);
return new Customers(result.add(customer));
}
第2章 場合分けのロジックを整理する
①ガード節
else句をなくして単純にする。
⚫︎悪い例(else句を使った書き方)
Yen fee(){
Yen result;
if(isChild()){
result = childFee();
} else if(isSenior()){
result = seniorFee();
} else {
result = adultFee();
}
return result;
}
⚫︎良い例(else句をなくした書き方)
Yen fee(){
if(isChild()) return childFee();
if(isSenior()) return seniorFee();
return adultFee();
}
②列挙型(enum)を使用する
区分を扱う際に、インターフェースを継承させて同じ型(多態)とすることもできるが、使う側の処理でif文が必要になる。例えば、料金をインターフェースとして子供/大人料金の区分を実装し呼び出すときなど。
そのためこのように書くのが良い。
class FeeFactory{
static Map<String,Fee> types;
static{
types.put("adult", new AdultFee());
types.put("child", new ChildFee());
}
static Fee feeByName(String name){
return types.get(name)
}
}
呼び出す側ではこう。
FeeFactory.feeByName("adult")
状態遷移を行う際もenumで行う。
第3章 業務ロジックをわかりやすく整理する
三層+ドメインモデルで記述しよう。
業務アプリケーションでは、プレゼンテーション層/アプリケーション層/データソース層の三層アーキテクチャが一般的。
・プレゼンテーション層→画面や外部接続インターフェース
・アプリケーション層 →業務ロジック、業務ルール
・データソース層 →データベース入出力
三層アーキテクチャでデータを保持するだけのデータクラスを使用すると、いずれの層からもデータクラスを参照できる。そうすると、データクラスから取得した値を用いたロジックがいたるところに点在し修正するのが大変になる。例えば、getFirstName()メソッドで取得した値を条件分岐で使用したりするなど。
⚫︎悪い例(データを保持するだけのデータクラス)
class Person{
private String firstName;
private String lastName;
String getFirstName(){
return firstName; // インスタンス変数をそのまま返す
}
String getLastName(){
return lastName; // インスタンス変数をそのまま返す
}
}
じゃあどうしたらいいか?getterメソッドを記述しないようにして、メソッドにロジックを持たせる。メソッドは本来、インスタンス変数を使用するロジックを保有するべき。
⚫︎メソッドにロジックを持たせる
class Person{
private String firstName;
private String lastName;
String fullName(){
return String.format("%s %s", firstName, lastName);
}
}
このように業務データとロジックを1つのクラスにまとめたオブジェクトをドメインオブジェクトという。さらに、ドメインオブジェクトをまとめて整理したものをドメインモデルと呼ぶ。
三層+ドメインモデルにして関心事を分離することで、構造をシンプルにする。
・プレゼンテーション層→UIなど外部との入出力を受け持つ
・アプリケーション層 →業務機能のマクロな手順の記述
・データソース層 →データベースとの入出力を受け持つ
・ドメインモデル →業務データと関連する業務ロジックを表現したドメインオブジェクトの集合
ドメインモデルに業務ロジックが集まるため、画面やデータベースの都合から独立させることができ、3層の記述がシンプルでわかりやすくなる。
第4章 ドメインモデルの考え方で設計する
ドメインモデルの考え方
手続き型の設計ではデータモデルと機能クラスに分けて、機能クラスで判断/加工/計算を行う。
ドメインモデルでは業務データとそのデータに関連する判断/加工/計算の業務ロジックを1つのクラスに集めて行う。
年齢と生年月日を例にすると、手続き型の設計だと、生年月日をもとに年齢の計算を行う。生年月日データを参照できる場所であればどこでも年齢の計算ロジック記述ができてしまう。
ドメインモデルでは、年齢に関する記述できるのは年齢クラスだけ。そのため、年齢に関するロジックは年齢クラス内に閉じ込めることができ、修正が必要になった際も年齢クラスを確認すれば良い。
ドメインモデルの作り方
手続き型の設計では、全体から詳細へと段階的に分割しながらトップダウンで定義を進めていく。
ドメインモデルのオブジェクト指向では個々の部品を作り始め、それを組み合わせながら、ボトムアップで全体を作っていく。とはいえ、全体を行き来しながら作成していく。
ドメインオブジェクトの見つけ方
ヒト/モノ/コトのうちコトに注目すると全体の整理がしやすい。
・ヒト→個人、企業、担当者など
・モノ→商品、サービス、店舗、場所、権利、義務など
・コト→予約、注文、支払、出荷、キャンセルなど
コトに注目すると、次の関係も明らかになる。
・コトはヒトとモノとの関係として出現する(例:個人の商品についての予約など)
・コトは時間軸に沿って明確な前後関係も持つ
第5章
アプリケーション層のクラスをサービスクラスと呼ぶ。
・プレゼンテーション層に業務ロジックを記載すると変更が容易ではなくなるため記載しない。
・業務ロジックはドメインモデルに記載する。
・サービスクラスのメソッドは小さく分けて必要があれば組み合わせる。
・サービスクラスはごちゃごちゃしやすいので、シンプルな設計になるように心がける。
・サービスクラスでデータベースの操作を行う詳細は記述せず、データソース層で行う。
要は設計をシンプルにしたいから役割ごとに分けろよってことかと思います。
第6章
データベースの設計では制約を必ず使う。
・NOT NULL制約
・一意制約
・外部キー制約
データモデリングの概念で正規化があるが上記3つを満たすようにすれば、自然と正規化されたテーブル設計になる。
・全てのカラムはNOT NULL、かつ一意制約となるようにし、NULLが入りそうなカラムや重複しそうなデータがある場合は別テーブルに分けることを検討する。
・このとき、単にデータを分割しただけだと、データとデータの関係が失われる。データ間の関係を記録するために、外部キー制約を使用する。
データベースへの記録の工夫
・記録のタイミングが異なるデータはテーブルを分ける。
(記録のタイミングが異なるとNULLになるカラムが出現する可能性があるため)
・記録の変更を禁止する
過去の記録を修正する場合はinsert文を使用し、元データ、取消データ、新データの3つが記録されるようにする。
・カラムの追加はテーブルを追加する。
注意点
オブジェクトとテーブルの設計は似ているが、アプローチの仕方が違うため同じものではない。
オブジェクトは小さな部品をボトムアップ式で組み立てていく。
テーブルは関連するデータを洗い出して整理するトップダウン式で設計していく。
第7章 画面とドメインオブジェクトの設計を連動させる
小さく分ける
1つの画面ではなく、複数の画面に分けるタスクベースのユーザインターフェースを採用するべき。
なぜなら、スマートフォン利用時に、何かの都合で操作を中断するといったようなことがあるため、1つの画面に情報を集めるのは使いにくい。
例えば、1つの画面で下記項目の登録を一括で行うよりも、それぞれの登録画面があり、自分都合でバラバラに登録できるほうが使いやすい。
・顧客氏名
・注文内容
・決済方法
・配送手段
・連絡先
・注文
連動させる
表示用のロジックを持つビュー専用オブジェクトを用意する選択肢もあるが、画面表示にはドメインオブジェクトをそのまま使うのを重視するべき。もし画面とドメインオブジェクトが一致していない場合、ドメインオブジェクトを改善するか、ドメインオブジェクトを画面に反映させるか検討する。
画面以外の利用者向けの情報もソフトウェアと整合させる
利用者の関心事を表現したものとして以下のものもある。
・プレスリリース
・リリースノート
・利用者ガイド
利用者が確認できるこれらの3つと開発者にしか見えないプログラムの構造や記述が一致することがゴール。
第8章 アプリケーション間の連携
アプリケーション間の連携方式は4つあり、WEB APIが広く使われている。
・ファイル転送
・データベース共有
・Web API
・メッセージング
WEB APIでの登録はPOST、参照はGETを使う。(PUTとDELETEは決め事が多いので使用しない。)
POST members --会員情報を渡してレスポンスとして会員番号1234を受け取る
GET members/1234 --会員番号1234の内容を返す
ただ、このままだと会員情報をすべて取ってきてしまうので小さな単位で分けられているほうがよい。
GET members/1234/name
GET members/1234/gender
GET members/1234/birthDate
連携するアプリケーションが複数ある場合はより柔軟な非同期メッセージングを検討すると良い。
第9章 オブジェクト指向の開発プロセス
・分析工程(要件定義)と設計工程を分割せず、同じ人間が担当する
・分析設計はソースコードで直接表現することで作成する文書を最小限にする
・更新するべきは利用者ガイドや業務マニュアル
・ソースコードを中心に進捗と品質管理を行う
第10章 オブジェクト指向設計の学び方と教え方
・『リファクタリング』の本や現場のコードを改善して実践的にオブジェクト指向を学ぶ
・『実装パターン』、『オブジェクト指向入門』、『ドメイン駆動設計』を読んでみる
まとめ/感想
見えている世界が広がりました。。
本書に言葉は出てきませんでしたが、宣言型プログラミングやアジャイル開発はこういうものか!!!という印象を受けました。
オブジェクト指向言語を手続き型プログラミング(命令型プログラミング)で表現していたり、とにかく設計書などの文書を作るv字モデルの現場が今まで多かったので、不要な文書を書かずソースコードを文書化できたら最高だなと思いました。。。そのような現場に行けるように勉強しようと思えた本でした。