📓

「現場で役立つシステム設計原則」を読んだ

2022/08/15に公開

小さくまとめる

  • 短い名前とメソッド、小さなクラス
    • メソッド抽出してわかりやすく小さい処理にまとめる
  • あいまいな基本データではなく、ドメインに沿った具体的な値オブジェクトを作成する
    • 不変にする
    • 入力制限やメソッドをデータに近いところに設けられる
    • わかりやすい名前がコードに表れ、取り違いも起きなくなる
  • 配列という複雑性をコードに表出させないため、次のような処理をまとめた専用のコレクションオブジェクトを作成する
    • ループ処理のロジック
    • コレクション要素数、要素内容の変化
    • 0件の場合
    • 要素数の最小/最大制限

場合分けのロジックを整理する

  • なるべくif文を使わない
    • 区分ごとに別のクラスに分けたりenumを使う方がわかりやすい
    • if文を使う場合、早期リターンやガード節でわかりやすく整理する

業務ロジックを整理する

  • データとロジックのクラスを分けない
    • 汎用的な共通関数は理解していないメンバーは使えず自作関数がどんどん増えていく
    • クラスにデータとそのデータを使う判断/加工/計算のロジックを一緒に書く
    • データを使う側のクラスにロジックを書き始めたら設計を見直す
    • クラスが肥大化したら小さく分ける
  • 三層 + ドメインモデルで整理する
    • プレゼンテーション層(Controller): UIなど外部との入出力を受け持つ
    • アプリケーション層(Service): 業務機能のマクロな手順の記述
    • データソース層(Repository): DBとの入出力を受け持つ
    • ドメインモデル: 業務データと関連する業務ロジックを表現したドメインオブジェクトの集合

ドメインモデルの設計

  • ドメインモデルとなるものはERクラスと一致するわけではない(データではなく業務ロジックで整理する)
    • 重要な言葉とそうでない言葉を判断する
    • 言葉と言葉の関係性を見つける
  • 業務の関心事はヒト/モノ/コトで整理する
    • ヒト: 業務活動の当事者。ドメインオブジェクトはヒトの意思/判断/行動についてのデータを持つ
    • モノ: ヒトが業務を遂行するときの関心の対象。数量/説明/状態/期間/位置などの属性を持つ
    • コト: 業務活動で起こる事象。ヒトの意思決定や行動の結果。対象/種別/時点の属性を持つ
  • コトを整理の軸にする
    • コトはヒトとモノとの関係として出現する
    • コトは時間軸に沿って明確な前後関係を持つ
  • 起きてよいこと/起きてはいけないことの判断と対応が業務ルール
    • コトが起きてよいかの妥当性の確認
    • 妥当であった/妥当でなかった場合の次のアクション
    • コトの予定と実績から差異を検知/判定する
    • 想定外のコトが起きたときの対応
  • ドメインオブジェクトの設計パターン
    • 値オブジェクト: 数値、日付、文字列をラッピングしてロジックを整理する
    • コレクションオブジェクト: 配列やコレクションをラッピングしてロジックを整理する
    • 区分オブジェクト: 区分の定義と区分ごとのロジックを整理する
    • 列挙型の集合操作: 状態遷移ルールなどを列挙型の集合として整理する
    • これらの小さな独立性の高いドメインオブジェクトを組み合わせて業務の主たる関心となるドメインオブジェクトを作る
  • 業務の関心事のパターン(これらに上の設計パターンを当てはめていく)
    • 口座パターン: 現在の値を表現し、妥当性を管理する
    • 期日パターン: 約束の期日と判断を表現する
    • 方針パターン: さまざまなルールが複合する、複雑な業務ロジックを表現する
    • 状態パターン: 状態と状態遷移のできる/できないを表現する

アプリケーション層(Service)の設計

  • 業務ロジックはサービスクラスには書かずシンプルに保つ
  • ひとつのサービスクラスの中で参照/更新を同時に行わず、サービスクラスを参照/更新系に分けるとシンプルに保ちやすい
  • 更新系のサービスクラスは何も情報を返さない
  • 参照/更新系サービスクラスを組み立てるシナリオクラスを作りプレゼンテーション層から呼ぶ
  • サービスを提供する側がなにが入ってくるかを検査するのではなく、サービスを利用する側がサービスを使う前の約束を決める
    • nullを渡さない/nullを返さない
    • 状態に依存する場合、使う側が事前に確認する
    • 異常が起きた場合、サービスを使う側が例外を通知する
  • DB操作の都合はサービスクラスには表出させずデータソース層に任せる

参照系と更新系にサービスを分け、シナリオサービスでそれらを組み立てる

// 参照系サービス
@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);
    }
}

// 組み立てサービス
@Service
class BankAccountSenario {
    @Autowired
    BankAccountService queryService;
    @Autowired
    BankAccountUpdateService updateService;

