blog

[ドメイン駆動設計(DDD)] ドメイン駆動設計 - DDDの威力を示す実例

バグを修正する必要があるとき、何百行ものコード、コメントなし、奇妙なメソッドや変数名、ネストされたメソッド呼び出し、混沌とした構造、バグの正確な場所を見つけるとは言いませんが、コードの一部が何であるか...

Apr 6, 2020 · 9 min. read
シェア

オブジェクト指向設計の大家であるMartin Fowler氏は、彼のブログや著書「Enterprise Application Architecture Patterns」の中で、設計におけるこのような一般化の力を何度も提唱しています。また、ドメインモデリングのもう一人の優れた専門家であるEric Evans氏は、著書「Domain Driven Design」の中で貴重な教訓やアプローチを提供しています。

本連載では、長年システム設計に携わってきた私のドメイン駆動設計への理解と、実際のプロジェクトで蓄積された分析作業の経験を合わせて、皆様にお伝えし、学んでいただければと思います。

この一連のブログ記事の冒頭で、私は、まず貧弱なモデルを使用して設計する伝統的なプロセス指向の方法を示す例を考え、その後、徐々に要件に変更を加えていきます。読者は、システムの継続的な変更に伴い、貧弱なモデルベースの設計がシステムを徐々に泥沼に陥らせ、保守がますます困難になることを理解し、その後、再アベプロセスに基づくオブジェクト指向のドメイン駆動設計を使用します。この比較は、複雑なビジネスシステムに対するドメイン駆動設計の威力を示しています。

今、銀行決済システムのプロジェクトがあり、重要なビジネスユースケースの1つが口座振替業務であるとします。システムは繰り返しアプローチで開発され、バージョン1.0では、このユースケースの機能要件は非常にシンプルで、イベントフローは以下のように記述されています:

メインイベントの流れ

1) 銀行のオンライン決済システムへのログイン

2) 銀行に登録されているユーザーのインターネットバンキング口座を選択します。

3) 振込先口座を選択し、振込金額を入力し、振込依頼をしてください。

4) 銀行システムは、口座から振り込まれた金額の妥当性をチェックします。

5) 振込口座から振込金額を差し引き、振込口座の残高を更新します。

6) 振込金額を振込口座に追加し、振込口座の残高を更新します。

オルタナティヴ・イベントの流れ

4a) 転送先口座の残高が不足している場合、転送は失敗し、エラーメッセージが返されます。

プロセス指向設計(POD)

デザインオプションは以下の通りです:

1)アカウントトランザクションのサービスインターフェイスAccountingServiceを設計し、サービスメソッドの転送()を設計し、特定の実装クラスAccountingServiceImplを提供し、すべてのアカウントトランザクションのビジネスロジックは、サービスクラスに配置されます。

2) AccountInfoとAccountを提供し、前者はプレゼンテーション層とアカウントデータを交換するためのアカウントデータ転送オブジェクトであり、後者はアカウントエンティティであり、どちらも関連するプロパティと単純なget/setメソッドを持つ普通のJavaBeansです。

以下は、AccountingServiceImpl.transfer()メソッドの実装ロジックです:

public class AccountingServiceImpl implements AccountingService {
 public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount)
 throws AccountingServiceException {
 Account srcAccount = accountRepository.getAccount(srcAccountId);
 Account destAccount = accountRepository.getAccount(destAccountId);
 if(srcAccount.getBalance().compareTo(amount)<0)
 throw new AccountingServiceException(
 AccountingService.BALANCE_IS_NOT_ENOUGH);
 srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));
 destAccount.setBalance(destAccount.getBalance().add(amount));
 }
}
public class Account implements DomainObject {
 private Long id;
 private Bigdecimal balance;
 /**
 * getter/setter
 */
}

ご覧のように、バージョン1.0の機能要件は非常にシンプルなので、プロセス指向の設計アプローチに従って、すべてのビジネスコードをAccountingServiceImplに置くことはまったく問題ありません。

この時、新たな要件が来て、バージョン1.0.1では、口座振替業務のための次の機能を追加する必要があり、転送では、まず第一に、アカウントが使用可能かどうかを判断する必要があり、その後、口座の残高が2つの部分に分割する必要があります:金額の凍結部分のお金の凍結部分とアクティブな部分は、任意のトランザクションのビジネスに使用することはできません、変更後のコードを見てみましょう:

