🌱

精読「現場で役立つシステム設計の原則」(2/3)(ドメイン駆動設計/データベース設計編)

2024/12/21に公開


現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法
ドメイン駆動開発をベースとした実践的な設計の原則を学べる良書です。この設計原則が頭に強く刻み込まれるまで繰り返し読み返そうと思います。

ドメインモデルの考え方で設計する

ドメインモデルの考え方を理解する

  • 業務の関心事の単位とプログラミングの単位が一致していると、業務ルールの修正や追加が楽で安全になる
  • ドメインモデルの設計とは、業務を理解するための分析と、ソフトウェアとして実現するための設計が、一体になった活動
    • 分析…要求の聞き取り、不明点を確かめるための会話、図や表を使っての整理、理解した結果を記録するための文書の作成
    • 設計…パッケージ構成と名前、クラス構成と名前、メソッド構成と名前
  • 分析クラスと設計クラスは一致させる
  • 業務に使っている用語をクラス名にする
  • ドメインモデルは業務ロジックの整理手法なので、関心の中心は業務ロジック(データが主役なのがデータモデルで、ドメインモデルとは本質的に異なるので要注意)

ドメインモデルをどうやって作っていくか

  • オブジェクト指向のアプローチ(ボトムアップ)で、部分を作りながら全体を組み立てていく(⇔手続き型のアプローチはトップダウン)
  • ただ、全体を意識して開発する必要はあるため、全体を俯瞰する道具としてパッケージ図業務フロー図が必要
    • パッケージ図…個々のクラスを隠蔽し、パッケージ単位で全体の構成を俯瞰する手段。分析の初期の段階は、クラス単位で考えるよりも、業務ロジックのおおよその置き場所をパッケージとして割り振ってみる
    • 業務フロー図…業務の活動を、時間軸に沿って図示したもの
  • 重要な部分(間違いなく必要になる部分)から作っていく(重要だと判断できた段階で、実際に作ってしまう)
  • ドメインモデルは、ドメインオブジェクトを集めて整理した部品の倉庫。独立した部品を組み合わせて機能を実現する
  • ドメインオブジェクトを機能の一部として設計しない(機能中心で作ると、機能の分解構造や時間的な依存関係を持ち込むことになるため)

ドメインオブジェクトの見つけ方

  • 業務の関心事をヒト/モノ/コトに分類し、コトに注目すると全体の関係を整理しやすい
  • コトに注目することで、以下の関係も明らかになる
    • コトはヒトとモノとの関係として出現する(だれの何についての行動か)
    • コトは時間軸に沿って明確な前後関係を持つ
  • コトの背後にある業務ルールを把握する
例)受注の特徴
  • 業務ルール1.発生源が外部のヒトである
  • 業務ルール2.将来についての”約束”である
  • ”約束”の妥当性の業務ルールを特定したら、そのルールを実現するデータとロジックの組み合わせを考える
例)受注の妥当性の業務ルール
  • 約束の相手が誰かによって、約束してよい範囲が異なる
  • どの商品についての約束かによって、金額や納期が異なる
  • どのタイミングの約束かによって、金額や納期が異なる
例)受注の業務ルールを実現するためのデータとロジックの組み合わせ
  • 「数量」に関する業務ロジックは、数量クラスにまとめられそう
  • 「数量」が個数単位と箱単位で扱いが必要なら、「数量単位クラス」を追加する
  • いくつまで受注してよいかを判断するロジックを「販売可能数量クラス」が持つ
  • "約束"どおりに実行されなかったときのルールも考える
例)実行されなかったことの検知と対応
  • 「予定」を記録する → 「予定」クラスには、予定の変更などを吸収し、現在の有効な約束を表現する
  • 「実績」を記録する → 「実績」クラスには、分割した出荷などを合算した、現時点の実績を表現する
  • 「差異」を判定する → 「差異」クラスは、差異を検出するロジックを置く

業務の関心事の基本パターンを覚えておく

  • ドメインモデルで開発していても、トランザクションスクリプトになりがち(「ちょっとしたif文」の追加などにより、アプリケーション層のクラスが手続き型のトランザクションスクリプトになってしまう)
  • ドメインオブジェクトの設計パターンを体で覚えよう

ドメインオブジェクトの基本の設計パターン

ドメインオブジェクト 設計パターン
値オブジェクト 数値、日付、文字列をラッピングしてロジックを整理する
コレクションオブジェクト 配列やコレクションをラッピングしてロジックを整理する
区分オブジェクト 区分の定義と区分ごとのロジックを整理する
列挙型の集合操作 状態遷移ルールなどを列挙型の集合として整理する

この4種類のドメインオブジェクトを組み合わせて、次の4つの関心事のパターンに業務ロジックを分類して整理していくと、業務ロジックの大半がドメインモデルに自然に集まる

業務の関心事のパターン

関心事のパターン 業務ロジックの内容
口座(Account)パターン 現在の値(現在高)を表現し、妥当性を管理する
期日(DueDate)パターン 約束の期日と判断を表現する
方針(Policy)パターン さまざまなルールが複合する、複雑な業務ロジックを表現する
状態(State)パターン 状態と、状態遷移のできる/できないを表現する

