ドメインモデルによる機能の実現

アプリケーション層のクラス

  • アプリケーション層の役割は以下
    • プレゼンテーション層の依頼を受ける
    • 適切なドメインオブジェクトに処理を依頼する
    • プレゼンテーション層に結果を返す
    • データソース層に入出力を指示する
  • アプリケーション層のクラスは業務サービスを提供するという意味で、サービスクラスという
  • サービスクラスの実装は以下の要因によりごちゃごちゃしがちである
    • ドメインオブジェクトが業務ロジックの置き場所として十分機能していない
    • プレゼンテーション層の関心事に引きずられる
    • データベースの都合に引きずられる
  • そうならないために、以下のことを徹底する
    • 業務ロジックはサービスクラスに書かずにドメインオブジェクトに任せる(サービスクラスで計算・加工などをしない)
    • 画面の複雑さをサービスクラスに持ち込まない
    • データベースの入出力の都合からサービスクラスを独立させる

プレゼンテーション層からの依頼を受け付ける

ロジックを小さく分ける
  • 画面の多様な要求をサービスクラスの1つのメソッドに押し込めると、 そのメソッドはif文だらけになり、判断と分岐の流れを追うだけでも大変になる
  • サービスクラスを小さく分ける
    • 登録系:プレゼンテーション層からのデータを計算・加工し記録する
    • 参照系:プレゼンテーション層からの依頼に基づきデータを生成し返す
  • 意味のある最小単位で、かつ単独でテスト可能な単位にメソッドを分割するのがサービスクラス設計の基本

参照系サービスクラス

// 口座情報を参照する
class BankAccountService {
    @Autowired
    BankAccountRepository repository;

    Amount balance() {
        return repository.balance();
    }

    boolean canWithdraw(Amount amount) {
        Amount balance = balance();
        return balance.has(amount);
    } 
}

登録系サービスクラス

// 口座の残高を更新する
class BankAccountUpdateService {
    @Autowired
    BankAccountRepository repository;

    void withdraw(Amount amount) { 
        repository.withdraw(amount);
    } 
}
小さく分けたサービスを組み立てる
  • 小さく分けたサービス(参照系サービスクラス登録系サービスクラス)を組み立てるため、組み立てる専用のシナリオクラスをつくる
  • 基本サービスを提供するサービスクラス群と複合サービスを提供するシナリオクラス群の2層構造になる
  • プレゼンテーション層のコントローラで組み立ててしまうと、プレゼンテーション層に業務の手順という業務知識が溢れ出すことになってしまう
    • 業務ロジックがプレゼンテーション層に書かれ始めると、ロジックが重複し、調査箇所、修正箇所が広がってしまうことにつながる
  • シナリオクラスにはコードの整理だけではなく次の2つの効果もある
    • アプリケーション機能の説明(シナリオクラスを起点に関連する基本サービスを特定しやすくなる
    • シナリオテストの単位

シナリオクラス

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();
    } 
}

データソース層に入出力を依頼する

データベースではなく業務の関心事で考える
  • 参照や記録という業務の関心事をデータベースのCRUDに勝手に変換してしまうと、データベースの入出力に引きずられたコード(データベース操作の手続きの羅列)が出来上がる
    • 業務的な意図が読み取りにくくなる
    • テーブル構造が複雑化すると、同じようなロジックが重複する
  • 業務の関心事としての参照と記録を記述する
    • 業務の視点からの参照や記録の関心事をリポジトリとして宣言する
    • リポジトリドメインオブジェクトの保管と取り出しができる架空の収納場所
    • すべてのドメインオブジェクトをメモリ上に保管していつでも取り出せる仕組み
    • リポジトリのメソッド名・引数・返り値はすべて業務の用語で表現する
  • サービスクラスの中からはリポジトリを利用して業務データの記録や参照を行う
interface BankAccountRepository { 
    boolean canWithdraw(Amount amount); 
    Amount balance();
    void withDraw(Amount amount);
}
実際のデータベース操作とリポジトリを組み合わせる

データソース層のクラスでデータベース操作を行う。このように具体的なデータベース操作はリポジトリインターフェイスによって隠蔽され、サービスクラスはデータベース操作と独立して、業務の関心事として参照と記録を行う。

class BankAccountDatasource implements BankAccountRepository { 

    boolean canWithdraw(Amount amount) {
        // データベースに残高を照会して結果を返す 
    }

    Amount balance() {
        // データベースに残高を照会して結果を返す
    }
    
    void withdraw(Amount amount) {
        // データベースの残高を変更する
    } 
}