public class AccountingServiceImpl implements AccountingService {
 public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) 
 throws AccountingServiceException {
 Account srcAccount = accountRepository.getAccount(srcAccountId);
 Account destAccount = accountRepository.getAccount(destAccountId);
 if(!srcAccount.isActive() || !destAccount.isActive())
 throw new AccountingServiceException(
 AccountingService.ACCOUNT_IS_NOT_AVAILABLE);
 BigDecimal availableAmount = srcAccount.getBalance().substract(
 srcAccount.getFrozenAmount());
 if(availableAmount.compareTo(amount)<0)
 throw new AccountingServiceException(
 AccountingService.BALANCE_IS_NOT_ENOUGH);
 srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));
 destAccount.setBalance(destAccount.getBalance().add(amount));
 }
}
public class Account implements DomainObject {
 private Long id;
 private BigDecimal balance;
 private BigDecimal frozenAmount;
 /**
 * getter/setter
 */
}

この時点で、1.0.2の要件が戻ってきました。トランザクションが成功するたびに、トランザクション元帳を作成する必要があるため、やはり、トランザクション元帳の作成と永続化内部のビジネスロジックのtransfer()アスペクトになければなりません:

AccountTransactionDetails details= new AccountTransactionDetails( ;
accountRepository.save(details);

ビジネス要件はどんどん複雑になっていきます。1回の送金で送金できる口座の最大金額は、その口座の信用指数によって決定される必要があり、手数料は銀行の手数料ポリシーに従って計算され、差し引かれる必要があります......ビジネスがますます複雑になるにつれて、transfer()メソッドのロジックはますます複雑になり、徐々に上記の数百行から数千行のコードになります。コードになります。経験豊富なプログラマは、この「メソッド抽出」に似たリファクタリングを行い、振込操作を論理的にいくつかのブロックに分割することができます。残高が十分かどうかを判断し、口座の信用指数を判断して1回の振込の上限金額を決定し、銀行の手数料ポリシーに従って手数料を計算し、取引の詳細を記録する......。...、こうしてコードをより構造化します。これは良いスタートですが、まだ明らかに不十分です。

ある日、システム要件に新しいモジュールが追加され、銀行の利用者がオンラインで買い物ができるように、オンラインショッピングモールがシステムに追加されたとします。オンラインショッピングにも、残高が十分かどうかを判断し、残高を変更するために口座に対して引き落とし操作を実行し、手数料を請求し、取引明細を作成する...という、口座のクレジット引き落とし操作と同じか類似したビジネスロジックがたくさんあります。...

このような状況に直面した場合、2つの解決策があります:

2) OnlineShoppingServiceImplがAccountingServiceImplの同じサービスを呼び出すようにします。

明らかに、2番目の方法は、最初の方法よりも良い、明確な構造とメンテナンスが容易です。しかし、問題は、これは、オンラインショッピングモールサービスモジュールとアカウントの収支サービスモジュールの間に不要な依存関係を形成することです、システムのカップリングが高いので、より柔軟性と拡張性のために、システムは、各ビッグビジネスモジュールが独立して展開するためだけでなく、2つの間の依存関係のため、間違いなく、設計、開発、運用保守のコストを増加させる分散コールを確立します。

経験豊富な設計者であれば、同じビジネスロジックを、上記の両方のビジネスモジュールのパブリックサービスとして使用できる新しいサービスに抽出するという、3つ目の解決策を見つけることができるかもしれません。これはまさに、ドメイン駆動設計を使用した解決策です。

プロセス指向のドメイン駆動設計アプローチ

スペースを節約するため、ここでは最も複雑なビジネス要件に対応できるよう、デザインはストレートになります。

ドメイン駆動設計のキーコンセプトのひとつにドメインモデルがあります。 まず、ビジネスドメインに基づいて、以下のようなコアビジネスオブジェクトモデルを抽象化します:

  • アカウント:アカウントは、システム全体の最もコアなビジネスオブジェクトであり、次の属性が含まれています:オブジェクトの識別、アカウント番号、それが有効な識別であるかどうか、残高、凍結金額、アカウント取引の詳細のコレクション、およびアカウントの信用格付け。

  • AccountTransactionDetails:アカウントのトランザクションの詳細、それはアカウントに従属し、各アカウントは、複数のトランザクションの詳細を持って、それは次の属性が含まれています:オブジェクトの識別、アカウントに属しているトランザクションの種類、トランザクションの量が発生すると、トランザクションが発生します。

  • AccountCreditDegree(アカウント・クレジット・ディグリー):アカウント・クレジット・ディグリー(Account Credit Degree)。

  • BankTransactionFeeCalculator: 銀行取引手数料の計算。定数: 取引ごとの手数料の上限。