口座(Accout)パターン

  • 銀行の口座、在庫数量の管理、会計などで使うパターン
  • しくみは以下で実現
    • 関心の対象を「口座」として用意する
    • 数値の増減の「予定」を記録する
    • 数値の増減の「実績」を記録する
    • 現在の口座の「残高」を算出する

  • 口座クラスは、入荷と出荷の予定と、入荷と出荷の実績を持つ
  • いつの時点で、残高がどのくらいあるかの問い合わせに答える

期日(DueDate)パターン

  • 予定と実行の管理は、業務アプリケーションの中核の関心事で、以下の業務ロジックがある
    • 約束を実行すべき期限を設定する
    • その期限までに約束が適切に実行されることを監視する
    • 期限切れの危険性について事前に通知する
    • 期限までに実行されなかったことを検知する
    • 期限切れの程度を判断する

期日の業務ルールを扱うクラス

  • 期日に関わる業務ルールはドメインによって異なるので、DueDateクラスは期日について汎用的に使い回さず、クラスを分けるべき(出荷期日と支払期日など)

方針(Policy)パターン

  • 複合した業務ルールを扱う1つの方法として、ルールの集合を持ったコレクションオブジェクトを作ること
  • 一つ一つのルールごとに、ルールインターフェースを持ったオブジェクトを作る
  • ルールの集合に対して全ての条件が一致するとか、1つは一致する、などの判定をPolicyクラスに任せる

ドメインオブジェクトの設計を段階的に改善する

  • ドメインモデルの開発とは、小さな独立性の高いドメインオブジェクトを揃えていく活動
  • 業務を学びながらドメインモデルを成長させていく(自己文書化[1]させる)

業務の理解がドメインモデルを洗練させる

  • 業務を効率的に学び、役に立つドメインモデルを設計するための基本は2つ
     - 重要な言葉とそうでない言葉を判断する
    • 言葉と言葉の関係性を見つける
  • 聞き慣れない言葉(業務知識)をキャッチするコツは、後にメモ書きや絵にして他の人に確認してもらうこと
  • 単語単位ではなく言葉の言葉の結びつきが自然になるように理解を言語化し、それが業務の経験者にとって自然な言い回し、且つ業務の重要な関心事であれば、相手の反応が積極的になる
  • 形式的な資料はかえって危険。資料を大量に作るより、重要な言葉が何で、骨格となる関係は何かを判断することに時間とエネルギーを使ったほうが、大きな成果を手に入れることができる
  • 全体を俯瞰するための図法は以下のような物があるが、これらは公式のドキュメントとして維持するものではなく、「考えたことの履歴」として記録しておく
図法名 目的
コンテキスト図 システムの目的を表す言葉を探す(重要なクラスの発見の手がかり)
業務フロー図 コトの発生を時系列に整理する
パッケージ図 業務の関心事を俯瞰する(用語の全体的な整理)
主要クラス図 重要な関心事とその関係を明確にする

アプリケーション機能を組み立てる

ドメインオブジェクトを使って機能を実現する

  • 三層+ドメインモデルの構造をわかりやすく実装する
  • サービスクラス(アプリケーション層)の設計はごちゃごちゃしやすいため、以下の方針を徹底する
     - 業務ロジックは、サービスクラスに書かずにドメインオブジェクトに任せる
    • 画面の複雑さをそのままサービスクラスに持ち込まない
    • データベースの入出力の都合からサービスクラスを独立させる

サービスクラスを作りながらドメインモデルを改善する

  • サービスクラスに業務ロジックを書くほうが早くても、ドメインオブジェクトを追加/修正してドメインモデルを育てる

画面の多様な要求を小さく分けて整理する

  • たくさんの要件を詰め込んだ画面(プレゼンテーション層)が作られ、結果サービスクラスが複雑になりがち
  • サービスクラスを小さく分ける基本は、登録系参照系でサービスを分けること
系統 説明
登録系 プレゼンテーション層から渡された情報を検証し、加工や計算を行ったうえで。記録したり通知する
参照系 プレゼンテーション層の依頼に基づき情報を生成し、プレゼンテーション層に戻す機能
  • 契約による設計[2]により、コードをシンプルに保ち、プログラムの異常な処理を防止する
  • シナリオクラス[3]は、以下の効果がある
    • コードの整理
    • アプリケーション機能の説明
    • シナリオテストの単位

データベースの都合から分離する

  • データベース操作ではなく業務の関心事で考える
  • 業務の観点からの記録と参照の関心事を、リポジトリとして宣言する(ドメインオブジェクトの保管と取り出しができる架空の収納場所)
  • リポジトリを使った業務データの記録と参照は、データベース操作ではなく、あくまでも、業務の関心事として記述するための工夫
  • 具体的にどうやってデータベースで実現するかは、リポジトリインターフェースの背後に隠蔽する
  • 他にも、例えば注文データを記録するときに、注文ヘッダテーブルと明細テーブルに別々にINSERTするにしても、「注文を記録する」という業務の関心事には関係がない

