精読「現場で役立つシステム設計の原則」(3/3)(画面設計/アプリケーション間連携/オブジェクト指向編)
現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法
ドメイン駆動開発をベースとした実践的な設計の原則を学べる良書です。この設計原則が頭に強く刻み込まれるまで繰り返し読み返そうと思います。
画面とドメインオブジェクトの設計を連動させる
画面アプリケーションの開発の難しさ
- 画面には様々な利用者の関心事が詰め込まれる
- 画面に引きずられた設計はソフトウェアの変更を大変にする(表示のためのロジックと業務ロジックが混在する、複数の画面に同じコードが重複する等)
- そうならないように、ここでも基本的な考え方は、関心事を分けて整理すること
画面の関心事を小さく分けて独立させる
- 画面の関心事を小さく分けて独立させる
例)注文登録時の大きなクラスと大きなメソッド
@Service
class OrderService {
void register(Order oder){
// 氏名の妥当性チェック
// 顧客番号の妥当性チェック
// 商品の妥当性チェック
// 数量の妥当性チェック
…
// すべてOKなら登録
}
}
このような肥大化したクラスとメソッドは変更が大変(配送手段を追加するとき、大きな注文クラスの「どこか」に追加し、大きな登録メソッドの「どこか」で配送手段の妥当性の検証のロジックを変更しなければならない)
例)注文に関するドメインオブジェクトと注文登録メソッドを分けて考える
対象 | ドメインオブジェクト | 登録メソッド |
---|---|---|
注文者 | Customer | void register(Customer customer) |
注文内容 | Items | void register(Items items) |
決済方法 | PaymentMethod | void register(PaymentMethod method) |
配送手段 | Delivery Specification | void register(DeliverySpecification specification) |
連絡先 | ContactTo | void register(ContactTo contactTo) |
注文の確定 | Order | void submit(Order order) |
このように関心事を分解して設計すると、配送手段を追加する変更は、対象をDeliverySpecificationクラスに限定できる
- 画面も分けてしまう
例)タスクベースのユーザーインターフェース
スマホの利用拡大と通信環境の変化により、タスクベースのインターフェースが増えている
アプリケーション間の連携
アプリケーションとアプリケーションをつなぐ
- アプリケーション間の代表的な連携方式は4つ(本書ではWeb API中心に説明していく)
方式 | 説明 |
---|---|
ファイル転送 | ファイルを使ってデータを受け渡す |
データベース共有 | 共通のデータベースを使ってデータを共有する |
Web API | HTTPをつかってリクエスト/レスポンス方式でデータをやりとりする |
メッセージング | メッセージング基盤を使って非同期にデータ(メッセージ)を送る |
Web APIのしくみを理解する
-
Web APIを使ってアプリケーション間で行う処理の基本は、データの取得とデータの登録の2つ
-
要求の対象を指定する(URI)
- 要求の対象を指定するのがURIで、対象となるデータのことがリソース(資源)
リソースの識別方法
形式 | スキーム://ホスト名/リソースへのパス |
例 | https://api.hoge.com/customers/123 |
意味 | HTTPSを使ってホストapi.hoge.comが持つID123の顧客データ |
- 要求の種類を指定する(HTTPメソッド)
- URIで指定したデータ(リソース)に対して、どのような操作を要求するかがHTTPメソッド
基本的なHTTPメソッド
HTTPメソッド | 説明 | 成功時のHTTPステータスコード |
---|---|---|
GET | データを取得する | 200 OK |
POST | データを登録する | 201 Created |
PUT | データを登録する | 200 OK/201 Created/204 No Content |
DELETE | データを削除する | 200 OK/202 Accepted/204 No Content |
- データを取得するGET
- データを登録するPOSTとPUTの違いは、対象の指定方法
メソッド | 対象の指定方法 | 説明 |
---|---|---|
POST | books | 書籍データを登録し、識別番号を発行してもらう |
PUT | books/1234 | 識別番号1234の書籍データを登録する |
- 更新の依頼もPOSTを使う(実際にどう更新するかはWeb APIを提供する側のアプリケーションの責任)
- データを削除するDELETEは、PUT同様にアプリケーション間の依存性を強くする[2]ため、 POSTメソッドで削除を依頼する方法がある(この方法だと、削除をどう実行するかの判断や、どのようにデータの削除を実現するかは依頼を受け取った側のアプリケーションの責任になる)
良いWeb APIとは何か
- 大は小を兼ねるAPI[3]は、利用する側の負担が大きくなる良くないWeb API
- 良いAPIは、さまざまなアプリケーションを組み立てるために役に立つことが重要(但し、組み立ての負担が増えすぎない適度の大きさの部品であるべき)
APIの粒度と組み立ての特性
APIの粒度 | 実現できる機能の多様性 | 組み立ての複雑さ |
---|---|---|
小さい | 幅広い | 複雑 |
大きい | 限定的 | 単純 |
発展性に富んだAPI開発のやり方
- 単純なことをかんたんにできるAPIの提供から始める
例)単純な用途を実現するAPI
会員番号を指定すると名前とメールアドレスを取得する単純なAPI
//リクエスト
GET members/123
//レスポンス
{
"name":"山田 太郎",
"mail":"hoge@hoge.com"
}
- 初期の検討段階からAPIを実際に作って利用する側に試してもらい、フィードバックをもとにAPIを修正/拡張していく[4]
- APIの利用側と提供側間の共同作業環境を整え[5]、APIの成長サイクルを回す
1. API原案の提示と意見交換
2. 合意したAPIの開発
3. テスト環境とドキュメントの自動生成
4. フィードバック
5. フィードバックをもとにした改良
6. 改良結果の確認 - 明確で単純な小さなWeb APIの設計原則(その1) 登録と参照を分ける
例)指定席を予約するAPI
予約がPOSTされたとき、POSTのレスポンスは予約番号だけを返し、予約内容は予約番号で別途GETする(予約内容の詳細をPOSTのレスポンスとするのは良くない)
予約と結果参照を分離したAPI
目的 | API | 説明 |
---|---|---|
予約の登録 | POST reservations | レスポンスとして予約番号2345を返す |
予約の確認 | GET reservations/2345 | 予約番号2345の内容を返す |
- 明確で単純な小さなWeb APIの設計原則(その2) リソース(データのかたまり)の単位を分ける
例)会員情報の登録と参照を行うWeb API
//会員情報の参照(氏名、性別、生年月日、住所を返す)
GET members/1234
このAPI設計では、氏名だけが必要な場合でも毎回性別/生年月日/住所を取得することになるため、用途別により小さな単位のデータを受け取るAPIを提供する
//用途別の小さな単位でAPIの例
GET members/1234/name //名前を返す
GET members/1234/gender //性別を返す
GET members/1234/dateOfBirth //生年月日を返す
GET members/1234/contactMethods //電話番号とメールアドレスを返す
GET members/1234/address //住所を返す
情報を変更する場合も、変更が起きそうなリソースに限定したAPIを提供する
//対象のリソースを限定したAPIの例
POST members/1234/contactMethods/telephone //電話番号の変更
POST members/1234/address //住所の変更
- Web APIのバーション管理はあまり意味がなく、リソースを小さな単位に分けるAPIでは新たな小さなAPIの追加が中心になる
例)古いAPIの廃止の予告と実施の流れ
- 新しいAPIを追加しても、互換性のため古いAPIも提供する
- 古いAPIは残すが、「303 See Other」を返すように変更する(新しいAPIの情報を返す)
- 古いAPIのレスポンスとして「404 Not Found」を返すように変更する
- API自体を削除する
ドメインオブジェクトとWeb API
- JSONとドメインオブジェクトでは、アプリケーションが異なる以上、データ構造や関心ごとの不一致はどうしても発生する
- ズレが大きい場合、変換用の中間オブジェクトを用意したほうが、コードをシンプルに保ちやすくなる
例)ドメインオブジェクトからレスポンスオブジェクトを生成する
class BookResponse {
...
static BookResponse fromBook(Book book){
//BookオブジェクトからBookResponseを生成する(ファクトリメソッド)
}
}
構造や項目の違いを、このファクトリメソッド[8]が吸収する
例)リクエストオブジェクトからドメインオブジェクトを生成する
class BookRequest {
...
Book toBook(){
//BookRequestからBookを生成する
}
}
HTTPでPOSTされたデータを、まずBookRequestオブジェクトにマッピングし、BookRequestクラスにドメインオブジェクトBookを返すtoBook()メソッドを用意する
複雑な連携に取り組む
- 接続相手が複数の場合、APIを3種類に分けて検討する
APIの種類 | 説明 |
---|---|
基本API(コア) | どの利用にも共通するAPI。関心事を小さな単位に分け、登録と参照を別のAPIにした、最小単位のAPIのセットとして提供する |
拡張API | 利用者がより使いやすいように基本APIを組み合わせた複合API。どの利用者にも共通に使えるものだけを用意する |
個別対応API | 特定の利用者のニーズを満たすAPIの集合。複合APIが中心になるが、場合によっては基本APIを特別に変更したAPIの提供も選択肢に入れる |
- APIの進化方法は、APIを基本/拡張/個別対応にグルーピングし、各グループ間でAPIを移動すること
例)APIの進化プロセス
- 複数の利用者に同じ様な個別対応APIを提供していることを見つける→拡張/基本APIへ移動
- 基本/拡張APIを特定の利用者だけが利用していることに気づく→個別対応APIへ移動
- 交換する対象のデータが複雑になり、属性情報をメタ情報も含めて扱いたい場合は、JSONだけではなくXMLも選択肢となる
- アプリケーション間の連携をAPIだけではなく、非同期メッセージングを使うことも選択肢の1つ
オブジェクト指向の開発プロセス
ドメインモデルを中心にしたソフトウェア開発の進め方
- ドメインモデルに業務ロジックを集めて整理する
- 分析と設計は同じ人間/チームが担当する体制
ソースコードを第一級のドキュメントとして活用する
- 分析者と設計者が同じであれば、多くのドキュメントは不要になる
- 正式なドキュメントは可能な限りソースコードに集中し、ホワイトボードに描いたラフスケッチの写真やコミュニケーションツールを使うことが確実で効率的な開発スタイル(オブジェクト指向らしい開発のやり方)
-
技術者以外の関係者と情報を共有する手段は、以下の3つ
- 利用者向けドキュメント…利用規約やユーザガイドは外部仕様書の役割を果たす
- 画面や帳票…詳細な要求の実態
- データベースのテーブル名/カラム名のコメント…コメントを丁寧に記述して、更新すれば重要なドキュメントになる
-
全体を俯瞰するドキュメント(システムの基本目的や方向性)を作成して関係者に共有する
- システム企画書やプロジェクト計画書のシステム概要説明
- プレスリリース
- リリースノート
- 利用者ガイドの導入部
- 営業ツールのキャッチフレーズ
分析と設計が一体になった開発のやり方をマネジメントする
- オブジェクト指向の変更容易性をより効果的に活かすには、準委任[11]の契約にすべき
- ソースコードを中心に進捗と品質をマネジメントする
- 対象業務の理解と整理に意欲がある技術者を選んで育成する
オブジェクト指向設計の学び方と教え方
既存のコードを改善しながらオブジェクト指向設計を学ぶ
- 「リファクタリング」を参考に、既存のコードを改善してみる
おすすめ参考書「リファクタリング 既存のコードを安全に改善する」
- 重複したコードはメソッドに抽出し、最適な場所に置く(第一候補は、抽出したメソッドが使うデータを持っているクラス)
- 長いメソッドはコードの重複の原因なので、メソッドに抽出する
-
巨大なクラスもコードの見通しを悪くし、変更を厄介にする
- インスタンス変更が多い[12]→メソッドとインスタンス変数をセットで切り出す
- 巨大なデータクラスを受け取っている→データを持つクラスにロジックを移動することを考える
- メソッドの引数が多い→すべての引数を持つ、引数の入れ物クラスを作る
- リファクタリングは部分的に少しずつやる
- 短いメソッドと小さなクラスを作れたら、小さな部品の改良を行う
- 名前の変更
- クラスにロジックを追加する
- 小さなクラスを束ねるクラス[13]を追加する
オブジェクト指向らしい設計を体で覚える
-
「オブジェクト指向エクササイズの9つのルール」で練習してみる
おすすめ参考書「ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション」
-
ルール1:1つのメソッドにつきインデントは1段落までにすること
- メソッドの抽出を徹底するため
- 一番深いインデントの処理をメソッドに抽出し、次のインデントをさらにメソッドに抽出する
-
ルール2:else句を使用しない
- ガード節や早期リターンを組み合わせて複文を単文にする
-
ルール3:すべてのプリミティブ型と文字列型をラップすること
- 値オブジェクトのこと
-
ルール4:1行につきドットは1つまでにすること
- 「else句を使わない」と同じく、複文構造を単文に分解することを狙ったルール
- ドットの1つごとに説明用の変数に代入して、別の分に分けること
-
ルール5:名前を省略しないこと
- パッケージ名/クラス名/メソッド名/変数名が利用者の関心事に一致していることが重要
-
ルール6:すべてのエンティティを小さくすること
- オブジェクト指向は、短いメソッドと小さなクラスでプログラムを組み立てる技術
エンティティを小さく保つガイドライン
対象 | ガイドライン |
---|---|
メソッドの行数 | 3行を目標にする 1行でもよい |
クラスの行数 | 50行を目標にする 100行以上は不可 |
パッケージのファイル数 | 10ファイル以内 |
-
ルール7:1つのクラスにつきインスタンス変数は2つまでにすること
- インスタンス変数とメソッドの関連付けを徹底するためのルール
- インスタンス変数とメソッドが密接に結びついたクラスは、目的が単純で、意図が明確になる
-
ルール8:ファーストクラスコレクションを使用すること
- コレクションオブジェクトのこと
- クラスを巨大にしないために、対象の配列/コレクションを1つだけ持つ独立したクラスにする
-
ルール9:getter、setter、プロパティを使用しないこと
- データクラスは諸悪の根源
- メソッドは、何らかの判断/加工/計算をしなければいけない
オブジェクト指向の考え方を理解する
- オブジェクト指向らしい設計に慣れてきたら、より深く学ぶために「実践パターン」「オブジェクト指向入門」「ドメイン駆動設計」を読むと良い(特に「ドメイン駆動設計」は、オブジェクト指向を現場で実践するための素晴らしい手引き)
参考
-
アプリケーションの独立性が高くなり、将来の修正や拡張の影響が小さくできるため ↩︎
-
削除の実際のタイミング、妥当性のルール、うまくいかなかった場合の挙動などの決め事が必要になるため ↩︎
-
指定パラメータや取得したデータの項目が多いAPI ↩︎
-
かつてはIF一覧を作り、IF毎に詳細仕様を定義し確認するためのドキュメントを事前に作ることが普通だったが、時間がかかるしフィットしない場合がある ↩︎
-
SwaggerUIなどのツールを使ってクイックにAPIを作り検証してもらう ↩︎
-
APIを提供する側が部品だけでなく、もっとまとまった機能をサービスとして提供すること ↩︎
-
APIを提供する側にAPIを利用する側の知識が入り込んでくる ↩︎
-
オブジェクト生成を容易にするデザインパターン。インスタンスの作り方をスーパークラスで定め、具体的な処理をサブクラスで行う ↩︎
-
CICDのスクリプトを自己文書化の対象とすれば、別途ドキュメントを作成し二重管理する必要がない ↩︎
-
監視ツールの設定/実行スクリプト、性能テストのコード、脆弱性診断、認証/認可のテスト、擬似的に障害を発生させ自動復旧することを確認するテストコード等 ↩︎
-
期間を決めて、その期間内の作業に対する対価を支払う ↩︎
-
画面やデータベースのデータを格納するために作ったデータクラスにロジックを追加した場合等 ↩︎
-
同じような組み合わせパターンを繰り返し利用している場合、束ね役のクラスを作ったほうがシンプルになる ↩︎
Discussion