    // 参照/更新系サービスを組み合わせる
    Amount withdraw(Amount amount) {
        // 例外を返すのは組み立てサービス側で行う
        if (! queryService.canWithdraw(amount)) throw new IllegalStateException("残高不足");

        updateService.withdraw(amount);
        return queryService.balance();
    }
}

データベースの設計

  • カラム名などの名前を省略しない
  • 適切なデータ型を使う
  • 制約を使う(NOT NULL, UNIQUE, 外部キー)
  • ヒトやモノとの関係を正確に記録する
    • 記録のタイミングが異なるデータはテーブルを分ける
      • そのテーブル間の関係を明示するために外部キー制約を使う
    • 記録の変更を禁止する(UPDATEは禁止)
      • 過去の記録を修正する場合は取り消し自体を記録する(元データ、取り消しデータ、新データがINSERTされる)
    • デフォルト値を明確に決められないカラムの追加はテーブルを追加する
      • NULL許可を避けるため
  • コトの記録を徹底する
    • コトの失敗はトランザクション失敗であっても記録されるべき
    • 状態テーブルの更新はコトの更新と同トランザクションである必要はない(厳密な同時性の緩和、非同期メッセージング)
    • 状態テーブルはコトの記録から再構築可能なデータとする

画面とオブジェクトの設計

  • 業務ロジックと表示ロジックを分離する
    • 用途を小さく分けたタスクベースのUIが有効(必要な情報だけを編集できるなど)
  • 画面とドメインオブジェクトの不一致を改善する
    • 画面での項目の並び順とドメインオブジェクトのフィールドの並び順
    • 画面上の項目のグルーピングとドメインオブジェクトの単位
    • リリースノート、利用者ガイドなどもドメインオブジェクトの設計と一致させる

アプリケーション間の連携

  • Web API
    • 同期型の処理方式が運用面と性能面の制約になる
    • GETは識別番号やリソースのグループはURIパスで、オプショナルな絞り込み条件はクエリパラメータで指定する
    • PUTでリソースの状態の確認と更新を行わず、GETで状態の確認を行いPUTは更新に徹するか、POST /updatesなどを使いPUTは使わない
    • DELETEでリソースの状態の確認と削除を行わず、GETで状態の確認を行いDELETEは削除に徹するか、POST /deletionsなどを使いDELETEは使わない
    • 大きいAPIを作るのではなく小さいAPIを組み合わせる
      • コアAPI, 拡張API, 個別対応APIに分ける
  • 非同期メッセージング
    • 疎結合になり、並行処理することで大量の処理を捌くことができる
    • 安定したメッセージング基盤の構築と運用に課題がある

オブジェクト指向の開発プロセス

  • 経済活動の変化により、短期間で開発し修正と拡張を繰り返すことが重要になった
    • 分析/設計に時間を取りすぎると利用者の関心とプログラムの設計が不連続になる
    • 分析/設計に時間を取らないと規模が大きくなると手がつけられないコードになる
    • 分析/設計は同じ人間が一貫して担当し、ドメインモデルに業務ロジックを集めて整理することが効果的
  • ソースコードを第一級のドキュメントとして活用する
    • 更新すべきドキュメントは利用者向けのドキュメント
    • システム概要、基本目的、方向性などもソースコードでは共有しにくいためドキュメントとして管理する

オブジェクト指向らしい少し過激なコーディング規約

  • 1つのメソッドにつきインデントは1段階まで
  • else句を使用しない
  • すべてのプリミティブ型と文字列型をラップする
  • 1行につきドットは1つまでとする
  • 名前を省略しない
  • すべてのエンティティを小さくする
  • 1つのクラスにつきインスタンス変数は2つまで
  • ファーストクラスコレクションを使用する
  • getter, setter, プロパティを使用しない

感想

  • システム設計というかほぼDDD本だった
    • 破綻しないシステム設計≒DDDということで自然とそうなったと思われる
    • タイトルにはDDDの文字は一切ないけど入れといた方がわかりやすいのでは
  • 前に読んだDDD本よりわかりやすかった
    • コードはそこまで出てこないが、ドメインオブジェクトとして管理した方がよいケースが多く載っていてわかりやすい
  • ドメインモデル設計、アプリケーション層設計のあたりは、感覚としてできていない部分が多く実践していきたい
    • 小さなドメインオブジェクトを組み合わせてまとまりのあるドメインオブジェクトを作る
    • 参照/更新系サービスを分けて組み合わせて作る
  • SPA的なフロントエンドと業務ロジックが書かれるサーバが別になっているシステムを前提にしておらず、プレゼンテーション層がHTMLを返す形を想定しているからそのあたりは少し違和感があった
    • できればフロントエンドに表示ロジックが多く書かれるようなシステムにおけるプレゼンテーション層の有効な書き方なども読みたかった
GitHubで編集を提案

Discussion