ドメインオブジェクトの非常に重要な特徴は、それ自身の属性と状態を持つことに加えて、それ自身の責任範囲に属する振る舞いを持ち、ドメイン内のドメインビジネスロジックをカプセル化することです。したがって、ビジネス要件に基づいてドメインオブジェクトのビジネスメソッドを設計するために、さらなるモデリングが行われます:

単一責任の原則に従って、機能要件に記述された機能は、異なるドメインオブジェクトに合理的に割り当てられます:

アカウント

  • クレジット:銀行口座に入金された金額、信用
  • 借方:銀行口座からの振替額、借方
  • transferTo:指定した口座に一定額を送金。
  • createTransactionDetails: トランザクションの詳細を作成します。
  • updateCreditIndex: 口座のクレジットインデックスを更新します。

AccountCreditDegree:

  • getMaxTransactionAmount:所属アカウントの各取引の最大金額を取得します。

BankTransactionFeeCalculator:

  • calculateTransactionFee: 取引情報に基づいて取引手数料を計算します。

この設計では、前の例でサービスオブジェクトに配置されたすべてのビジネスロジックは、関連する職務を担当する別のドメインオブジェクトに割り当てられます。 次のタイミング図は、AccountingServiceImplの送金サービスの実装ロジックを説明します:

AccountingServiceImpl.transfer()の実装ロジックをもう一度見てください:

public class AccountingServiceImpl implements AccountingService {
 public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) 
 throws AccountDomainException {
 Account srcAccount = accountRepository.getAccount(srcAccountId);
 Account destAccount = accountRepository.getAccount(destAccountId);
 srcAccount.transferTo(destAccount,amount);
 }
}

ご覧のように、上記の例の複雑なビジネス・ロジック:残高が十分かどうかの判断、口座が利用可能かどうかの判断、口座残高の変更、手数料の計算、取引金額の決定、取引ジャーナルの生成......は、もはやAccountingServiceImplのtransfer()メソッドには存在しません。Accountの2つのメソッドが保護宣言されている理由は、借方メソッドと貸方メソッドが呼び出されたときに、AccountingServiceImplのために、トランザクションの詳細を生成し、勘定科目の貸方を更新する結果として、AccountingServiceImplが呼び出されるからです。AccountingServiceImplにとって、トランザクションの詳細を生成し、アカウントのクレジットインデックスを更新することは、その責任範囲外であるため、このロジックを使用する必要はありませんし、使用する権利もありません。

おわかりのように、ドメイン駆動設計を使用すると、少なくとも次のような利点があります:

  • ビジネスロジックは異なるドメインオブジェクトに適度に分散され、コード構造はより明確で、読みやすく、保守しやすくなります。
  • オブジェクトの責任は、より均質でまとまりがあります。
  • 複雑なビジネスモデルをドメインモデリングによって明確に表現でき、開発者はソースコードを読まなくてもビジネスやシステムの構造を理解できるため、既存システムのメンテナンスや反復開発が容易になります。

そして、あなたが新しいモジュールのオンラインショッピングモールに参加する必要がある場合を見て、開発者が行う必要がある、上記の3番目のオプションを覚えていますか?つまり、銀行のオンライン決済システムとオンラインショッピングモールのシステムサービス、実際には、この公共サービスは、要するに、これらはドメインオブジェクトのドメインロジックを持っているということです:アカウント、AccountCreditDegree......、ドメイン駆動設計も大きな利点を見つけることができます。このことからも、ドメイン駆動設計の大きな利点がわかります:

  • このシステムは高度にモジュール化されており、コードの再利用性が高く、ロジックの重複があまりありません。
Read next

正規表現

序文\n 文字列データの処理では正規表現がよく使われ、python のモジュール re が使われます。以下では、re.sub() の詳しい使い方を実例を通して紹介します。

Apr 6, 2020 · 4 min read