※この記事は著者の増田さんの了解の上で限定公開させて頂いております。
オブジェクト指向、設計がなぜ必要か = ソフトウェア全体の整理整頓をするため
第1章 小さくまとめてわかりやすくする
-
変更が大変なプログラムの特徴
-
メソッドが長い
-
クラスが大きい
-
引数が多い
- 関心事を詰め込みすぎている
- ちょっとずつゴミコードが追加されていった結果
- 重複しているコードをutil神クラスに押し込むと、あらゆる関心事が集中してしまう
-
-
変更に強いプログラムの書き方
- メソッドは短く、クラスは小さく
- 略語は使わない
- 意味のまとまりで空行をうまく使う
- 説明用のローカル変数の導入(変更の影響範囲を局所化)
- 1つの変数に代入を繰り返す破壊的代入を避ける
- 意味のあるコードのまとまり(段落)を「メソッド」として独立させる
- 業務の関心事に対応したクラス(ドメインオブジェクト)を作る
- メソッドは短く、クラスは小さく
-
値を扱うための専用クラスを作る(ValueObject)
-
まとめ
- オブジェクト指向設計は変更を楽で安全にする工夫
- コードの整理の基本は名前と段落
- 短いメソッド、小さなクラスを使ってコードを整理する
- 値オブジェクトでわかりやすく安全にする
- コレクションオブジェクトで、複雑なロジックを集約して整理する
- クラス名やメソッド名と業務の用語が一致するほど、プログラムの意図がわかりやすくなり、変更が楽で安全になる
第2章 場合分けのロジックを整理する
-
区分や種別がコードを複雑にする
- 区分や分類は、対象ごとに異なる業務ルールを適用するため(例:特別な顧客だけに割引ルールを適用したり、特定の地域向けの送料を割増にする)
-
区分毎の業務ルールのコードを整理する
- 判断や処理のロジックをメソッドに独立させる
- else句を使わずに条件分岐を単純化する(ガード節)
- 区分毎のロジックを多態性を使って別クラスにわける
- さらに列挙型の区分オブジェクトを使って区分を整理する
- 状態の遷移ルールを分かりやすく記述する
- EnumSet#of()で指定した複数の状態を含むSetクラスオブジェクトを生成できる
- ある状態から遷移可能な状態(複数)をSetで宣言する
- 遷移元の状態を「キー」に、遷移可能な状態のSetを値(バリュー)にしたMapを宣言する
- 応用が効く例
- あるイベントがその状態で起きてよいイベントか起きてはいけないイベントかの判定
- ある状態で発生してもよいイベントの一覧の提示
- 応用が効く例
class StateTransitions { Map<State, Set<State>> allowed; { allowed = new HashMap<>(); allowed.put(審査中, EnumSet.of(承認済, 差し戻し中)); allowed.put(差し戻し中, EnumSet.of(審査中, 終了)); allowed.put(承認済, EnumSet.of(実施中, 終了)); allowed.put(実施中, EnumSet.of(中断中, 終了)); allowed.put(中断中, EnumSet.of(実施中, 終了)); } boolean canTransit(State from, State to) { Set<State> allowedStates = allowed.get(from); return allowedStates.contains(to); } }
-
まとめ
- 区分ごとのロジックはプログラムを複雑にするやっかいな存在
- 早期リターンやガード節を使うと区分ごとのロジックをわかりやすく整理できる
- 区分ごとに別のクラスに分けると独立性を高めることができる
- 多態を使うと、区分ごとのロジックをif文/switch文を使わずに記述できる
- Javaの列挙型(enum)は多態をシンプルに記述するしくみ
- 列挙型を活用すると、区分ごとの業務ロジックをわかりやすく整理して記述できる
第3章 業務ロジックをわかりやすく整理する
データとロジックを別のクラスに分けることがわかりにくさを生む
-
業務アプリケーションのコードの見通しが悪くなる原因
-
データクラスのいろいろな呼び方
<img width="400" alt="スクリーンショット2" src="https://user-images.githubusercontent.com/4298012/71634649-286e7980-2c61-11ea-9b1e-54e1baf439d4.png">
-
データクラスと機能クラスを分ける従来の手続き型の設計では下記のような問題が顕著になる
- 同じ業務ロジックがあちこちに重複して書かれる
- どこに業務ロジックが書いてあるか見通しが悪くなる
-
結果として、アプリケーションの修正や拡張が必要になった時に以下の状況になりがちである
- 変更の対象箇所を特定するために、プログラムの広い範囲を調べる
- 1つの変更要求に対して、プログラムのあちこちの修正が必要
- 変更の副作用が起きていないことを確認するための大量のテスト
-
-
データクラスを使うと同じロジックがあちこちに重複する
- たとえレイヤーを分けたとしても、データクラスはどの層からでも参照できるので、あるデータクラスのデータを使用した業務ロジックはあちこちに散らばり、変更の副作用が肥大化する
- 本来Javaの「クラス」の仕組みはデータをインスタンス変数として持ち、そのデータを判断/加工/計算のロジックをメソッドに書くのが、クラス本来の使い方
-
データクラスを使うと業務ロジックの見通しが悪くなる
- 三層アーキテクチャでは、業務ロジックをアプリケーション層に記述するのが基本
- しかし、データクラスを使ってしまうと、アプリケーション層に業務ロジックを集めても、下記2つの理由から見通しが悪くなる
- アプリケーション層の構造が画面の構造に引きずられる
- アプリケーション層の機能クラスと画面が密結合して、複数の機能クラスに重複した業務ロジックが発生し、変更・修正・テスト時に失敗するケース
- 例: POSTとPATCHで似たようなロジックが重複する
- アプリケーション層の構造がデータベースの都合に影響される
- 機能クラスをテーブルのCRUD操作単位に設計して業務ロジックの書くべき場所を見失う失敗ケース
- 例: 注文テーブルと出荷テーブルがあり、合計金額を出す業務ロジックを書きべき場所を見失う
- アプリケーション層の構造が画面の構造に引きずられる
-
共通機能ライブラリ(Utilクラス、Commonクラス)が失敗する2つの理由
- 汎用的な共通関数
- 汎用化のために、微妙に異なるニーズに対応しようと引数にフラグやオプション引数が増えて使う側の学習コストが増して使い勝手が悪くなる
- 用途ごとに細分化した共通関数
- 微妙に異なるニーズ毎にメソッドを増やしていくと、微妙な違いを理解してメソッドを使い分けるのが大変になる
結果的に共通関数を使わず自作したメソッドの中に重複が散らばり変更に耐えられなくなる
- 汎用的な共通関数
-
業務ロジックをわかりやすく整理する基本のアプローチ
- データとロジックを一体にして業務ロジックを整理する
- 三層のそれぞれの関心事と業務ロジックの分離を徹底する
COLUMN データクラスが広く使われているのはなぜか
-
C言語からの移行しやすさを重視して設計されている
-
COBOLやC言語で業務アプリケーションを作っていた開発者が従来の開発方針をJavaにも適用した
-
関数とメソッドの違い (https://wa3.i-3-i.info/diff97function.html)
- メソッドオブジェクト指向プログラミングにおいて、各オブジェクトのインスタンス変数を使った自身の振る舞いを定義したもの
- 関数メソッド以外のもの。wiki的には引数と呼ばれるデータを受け取り、定められた通りの処理を実行して結果を返す一連の命令群
データとロジックを一体にして業務ロジックを整理する
-
業務ロジックを重複させないためにはどう設計すればよいか
データクラスと機能クラスは「クラス」というしくみを使っていますが、実際はデータ構造と処理手順という典型的な手続型の設計です。 オブジェクト指向では、データとロジックを1つのクラスにまとめます。そして、それぞれのクラスを独立したプログラミング単位として開発し、テストします。 クラスにデータとそのデータを使う判断/加工/計算のロジックを一緒に書いておけば、コードの重複をなくせます。そのクラスを使う側のクラスに同じロジックを書く必要がなくなるからです。使う側にロジックを書かなくてよくなれば、使う側のクラスのコードはシンプルになります。 その結果、プログラムの見通しが良くなり、修正の対象箇所が少なくなり、変更の影響を狭い範囲に限定できます。つまり、変更が楽で安全になるわけです。
- オブジェクト指向らしいクラスを設計する
- メソッドをロジックの置き場所にする
- インスタンス変数を返すだけのgetterメソッドを書かない
- インスタンス変数に対し、なにかしらの判断/加工/計算をした結果を返すのがメソッドの正しい姿
- ロジックを、データを持つクラスに移動する
- 無意味なgetterメソッドを見つけたら、getしているクラスの業務ロジックをデータを持つクラスに移動する
- データを使う側のクラスにロジックを書き始めたら設計を見直す
- TODOとしてコメントを残し、そのまま放置しない。データを持つクラスにロジックを移動する。
- ロジックの置き場所やクラス名/メソッド名は改善を続け、より良い設計を見つけるのがオブジェクト指向設計の基本
- メソッドを短くして、ロジックの移動をやりやすくする
- メソッドを小さく、短く独立させておくと、データを持つクラスにロジックの移動がしやすい
- 業務ロジックのないデータを持つだけのクラスは自然に書かなくなる
- メソッドでは必ずインスタンス変数を使う
- インスタンス変数を使わないメソッドは、そのクラスのメソッドとしては不適切
- 引数で渡されるデータを持つクラスにロジックを移動できないか検討する
- クラスが肥大化したら小さく分ける
- インスタンス変数が増えて、業務ロジックが集まってくると、変更する時に影響範囲が広くなる
- インスタンス変数とメソッドの関係を調べて、別のクラスに抽出できないか考える
- 関連性の強いデータとロジックだけを集めたクラスは凝集度が高いといえる
- 凝集度の高いクラスに対する変更は、影響範囲が狭く結果として疎結合になる
- パッケージを使ってクラスを整理する
- クラス数が増えた時の整理の手段がパッケージ
- 関連性の強いクラスを同じパッケージに集めて、パッケージスコープを利用し影響範囲をパッケージに閉じ込める
- 長いパッケージ名をつけたくなったら、パッケージを階層にして1つ1つのパッケージ名を短くする
- 開発が進むにつれて、パッケージ構成も改善をし続ける
三層の関心事と業務ロジックの分離を徹底する
-
業務ロジックを小さなオブジェクトに分けて記述する
- 関連する業務データと業務ロジックを1つにまとめたオブジェクトをドメインオブジェクトと呼ぶ
- 注文のような多くの業務ロジックが関わる巨大なドメインオブジェクトは「商品」「数量」「金額」「納期」「届け先」「請求先」という凝集度の高い単位に分かれたドメインオブジェクトを組み合わせてつくる
-
業務ロジックの全体を俯瞰して整理する
- クラス数が増えてきたときはパッケージと、その参照関係を整理整頓する
- 業務アプリケーションでは、パッケージの参照関係は基本的に時間軸に沿った関係になる
- ドメインオブジェクト設計時には、どのパッケージに置くべきか、どのパッケージに依存するか/依存してはいけないかを整理する
- 業務アプリケーションの対象領域(ドメイン)をオブジェクトのモデルとして整理したものをドメインモデルと呼ぶ(パッケージ構成/参照関係を含めて、業務全体がどのような関心事から成り立っているかを理解できるもの)
-
三層+ドメインモデルで関心事をわかりやすく分離する
- まとめ
- 変更が大変になるのはデータクラスと機能クラスを分けるから
- データクラスを使うと、業務ロジックの重複が増える
- アプリケーション全体のコードの見通しを良くするためには、データとロジックを一体にする設計を徹底する
- 業務ロジックは業務データの近くにまとめる(ドメインオブジェクト)
- ドメインモデルに業務ロジックを集める
- ドメインモデルは画面やデータベースの都合から独立させる
- ほかの層は業務的な判断/加工/計算のロジックをドメインモデルに任せることでシンプルでわかりやすい構造になる
第4章 ドメインモデルの考え方で設計する
ドメインモデルの考え方を理解する
-
ドメインモデルで設計すると何がよいのか
-
業務的な判断/加工/計算のロジックを重複なく一元的に記述する
-
業務の関心事とコードを直接対応させ、どこに何が書いてあるかわかりやすく整理する
-
業務ルールの変更や追加のときに、変更の影響を狭い範囲に閉じ込
める -
ドメインモデルはプログラミング言語で書いた「業務の用語集」であり「業務の説明書」であるだけでなく、それぞれの用語がどのように関連し、どのように相互に作用するかをパッケージ構成やクラスの参照関係で立体的に表現する手段でもある。
-
-
ドメインモデルの設計は難しいのか
- 難しいと感じる要因
- オブジェクト指向プログラミングの経験不足
- 業務要件を収集・整理し、分析結果をクラス設計に反映する能力不足
- 難しいと感じる要因
-
利用者の関心事とプログラミング単位を一致させる
- ソフトウェアの目的は、人間の役に立つこと
- ドメインモデルを開発するために必要な2つの活動
- 分析...人間のやりたいことを正しく理解し、わかりやすく整理する活動
- 要求の聞き取り
- 不明点を確かめるための会話
- 図や表を使っての整理
- 理解した結果を記録するための文書の作成
- 設計...人間のやりたいことを動くソフトウェアとして実現する方法を考える活動
- パッケージ構成と名前
- クラス構成と名前
- メソッド構成と名前
-
分析クラスと設計クラスを一致させる
- よくわからなかった、UMLとか勉強しないとちゃんと理解できないかも
- 要件を理解し整理する分析作業も、設計やプログラミングをする人がやるべき
-
業務に使っている用語をクラス名にする
- 業務で使われていない抽象的な言葉をクラス名にすると、その名前は広い範囲の対象を包含してしまう → 凝集度の低下、密結合につながる
- 例: 「取引」クラスに「販売」と「仕入」の業務ルールを記述してしまう
-
データモデルではなくオブジェクトモデル
- ドメインモデルは対象業務(ドメイン)をオブジェクトの集合として表現する
- データモデルではデータの入れ物と、ロジックの機能モデルの分断が発生する
-
ドメインモデルとデータモデルは何が違うのか
- ドメインモデルは業務ロジックに主を、データモデルは文字通りデータが主
- 例: 生年月日と年齢
- ドメインモデル: 年齢を知りたいという関心事があれば、生年月日から年齢を計算するロジックの置き場所が必要だから年齢クラスを作る、というアプローチ
- データモデル: 年齢は記録すべきデータではなく、計算の結果。年齢という関心事はデータモデルには登場せず、生年月日だけを記録する
- ドメインモデルは業務ロジックに主を、データモデルは文字通りデータが主
-
なぜドメインモデルだと複雑な業務ロジックを整理しやすいのか
- 例: 年齢別に大人料金や子供料金といった業務の関心事がでてきた場合
- ドメインモデル: 「年齢」クラスに大人か子供を判断するロジックを置き、年齢別の料金を算出する
- データモデル: データクラスを参照できるクラスであれば、どこでもロジックを書くことができるので重複や、プレゼンテーション層へ漏れ出てしまう
ドメインモデルをどうやって作っていくか
-
部分を作りながら全体を組み立てていく
-
ドメインモデル: データとロジックを持つオブジェクト単位でプログラムを整理する
-
データモデル: どんなデータが業務上必要かを整理し、ロジックは別で整理する
-
両者では、規模が大きくなってきた時の設計のアプローチ法が異なる
-
手続型のアプローチ: トップダウン
-
オブジェクト指向のアプローチ: ボトムアップ
-
全体を定義し、網羅性や整合性を重視しながら部分を設計するトップダウンのアプローチでは、データとロジックの詳細まで一緒に考えるのは負担が大きく非効率
-
部分着目してデータとロジックが一体となった部品から全体を組み立てるのがオブジェクト指向
-
-
全体と部分を行ったり来たりしながら作っていく
-
重要な部分から作っていく
- 重要な部分とは、間違いなく必要になる部分
- オブジェクトは独立性の高いプログラムの部品なので、単体で動作しテスト可能
-
独立した部品を組み合わせて機能を実現する
- ドメインオブジェクトを組み合わせて、業務の機能を実現するのはアプリケーション層のクラスの役割
- ドメインモデルが部品単位で開発を進められる理由の1つが、部品の組み立てと部品の設計を分けて考えることができるから
-
ドメインオブジェクトを機能の一部として設計しない
- 機能中心に考え、機能を分解してプログラム部品を作る弊害
- 機能の分解構造に依存してしまう
- 処理の順番に依存してしまう
- ドメインモデルは単体で動作可能な独立性の高い部品として開発する
- 機能中心に考え、機能を分解してプログラム部品を作る弊害
ドメインオブジェクトの見つけ方
-
重要な関心事や関係性に注目する
- 業務に必要な全てのドメインオブジェクトは最初から網羅的に見つけることは不可能
- 機能を組み立てていく過程で明からになる
- その際は業務の重要な関心事から手をつけていく
-
業務の関心事を分類してみる
- ヒト/モノ/コト
-
コトに注目すると全体の関係を整理しやすい
- コトはヒトとモノとの関係として出現するため、それぞれの関心事の関係性が明らかになる
- コトは時間軸に沿って明確な前後関係を持つため、業務の流れが見えてくる
業務の最重要なコト、受注
-
受注の他のコトと異なる特徴
- 発生源が外部のヒトである→売り手と買い手の合意、「売買契約の成立」
- 将来についての約束である→売り手は商品の出荷、買い手は代金の支払いを約束
-
何でも約束してよいわけではない
-
受注内容が妥当かどうかの業務ロジックが必要(在庫確認など)
-
受注妥当性は固定ではなく、ヒトやモノ、キャンペーンなどの時期によっても異なる
-
妥当性のルールを整理したら、そのルールを実現するデータとロジックの組み合わせを考える
-
数量の例
-
数量クラス
-
数量単位クラス
-
販売可能数量クラス
-
これら数量関連のクラスを数量パッケージにまとめる
-
-
数量パッケージのように、与信パッケージ、基本契約パッケージなどを作成し、組み合わせることで受注ルールを網羅できるだけのドメインオブジェクトが揃ってくる
-
-
期待されるコト、期待されていないコト
- 売買契約が約束通りに実行されたか監視するのは基本機能
- 実行されなかったことの検知と対応
-
予定を記録する
-
実績を記録する
-
差異を判定する
-
差異クラスで差異の検出を行う
-
-
業務ルールの記述 〜手続き型とオブジェクト指向の違い〜
- 手続き型ではデータクラスを受け取った機能クラスでif/switch文を使って必要な条件判断と分岐を実行する
- それがトランザクションスクリプトと呼ばれる手続き型の業務ルールの記述方法(アンチパターン)
- 業務ルールの変更の度に分岐構造が増え複雑になる
業務の関心事の基本パターンを覚えておく
-
ドメインモデルで開発してもトランザクションスクリプトになりがち
- ちょっとしたif文の追加・放置が、手続き型のトランザクションスクリプト化する
-
業務ルールを記述するドメインオブジェクトの基本4パターン
- この4種類のドメインオブジェクトを組み合わせて、次の4つの関心事のパターンに業務ロジックを分類して整理していくと、業務業務ロジックの大半が、アプリケーション層ではなく、ドメインモデルに自然に集まるようになる- 口座(Account)パターン
- 銀行口座、在庫数量の管理、会計などで使うパターン
- 関心の対象を「口座」として用意する
- 数値の増減の「予定」を記録する
- 数値の増減の「実績」を記録する
- 現在の口座の「残高」を算出する
- 予定を含めた管理が業務的に重要
- 現在の残高だけでなく、入荷予定であれば商品が販売可能になる
- 口座クラスは入出荷の予定と実績を持つ
- いつの時点で残高がどれくらいあるかの問い合わせに答える
- 銀行口座、在庫数量の管理、会計などで使うパターン
-
期日(DueDate)パターン
-
予定とその実行の管理、業務アプリケーションの中核の関心事で使われるパターン
- 約束を実行すべき期限を設定する
- その期限までに約束が適切に実行されることを監視する
- 期限切れの危険性について事前に通知する
- 期限までに実行されなかったことを検知する
- 期限切れの程度を判断する
class DueDate { LocalDate dueDate; boolean isExpired() { // 期限切れかどうか } boolean isExpiredOn(LocalDate date) { // その日は期限切れかどうか } int remainingDays() { // 期限までの残日数 } AlertType alertPriority() { // 期限切れの警告度合いの判定 } }
- 出荷期日と支払い期日があればShippingDueDateクラスとPaymentDueDateクラスをそれぞれ作る
- DueDateクラスはAbstractクラスにする
-
-
方針(Policy)パターン
-
業務ルールは多くの場合複合している。複合したルールを扱うパターン
- ルールの集合を持ったコレクションオブジェクトを作る方法
class Policy { Set<Rule> rules; boolean complyWithAll(Value value) { for(Rule each : rules ) { if(each.ng(value)) return false; } return true; //すべてのルールに適合 } boolean complyWithSome(Value value) { for(Rule each : rules ) { if(each.ok(value) return true; } return false; //どのルールにも適合しない } void addRule(Rule rule) { rules.add(rule); } } interface Rule { boolean ok(Value value) ; default boolean ng(Value value) { return !ok(value); } }
-
- 口座(Account)パターン
ドメインオブジェクトの設計を段階的に改善する
-
組み合わせて確認しながら改良する
- 上記基本の4パターンを単純に適用しただけのドメインオブジェクトをアプリケーションに合わせてリファクタしていくポイント
- クラス名やメソッド名の変更
- ロジックの移動
- 使う側にロジックが増えてきたら使われる側にロジックを移動すべき明白な兆候
- 使う側にロジックを書くと重複する危険性が高くなる
- 取りまとめ役のクラスの導入
- ドメインオブジェクトの最小単位は、1つの数値/日付/文字列をラッピングした値オブジェクト
- 業務の主たる関心事の粒度としては小さい
- ドメインオブジェクトを複数持つより大きな関心事のクラスを作る
- 上記基本の4パターンを単純に適用しただけのドメインオブジェクトをアプリケーションに合わせてリファクタしていくポイント
-
業務の言葉をコードと一致させると変更が楽になる
-
どこに何が書いてあるか特定しやすい
-
変更の対象のクラスが、変更の要求の範囲と一致している
-
変更の影響するソフトウェアの範囲が、変更が関連する業務の範囲と一致する
-
クラス名が問題領域の関心事の用語と一致している
-
メソッド名が利用者が知りたいこと/やってほしいことと一致している
-
業務の関心事をプログラミング言語で記述することで、プログラムの側から問題領域を深く理解する手がかりが見つかることがある
- 同じことを複数の業務用語で表現しているあいまい性
- 1つの業務用語が使う文脈で異なる意図を持つあいまい性
- 業務ルール間の矛盾の発見
- 込み入った微妙な関心事の整理の軸
-
-
業務を学びながらドメインモデルを成⻑させていく
- 開発の初期段階では業務知識が不足しているが、それでも理解した範囲で実際にクラス設計し、実装することが大切
- ソースコードで業務の要求仕様を表現することをプログラムの自己文書化と呼ぶ
- 要件を理解するために分析中に発見した用語は、そのままクラスの候補
業務の理解がドメインモデルを洗練させる
-
業務知識を取捨選択し、重要な関心事に注力して学ぶ
- 開発者がアプリケーションの対象とする業務を効果的に学び、役に立つドメインモデルを設計するための基本二点
- 重要な言葉とそうでない言葉を判断する
- 言葉と言葉の関係性を見つける
- 開発者がアプリケーションの対象とする業務を効果的に学び、役に立つドメインモデルを設計するための基本二点
-
業務知識の暗黙知を引き出す
- ドメインエキスパートとの「言葉」と「会話」によって暗黙知となっている業務知識を引き出す
- ドメインエキスパートの言葉はそのままクラス名やメソッド名の候補になる
-
言葉をキャッチする
- 無意識に自分が理解できる言葉に置き換えて、勝手に解釈してしまいがち
- **「正しく聞き取れていない」**という自覚をもって対象業務(ドメイン)を学ぶ
- 言葉をアウトプットして可視化し、ドメインエキスパートと認識を擦り合わせる
- ホワイトボード(+写真)
- チャットやメールなどのQ&A
- ディスカッションボード(テーマごとに議論を記録して読み返せるオンラインツール)
- オンラインのTo-Do管理ツール
-
重要な言葉を見極めながらそれをドメインモデルに反映していく
- ドメインエキスパートとの会話の反応をみて、業務を正しく反映した言葉を理解できているか確認する
-
形式的な資料はかえって危険
- 思考停止に陥る
- 重要な言葉と骨格となる関係を整理するのが重要
- 言い換えれば業務の専門家にとって重要な関心事と、業務の基本構造
- それらをアウトプットし、認識を擦り合わせるための図法
-
言葉の曖昧さを具体的にする工夫
- 曖昧な会話の内容から、コードに落とせるレベルまで具体化する
- 「忙しいときは大変なんだよねえ」
- 「忙しいときって、月末とかですか?」
- 「いや月初」
- 「何が大変なんですか?」
- 「やることが多くて面倒なんだよね」
- 「特に大変なのは?」
- 「請求と入金を別々の画面で立ち上げて、確認しながら照合しているんだけど、並び順が違うので、行ったり来たりが大変」
- 「並び順って?」
- 「請求は請求先コード順、入金は入金通知番号順。いちおう振込元の名前はデータにあるんだけど、それで並べ替えられない」
- 曖昧な会話の内容から、コードに落とせるレベルまで具体化する
-
基本語彙を増やす努力
- その業務のマニュアルや利用者ガイドを読んでみる
- その業務の一般的な知識を書籍などで勉強する
- その業務で使っているデータに何があるか画面やファイルを調べる
- その業務の経験者と会話する
-
繰り返しながらしだいに知識を広げていく
- 勝手な自己解釈をしている可能性を常に意識しながら、ズレがあれたびに修正していく
-
改善を続けながらドメインモデルを成長させる
- ビジネスも常に変化し、業務ルールも変わっていく
- 変化に対応したソフトウェアの変更を早く、確実にできるようにドメインモデルを改良し続けなければならない
- 変更に手こずるようなら、設計の見直しの絶好の機会
- 変更後、思わぬ箇所に副作用が現れたなら、ソフトウェアの構造と業務の関心事がねじれている兆候
-
まとめ
- ドメインモデルは業務ロジックをオブジェクト指向で整理する技法
- データの整理ではなく業務ロジックの整理
- 業務の関心事はヒト/モノ/コトで整理できる
- コトを整理の軸にする
- 起きてよいこと/起きてはいけなくことの判断と対応が業務ルール
- 業務ルールをオブジェクトで表現する一般的なパターンを覚えておくとドメインモデルの設計がやりやすくなる
- ドメインモデル設計のインプットは業務の言葉の正しい理解
- 業務の言葉を正しく覚え、正しく使えるようになることが、良いドメインモデルの設計に直結する
- 業務知識をプログラミング言語で体系的に表現したドメインモデルを中核にした業務アプリケーションは、変更が楽で安全になる
第5章 アプリケーション機能を組み立てる
-
アプリケーション層のクラスの役割
- 三層+ドメインモデル設計におけるアプリケーション層の役割
- 処理の流れの進行役であり、調整役
- プレゼンテーション層からの依頼を受ける
- 適切なドメインオブジェクトに判断/加工/計算を依頼する
- プレゼンテーション層に結果(ドメインオブジェクト)を返す
- データソース層に記録や通知の入出力を指示する
- プレゼンテーション層に対して、業務サービスを提供する(アプリケーションサービスクラス、サービスクラス)
- 処理の流れの進行役であり、調整役
- 三層+ドメインモデル設計におけるアプリケーション層の役割
-
三層+ドメインモデルの構造をわかりやすく実装する
- フレームワークとしてSpring Frameworkを推している
-
サービスクラスの設計はごちゃごちゃしやすい
- ごちゃごちゃになりやすい理由
- ドメインオブジェクトが業務ロジックの置き場所として十分機能していない
- プレゼンテーション層の関心事に振り回される
- データベースの入出力の都合に引きずられる
- 整理整頓させるための方針
- 業務ロジックは、サービスクラスに書かずにドメインオブジェクトに任せる(サービスクラスで判断/加工/計算しない)
- 画面の複雑さをそのままサービスクラスに持ち込まない
- データベースの入出力の都合からサービスクラスを独立させる
- ごちゃごちゃになりやすい理由
サービスクラスを作りながらドメインモデルを改善する
オブジェクト指向の変更容易性は、段階的な開発を可能にする
-
初期のドメインモデルは力不足
- 新たに獲得した業務知識を既存のドメインモデルに反映したり、新たにドメインモデルを追加することで強化していく
- 安易にサービスクラスに業務ロジックやif文を書いてはいけない
-
ドメインモデルを育てる
- ぎこちない名前でもドメインモデルを作成することが重要
- サービスクラスに業務ロジックを書きたくなったら、ドメインモデルの改良の機会
画面の多様な要求を小さく分けて整理する
-
プレゼンテーション層に影響される複雑さ
- 1つの画面で、様々なニーズに対応できる画面を作るのが普通
- 複雑で多様な要求が、サービスクラスの設計に影響する
-
小さく分ける
- サービスクラスを小さく分ける基本は、登録系と参照系のサービスを分けること
- 登録系のサービスは状態を変更する副作用を伴うため処理が複雑になりやすい
- 適切な状態を管理するために事前と事後の確認が必要
- 状態の不整合を検知した場合の例外処理
-
参照と登録が一体になっている画面の場合(例:銀行口座から預金を引き出す)
- 引き出したい金額を入力する
- 残高が不足していなければ残高を更新する
- 更新後の残高を画面に表示する
-
更新と参照でメソッドを分離する
- 意味のある最小単位で、かつ単独でテスト可能な単位にメソッドを分割する
@Service class BankAccountService { @Autowired BankAccountRepository repository; // 残高を照会するだけのメソッド Amount balance() { return repository.balance(); } // 指定金額が引き出し可能か判定するだけのメソッド boolean canWithdraw(Amount amount) { Amount balance = balance(); return balance.has(amount); } } @Service class BankAccountUpdateService { @Autowired BankAccountRepository repository; // 実際に残高を更新するだけのメソッド void withdraw(Amount amount) { repository.withdraw(amount); } }
-
小さく分けたサービスを組み立てる
- 残高が不足していないことを確認する:canWithdraw(amount)
- 残高を更新する:withdraw(amount)
- 更新後の残高を照会する:balance()
- 組み合わせ用のサービスクラスを作る(facadeパターン)
@Service class BankAccountScenario { @Autowired BankAccountService queryService; @Autowired BankAccountUpdateService updateService; Amount withdraw(Amount amount) { // アプリケーション層では、残高不足を例外として投げるだけ // 例外をどうハンドリングするかはサービスを使うプレゼンテーション層の責務 if(! queryService.canWithdraw(amount)) throw new IllegalStateException("残高不足"); updateService.withdraw(amount); return queryService.balance(); } }
- アプリケーション層が、基本サービスを提供するサービスクラス群と、その基本サービスを組み合わせる複合サービスを提供するシナリオクラス群の2層構造になる
- 残高不足が起きた場合はサービスを使う側のプレゼンテーション層のクラスが責任を持つべき
-
利用する側と提供する側の合意を明確にする
- サービスを利用する側と、サービスを提供する側とでサービス提供の約束ごとを決め、設計をシンプルに保つ技法を契約による設計と呼ぶ
- サービスを利用する側が、事前条件を満たしているか確認してから使う
- 契約による設計と対照的なのが防御的プログラミング
- サービスを利用する側も、提供する側も相手が何をしてくるかわからない、という前提でさまざまな防御的なロジックを書いてしまう
-
シナリオクラスの効果
- アプリケーション機能の説明
- 業務の視点で必要とする機能単位をシナリオクラスで表現
- シナリオクラスを起点に、関係する基本サービス単位を特定しやすくなる
- シナリオテストの単位
- 業務視点での妥当性の検証単位として適切
- プレゼンテーション層から分離されているので、テストが書きやすい
- アプリケーション機能の説明
データベースの都合から分離する
-
データベースの入出力に引っ張られる問題
- データベース操作の手続きを並べるCRUDスタイルは、異なる画面や機能に同じようなロジックが重複しがち
- 手続き的なプログラムでは業務の意図が読み取りづらい
- 「記録」という業務の関心事を、INSERTというデータベース操作に開発者が頭の中で変換してしまう結果、プログラムの記述から業務の意図が消えてしまう
- 複雑なテーブル構造では、INSERT文やSELECT文のSQLもタイミングも複雑になり、プログラムがそうした複雑なDB操作を実現するための手続きの羅列になりがち
-
データベース操作ではなく業務の関心事で考える
- 業務の視点からの記録と参照の関心事をリポジトリとして宣言する
- リポジトリは、ドメインオブジェクトの保管と取り出しができる(架空の)収納場所
- リポジトリのメソッド名/引数/返す値は、全て業務の用語で表現する
- リポジトリを使った業務データの記録と参照は、データベース操作ではなく、あくまでも、業務の関心事として記述するための工夫
- 業務の視点からの記録と参照の関心事をリポジトリとして宣言する
-
実際のデータベース操作とリポジトリを組み合わせる
-
実際のデータベース操作はデータソース層のクラスの役割
-
業務の関心事を表現したリポジトリのメソッドを、具体的にどうやってデータベースで実現するかは、リポジトリインターフェースの背後に隠蔽
class BankAccountDatasource implements BankAccountRepository { boolean canWithdraw(Amount amount) { // データベースに残高を照会して結果を返す } Amount balance() { // データベースに残高を照会して結果を返す } void withdraw(Amount amount) { // データベースの残高を変更する } }
-
-
サービスクラスの記述をデータベース操作の詳細から解放する
- テーブル設計に依存する複雑さは、業務機能を記述するサービスクラスには不要
- リポジトリはDB操作の詳細をサービスクラスに意識させない工夫
-
まとめ
- アプリケーション層は業務処理の進行役であり調整役
- アプリケーション層のサービスクラスをシンプルに保つことが、システム全体の見通しの良さと、変更のやりやすさにつながる
- サービスクラスは、さまざまな関心事の交差点になり、ごちゃごちゃしやすい
- サービスクラスをシンプル保つための設計の徹底が重要
- 全体を段階的に組み立てながら設計の改善を続ける
- 業務的な判断/加工/計算の詳細はドメインモデルに集約する
- 画面の複合した関心事を持ち込まない
- そのためにサービスクラスのメソッドを基本処理単位に分解する
- 必要に応じて複合サービスを提供する
- データベースのデータ操作を意識しないように分離し隠ぺいする
第6章 データベースの設計とドメインオブジェクト
-
データの整理に失敗しているデータベース
- データベース設計やデータ内容に下記のような問題があると、プログラムがわかりにくく複雑になる原因となる
- 用途がわかりにくいカラム
- 巨大なテーブル
- テーブル間の関係のわかりにくさ
- データベース設計やデータ内容に下記のような問題があると、プログラムがわかりにくく複雑になる原因となる
-
用途がわかりにくいカラム
- カラム名が省略形
- NULL値が入ったカラム
- ほかのカラムの内容に依存して値の意味が変わるカラム
- カラムから取得した文字列を、プログラムで分解する必要がある
- 意味が読み取れないコード(0,1,9,...などのマジックナンバー)が付いている
- 「自由項目」「予備項目」などの、任意の文字列を扱うカラム
-
色々な用途に使う巨大なテーブル
- ある機能に必要なカラムは3,4つなのにテーブルに100のカラムがあるようなケース
-
テーブルの関係がわかりにくい
- 外部キー制約がない
- キーとなるカラムの名前に一貫性がない
データベース設計をすっきりさせる
-
基本的な工夫を丁寧に実践する
- データベースに用意されている基本的な仕組みをきちんと使うことが大事
- 名前を省略しない
- 文字数制限は緩和されているので、わかりづらい省略系は使わない
- 適切なデータ型を使う
- 桁数は適切な数に制限する
- TEXT型やLOB型、VARCHAR(2000)など安易に使わない
- 制約を必ず使う
- NOT NULL制約
- 一意性制約
- 外部キー制約
-
NOT NULL制約が導くテーブル設計
- 後からデータを設定することを想定してNULLを使うのは危険な方法
- DBは既知の事実を記録するための仕組み、DB(既知)にNULL(未知)を持ち込むのは掟破り
- 全てのカラムをNOT NULLで設計することがテーブルの正規化に繋がる
- 入力が必須ではない項目は別のテーブルに入るべきデータ
-
一意性制約でデータの重複を防ぐ
- 重複したデータは不整合の原因になりがち
- どのカラムの組み合わせが一意になるかを意識する→正規化に繋がる
-
外部キー制約でテーブル間の関係を明確にする
- 制約を使うためのテーブル分割は正規化と対応する
- テーブル分割する時はテーブル間の関係を明確にするために外部キー制約を正しく使う
コトに注目するデータベース設計
-
業務アプリケーションの中核の関心事は「コト」の管理
- 現実に起きたコトの記録
- 将来起きるコトの記録(約束の記録)
- 正しくコトを記録するためには制約が大事
- NOT NULL制約
- コトの記録としてNULLは不正なデータ
- 一意性制約
- データの重複や曖昧さの排除
- 外部キー制約
- 事実として正しく記録された複数のテーブルの関係を明確にする
- 複数のテーブルを結合して正確なデータを再現する
- NOT NULL制約
-
ヒトやモノとの関係を正確に記録するための3つの工夫
- コトは主体(ヒト)と対象(モノ)との関係として定義される
- コトの記録はヒトとモノへの関係も合わせて記録しなければいけない
- コトの記録にはヒトやモノを一意に識別できる外部キー制約が必須
- 記録のタイミングが異なるデータはテーブルを分ける
- 記録の変更を禁止する
- UPDATE文を使うのではなく「取り消し」を記録として追加する(赤黒処理)
- 元データ
- 取り消しデータ
- 新データ
- カラムの追加はテーブルを追加する
- カラムを追加すると、過去データが存在しないためNULL許容にするか、NOTNULL制約を逃れるための「虚」のデータを登録することになる
- カラムを追加したくなった時の正しい対処法
- 元のテーブルはそのまま利用する
- 追加するデータ項目をカラムに持つテーブルを新しく作る
- 追加したテーブルから元のテーブルに外部キー制約を宣言する
参照をわかりやすくする工夫
-
コトの記録に注力したテーブル設計の問題
- NOT NULL制約を徹底した場合、カラム数の少ないテーブルが沢山生まれる
- 現在の状態を導出するためのロジックが複雑になったり性能問題が起きやすい
-
状態の参照
- コトの記録のたびに状態を更新するテーブルを用意する
- 状態を更新するテーブルはコトの記録からいつでも再構築可能な二次的な導出データ
-
UPDATE文は使わない
- UPDATE文はデータの不整合が混入しやすい動作
- レコードが存在しなければINSERTにする仕組みが不要
-
残高更新は同時でなくてもよい
- 残高の更新に失敗したらコトの記録を取り消すような、記録と更新を厳密なトランザクション処理で行うのは正しい考え方ではない
- コトの記録と残高更新は同時でなくて問題ない
-
残高更新は1カ所でなくてもよい
- 分散したDBを非同期メッセージングで連動させる
-
派生的な情報を転記して作成する
- コトの記録を基本にして、そこから派生するさまざまな情 報を目的別に記録する方式をイベントソーシングと呼ぶ
-
コトの記録から状態を動的に導出する
- 入出金だけを記録し、残高は入金履歴と出金履歴が導出する方法
オブジェクトの設計とテーブルの設計
-
オブジェクトとテーブルは似てくる
- コトを記録するテーブルとコトを表現するドメインオブジェクトはほぼ一対一になる
- 似てはいても、設計のアプローチや設計を変更する動機が本質的に異なる
-
オブジェクトはオブジェクトらしく、テーブルはテーブルらしく
- ドメインオブジェクトにORMの都合を持ち込んではいけない
- DB操作などの技術的な関心を混在させない
- MyBatisはドメインモデルとの分離の面で相性がいい
-
業務ロジックはオブジェクトで、事実の記録はテーブルで
- ドメインモデルとテーブルは別々に設計する
-
まとめ
- 制約のないデータベースがプログラムを複雑にする
- 制約を徹底するとデータ管理がうまくいき、プログラムがわかりやすくなる
- テーブル設計の基本は3つの制約(NOT NULL制約、一意性制約、外部キー制約)
- 良いテーブル設計のコツは「コトの記録」の徹底
- 状態の更新はコトの記録とは独立させる
- オブジェクトとテーブルは設計の動機ややり方が基本的に異なる
- オブジェクトとテーブルの設計を独立させやすいしくみを活用する
第7章 画面とドメインオブジェクトの設計を連動させる
画面アプリケーションの開発の難しさ
-
画面にはさまざまな利用者の関心事が詰め込まれる
- 画面を通して、ヒアリングでは明確でなかったさまざまな要望がでてくる
- 要望を取り入れるたびに、画面もソフトウェアも見通しが悪くなる
-
画面に引きずられた設計はソフトウェアの変更を大変にする
- 単純なアプリケーションを除いて、画面単位にプログラムを書くと不要に複雑にしてしまう
- 表示のためのロジックと業務ロジックが混在する
- 例: ある金額を超えた場合に、見た目を変更する
- SmartUIパターンは画面毎に重複したロジックを生んでしまう
-
関心事を分けて整理する
- 画面アプリケーションコードが複雑で変更し難くなる原因
- 画面そのものが複雑
- 画面の表示ロジックと業務ロジックが分離できていない
- 関心事を整理する
- 何でもできる汎用画面ではなく、用途ごとのシンプルな画面に分ける
- 画面まわりのロジックから業務のロジックを分離する
- 画面とドメインオブジェクトは表現の仕方は違うが、利用者の関心事は同じ
- 画面デザインとドメインオブジェクトの連動がうまくいけばいくほど、良いソフトウェアになる
- 画面アプリケーションコードが複雑で変更し難くなる原因
画面の関心事を小さく分けて独立させる
-
複雑な画面は異なる関心事が混ざっている
- 注文のような大きな関心事の画面の場合、複数の関心事が混ざっている
- 注文者を特定する情報(氏名や顧客番号)
- 注文した商品と個数
- 決済方法
- 配送手段と配送先
- 連絡方法
- このような大きな関心事をOrderクラスと一括りにしては変更に弱いモデルになる
- 注文のような大きな関心事の画面の場合、複数の関心事が混ざっている
-
小さな単位に分けて考える
-
画面も分けてしまう
- 用途を特定した小さな単位に分けた画面を提供する事をタスクベースのユーザインタフェースという
-
タスクベースのインターフェースが増えている2つの理由
- スマートフォンの利用が増えた
- 通信環境の変化
-
タスクベースに分ける設計が今後の主流
画面とドメインオブジェクトを連動させる
-
画面もドメインオブジェクトも利用者の関心事のかたまり
- 画面は、ドメインオブジェクトを視覚的に表現したもの
- ドメインオブジェクトを画面に表示する選択肢
- ドメインオブジェクトをそのまま画面の表示にも使う
- 画面用のオブジェクトを別途用意する
-
ドメインオブジェクトと画面の食い違いは設計改善の手がかり
- ドメインオブジェクトをそのまま使うのがベスト
- ドメインオブジェクトと画面で関心事の不一致がある場合は、どちらかの改善が必要
- 「なんでも画面」を提供する場合は、複数のドメインオブジェクトを組み合わせたビュー専用のオブジェクトをプレゼンテーション層に用意する
-
ドメインオブジェクトに書くべきロジック
- ビューとモデルの分離で意識すること
- 論理的な情報構造はドメインオブジェクトで表現する
- 場合ごとの表示の違いをドメインオブジェクトで出し分ける
- HTMLのclass属性をドメインオブジェクトから出力する
- 論理的な情報構造はドメインオブジェクトで表現する
- 物理的なビュー 画面を表示する技術方式に依存したビューの表現
- HTMLの<p>タグや改行コード\n
- 論理的なビュー 技術方式には依存しない概念的な構造、ドメインオブジェクトで表現
- 複数の段落という構造だけを表現 String[] description;
- 場合ごとの表示の違いをドメインオブジェクトで出し分ける
- view側にif文の条件判断をさせない
- 例: 検索結果の件数に応じてメッセージの出し分けをする
- HTMLのclass属性をドメインオブジェクトから出力する
- ドメインオブジェクトで表現する論理的な状態を、ビュー側が利用する
- 物理的なビュー 画面を表示する技術方式に依存したビューの表現
- ビューとモデルの分離で意識すること