データベースの設計とドメインオブジェクト

テーブル設計が悪いとプログラムの変更が大変になる

  • 問題の多いでデータベース設計やデータ内容は次のようなものがある
     - 用途がわかりにくいカラム…例)カラム名が省略形、NULL値が入っている、マジックナンバーが付いている、自由項目などの拡張用カラム
    • 巨大なテーブル…例)似たようなカラムが多く、その使い分け方がわからない、NULL値が多い
    • テーブル間の関係のわかりにくさ…例)外部キー制約がない、キーとなるカラムの名前に一貫性がない

データベース設計をすっきりさせる

  • データベース設計で大切なのは、データベースに用意されている基本的なしくみをきちんと使うこと
     - 名前を省略しない
    • 適切なデータ型を使う…桁数にも現実的な桁数を指定する、TEXT型・LOB・VARCHAR(2000)ですべてを済ませてはいけない
    • 制約をきちんと使う…NOT NULL制約、一意性制約、外部キー制約
  • NOT NULL制約を使うことで、テーブルの正規化が進む
  • 一意性制約もNOT NULL制約と並んで重要な正規化の実践的な手段
  • 正規化されて小さく分割されたテーブル間の関係を明確にするために、外部キー制約を使う

コトに注目するデータベース設計

  • 業務アプリケーションでデータベースが重要なのは、コトを正しく記録し参照するため
     - 現実におきたコトの記録
    • 将来起きるコトの記録(将来の記録)
  • コトの記録はヒトやモノへの関係も合わせて記録しなければいけない
     - データの不整合を防ぐために、外部キー制約を使う
    • 記録のタイミングが異なるデータはテーブルを分ける(さもなくばNULL可能なカラムが生まれる)
    • 記録の変更を禁止する(データを更新したい場合、UPDATE文は使わずに赤黒処理(元データ/取り消しデータ/新データ) をする)
    • カラムの追加はテーブルを追加する(カラムの追加により、過去データがNULLになる)

参考をわかりやすくする工夫

  • コトの記録に注力し、データの整合性と信頼性を重視したテーブル設計をしつつ、現在の状態をわかりやすく参照し、且つ性能面でも問題を起こさないために工夫が必要
  • その解決策の1つが、イベントソーシングコト(イベント)の記録を基本にして、その履歴を保存することで状態を管理する手法
  • コトの記録を徹底すれば現在の状態を動的に導出できるがロジックの複雑さや性能面で問題があるので、状態の扱い方としてデータベースのインデックスのしくみが参考になる
    • 基本はコトの記録のテーブル
    • 導出の性能を考慮して、コトの記録のたびに状態を更新するテーブルも用意する
    • 状態を更新するテーブルはコトの記録からいつでも再構築可能な二次的な導出データ
       データベースの本質は事実の記録であり、コトの記録を徹底することが基本。状態テーブル補助的な役割であり、コトの記録から派生させる二次的な情報にすぎない
例)残高照会
  1. 口座に入金があったら入金テーブルにコトを記録する
  2. 残高テーブルのその口座の残高も増やす
  3. 口座から出金があったら、出金テーブルにコトを記録する
  4. そして、残高テーブルのその口座の残高を減らす
  • 状態を更新するテーブルは、UPDATE文ではなく、DELETE文/INSERT文の対で実行する(UPDATE文だと、「記録の同時性」に違反しデータの不整合が混入しやすいため)
  • コトの記録と状態の更新を厳密なトランザクションとして処理するのは、考え方として正しくない(状態の更新は二次的な導出処理なので、失敗したらコトの記録も取り消すというのは違うから)
  • 状態の更新を、例えば非同期メッセージングで別システムにまかせてしまうような方法で処理の独立性を高め、システムの設計をシンプルにできる可能性が生まれる
  • 非同期メッセージングを使えば、状態の更新を複数のサーバで別々に行うことができる
例)残高更新を複数のサーバで別々に行う
  • コトの発生を顧客管理サーバに通知すると、顧客管理サーバは顧客単位の残高を更新する
  • 同じコトの発生を営業管理サーバに通知すると、売上部門別の売上高を更新する
  • コトの記録を徹底し、コトの発生を非同期メッセージングで分配すれば、必要に応じて集計情報、コトの記録の複製、コトの記録のサブセットなどを並行して作成できる(従来であれば、バッチ式に生成していたレポート系の情報も、コトの発生ごとに更新して、その時点の最新結果を参照できる)
  • コトの記録と、集計情報やコトの記録のサブセットの参照分けることは、修正や拡張の柔軟性を高める(ただし、分散処理の非機能要件も含めた検討は必要)
  • 起きたコトだけを記録し、現在の状態は起きたコトから動的に導出する方法もある(テストがやりやすくなる)

参考記事

https://zenn.dev/shmi593/articles/56c890962bb807

脚注
  1. ソースコードで業務の要求仕様を一致させること ↩︎

  2. サービスを利用する側と提供する側で、サービス提供の約束事を決める。対象的な技法が防御的プログラミング ↩︎

  3. 基本的なサービスクラスを組み合わせた複合サービスを提供する ↩︎

Discussion