ライブラリアプリケーションを構築しています。 図書館に登録されている人から借りるに本をデフォルトの期間(4週間)許可する必要があるとします。
以下のコードでLoanというAggregateRootを使用してドメインのモデル化を開始しました。
_public class Loan : AggregateRoot<long>
{
public static int DefaultLoanPeriodInDays = 30;
private readonly long _bookId;
private readonly long _userId;
private readonly DateTime _endDate;
private bool _active;
private Book _book;
private RegisteredLibraryUser _user;
public Book Book => _book;
public RegisteredLibraryUser User => _user;
public DateTime EndDate => _endDate;
public bool Active => _active;
private Loan(long bookId, long userId, DateTime endDate)
{
_bookId = bookId;
_userId = userId;
_endDate = endDate;
_active = true;
}
public static Loan Create(long bookId, long userId)
{
var endDate = DateTime.UtcNow.AddDays(DefaultLoanPeriodInDays);
var loan = new Loan(bookId, userId, endDate);
loan.Book.Borrow();
loan.AddDomainEvent(new LoanCreatedEvent(bookId, userId, endDate));
return loan;
}
public void EndLoan()
{
if (!Active)
throw new LoanNotActiveException(Id);
_active = false;
_book.Return();
AddDomainEvent(new LoanFinishedEvent(Id));
}
}
_
そして、私のBookエンティティは次のようになります。
_public class Book : Entity<long>
{
private BookInformation _bookInformation;
private bool _inStock;
public BookInformation BookInformation => _bookInformation;
public bool InStock => _inStock;
private Book(BookInformation bookInformation)
{
_bookInformation = bookInformation;
_inStock = true;
}
public static Book Create(string title, string author, string subject, string isbn)
{
var bookInformation = new BookInformation(title, author, subject, isbn);
var book = new Book(bookInformation);
book.AddDomainEvent(new BookCreatedEvent(bookInformation));
return book;
}
public void Borrow()
{
if (!InStock)
throw new BookAlreadyBorrowedException();
_inStock = false;
AddDomainEvent(new BookBorrowedEvent(Id));
}
public void Return()
{
if (InStock)
throw new BookNotBorrowedException(Id);
_inStock = true;
AddDomainEvent(new BookReturnedBackEvent(Id, DateTime.UtcNow));
}
}
_
ご覧のとおり、ローンの集計ルートを作成するために、静的なファクトリメソッドを使用して、借りている本のIDと借りているユーザーのIDを渡します。ここでは、IDではなくこれらのオブジェクト(ブックとユーザー)への参照を渡す必要がありますか?どちらのアプローチが優れていますか?ご覧のとおり、私のBookエンティティには、書籍の可用性を示すプロパティ(InStock
プロパティ)もあります。次のユースケースで、たとえばLoadCreatedEvent
のハンドラーで、このプロパティを更新する必要がありますか?または、ここのAggregateRoot内で更新する必要がありますか?ここで更新する必要がある場合は、IDだけでなく、ブック参照全体を渡して、メソッド_book.Borrow()
を呼び出せるようにする必要があります。 DDDアプローチでかなり正確に実行したいので、この時点で行き詰まっています。それとも私は間違った側からそれを始めて、私は何かを逃したり、間違った方法で考えたりしていますか?
ドメイン主導の設計に関するスケーラブルな値オブジェクト、エンティティ、および集計ルートの構築は、おそらくDDDの概念自体と同じくらい長い間、検討されてきました。 DDDモデリングへの一般的なアプローチは次のとおりです。「モデルにはビジネスロジックが含まれているため、それらは重いものである必要があり、主に書き込み側で使用されるため、これで問題ありません。」ただし、スケーラブルなドメインモデルがないと、長期的にはあなたを傷つける可能性があります。
ドメインモデルは動作をカプセル化する必要があります。これらはビジネスルールのモデリングを進化させたものであり、ロジックをサービスにバックアップするのではなく、それ自体をモデルに組み込む 貧血モデル です。これはまさにドメインモデルの負荷が大きい動作ですが、必ずしもモデル内に格納されているデータも重いことを意味するわけではありません。
私にとって本当に効果的な実績のあるパターンは次のとおりです。
あなたの場合、ローンの合計は現在持っているものとは裏返しになり、次のようになります。
_public class Loan : AggregateRoot<long>
{
public static int DefaultLoanPeriodInDays = 30;
private readonly long _bookId;
private readonly long _userId;
private readonly DateTime _endDate;
private bool _active;
private Loan(long bookId, long userId, DateTime endDate, bool active)
{
_bookId = bookId;
_userId = userId;
_endDate = endDate;
_active = active;
}
public static Loan Create(Book book, RegisteredLibraryUser user)
{
book.Borrow();
var endDate = DateTime.UtcNow.AddDays(DefaultLoanPeriodInDays);
var loan = new Loan(book.Id, user.Id, endDate, true);
loan.AddDomainEvent(new LoanCreatedEvent(loan._bookId, loan._userId, endDate));
return loan;
}
public void EndLoan(BookLookUpService bookLookUpService)
{
if (!Active)
throw new LoanNotActiveException(Id);
_active = false;
bookLookUpService.getById(_bookId).Return();
AddDomainEvent(new LoanFinishedEvent(Id));
}
}
_
このように、ドメインレイヤー内からモデルを構築する場合、モデルはその契約を満たすために必要なすべての依存関係を要求します。同時に、モデルはデータベースから構築するのが非常に簡単です。重いオブジェクトは、本当に必要な場合にのみ読み込まれます(これにより、アプリケーションサーバーとデータベースサーバー間のデータフットプリントも削減されます。これは、アプリケーションで最もコストのかかる操作であることが多いです)。
ただし、かなりの問題があります。 Loan
モデルの現在の表現では、競合状態から保護されません。このモデルでは、Loan
のコンストラクターでbook.Borrows()
を呼び出すときに、他のスレッドが同じブックで同じ呼び出しを現在実行していないという保証はありません。この場合の競合状態は次のようになります。
Id=1
_を使用して本のローンを作成する2つの要求があります。Book:Id=1
_とともに_InStock=true
_をロードします。Book:Id=1
_とともに_InStock=true
_もロードします。Loan
オブジェクトを作成し、渡されたBook
オブジェクトの表現でbook.Borrow()
を呼び出します。Loan
オブジェクトを永続化し、1度しか利用できなかった本の複製ローンを正常に作成しました。問題の明らかな解決策は、lockingを追加することです。したがって、Book
オブジェクトをロードする前に、本の識別子に対してスレッドセーフロックが取得され、他のスレッドのクリティカルセクションがロックされます。プロセスは次のようになります。
Id=1
_を使用して本のローンを作成する2つのリクエストがあります。bookId=1
_をロックし、_Book:Id=1
_を_InStock=true
_とともにロードします。bookId=1
_のロックを取得しようとします。これにより、スレッドが一時停止状態になります。Loan
オブジェクトを作成し、渡されたBook
オブジェクトの表現に対してbook.Borrow()
を呼び出します。Loan
を永続化し、同じデータベーストランザクションでBook
を変更して_InStock=false
_属性を格納し、ロックを解放します。Book:Id=1
_をロードします。InStock
はfalse
に設定されています。Loan
を作成しようとしますが、book.Borrow()
メソッドの呼び出しで失敗します。これは有望に見えますが、ロックは一般的に問題です。ロックはシステムの動作を遅くし、スレッドをブロックすることで不要な負荷を引き起こします。正しく実装されていない場合、ユーザーフレンドリーではありません。この場合、単一のエンティティでのみロックしているため、それほど大したことではありませんが、参照された複数のエンティティを含むより複雑な集約ルートでこの道を進むと、システムに重大なパフォーマンスの問題が発生し、デッドロックにつながる可能性があります。
ロックなしの可能な解決策は、楽観的ロックを導入することです。この場合、ロックは不要であり、オブジェクトの正しい使用は永続化の際に処理されます。 processは次のようになります。
Id=1
_を使用して本のローンを作成する2つのリクエストがあります。Book:Id=1
_を_InStock=true
_および_Version=1
_とともにロードします。Book:Id=1
_と_InStock=true
_および_Version=1
_もロードします。Loan
オブジェクトを作成し、渡されたBook
オブジェクトの表現でbook.Borrow()
を呼び出します。Book
条件を利用して_InStock=false
_を設定することにより、変更されたWHERE
オブジェクトも永続化します:_UPDATE book SET in_stock = book.inStock, version = version + 1 WHERE id = book.id AND version = book.version
_。これにより、更新された1行が正常に返され、スレッド1トランザクションがコミットされます。UPDATE book SET in_stock = book.inStock, version = version + 1 WHERE id = book.id AND version = book.version
_が1であるため、現在は更新された行0を返す_book.version
_ですが、データベースでは本のバージョンは2です、スレッド1によって変更されたため。スレッド2の実行は失敗し、同時実行ヒットによりロールバックされます。残念ながら、どちらのソリューションも、プログラマーが気付くという事実に依存していますが、Book
オブジェクトも永続化する必要があります(ただし、技術的には、上記のシステム変更時にLoan
オブジェクトのみを使用しています)。これにより、操作が不明確になり、ブックオブジェクトの永続化が簡単に忘れられ、他の問題が発生する可能性があります。
幸いにも、まだ考えていない3番目の解決策があるかもしれません。あなたはすでにイベントについて考えていますが、これまでのところ、それらを再利用することはしていません。ただし、イベントは、システムの変更をコードの他の部分に伝達するための優れた方法です。これまでは、ローンを対象とすることで制限されていました。しかし、本を予約することは、ローンの作成につながる実際のプロセスではありませんか?おそらく、予約プロセスは同じ方法でモデル化する必要がありますか?
幸いなシナリオでは、融資プロセスであるBookLoanProcess
は、次のドメインイベントを使用してモデル化できます。
BookLoanRequestedEvent
、BookBorrowedEvent
、BookLoanRequestAcceptedEvent
。ビジネス分析の決定に基づいて、最初にLoan
を保留状態で作成し、BookLoanRequestAcceptedEvent
がシステムに公開された後にのみ完了するか、プロセス/サーガとして機能する別のクラスを作成して、実際にLoan
オブジェクトを作成するのは、システムでBookLoanRequestAcceptedEvent
が発生した後でのみです。
これにより、各モジュールの責任が効果的に分割されます。 BookLoanRequestedEvent
が発生すると、Book
モジュールは指定されたイベントをリッスンし、_Id=BookLoanRequestedEvent.BookId
_を含む本をスレッドセーフな方法でBorrow
に書き込もうとします。この操作が成功すると、BookBorrowedEvent
モジュールがパブリッシュされ、BookLoanProcess
モジュールが反応します。
BookBorrowedEvent.BookId
_の本のアクティブな本予約プロセスを見つけてください。AcceptLoanRequest
を発行したBookLoanRequestAcceptedEvent
メソッドを呼び出します。これで、Loan
モジュールはBookLoanRequestAcceptedEvent
をリッスンし、スレッドセーフな方法でBookLoanProcess
をロードします。反応として、次に、Loan
オブジェクトから必要なデータを取得するBookLoanProcess
オブジェクトを作成します。
このようなプロセスを使用して本の貸出をモデル化すると、ビジネス開発者にとって他の利点が得られる可能性があります。つまり、貸出プロセス中のすべてのステップに関する情報を保持しながら、貸出プロセスを適切にロールバックする機能を導入し、許可のみを行う可能性を導入します進行中のプロセス中のローンに対する特定のアクション。ただし、ローンの作成後にローンを変更することはできず、事実上不変として扱われます。
結局のところ、ドメイン主導の設計は、プログラム化されたあなただけでなく、利害関係者も知っている統一言語でのモデリングに関するものです。そのため、コードは会社のプロセスを表す必要があります。 BookLoanProcess
プロセスが会社で意味をなさない場合は、モデルとしないでください。コードとビジネスアナリストの間に不一致が生じるだけです。
提供した例を具体的に説明する前に、DDDの目的を思い出す必要があります。システムの動作要件の有用な抽象化を提供するため。
投稿の最初の文は、非常に明確なユースケースの概要を示しています(ユビキタス言語のコンポーネントを強調表示して完了しています)。その後、バラバラになり始めます!あなたが話しているLoan
のことは何ですか?それはユースケースの一部ではありませんでした。私たちがあなたの意図に少し近いものを導き出すことができないかどうか見てみましょう。
ルールから始めましょう。私の知る限り、このシステムには2つの不変式しかありません。
上記を踏まえて、各ユースケースのコマンドハンドラーがどのようになるかを書きましょう。
// BorrowBookHandler
var registeredUser = users.Find(cmd.UserId);
var borrowingCard = catalogue.FindAvailable(cmd.Isbn); // may throw "Book is not available"
var entry = registeredUser.FillOutCard(borrowingCard, cmd.FromDate, cmd.ToDate); // may throw "Book is reserved during date range"
catalogue.RecordEntry(entry); // save changes
そして:
// ReturnBookHandler
var entry = catalogue.LookUpEntry(cmd.BookId); // may throw "Card entry not found"
entry.MarkReturned(cmd.ReturnDate); // may throw "Book already returned"
catalogue.RecordEntry(entry); // save changes
ここで最初に気づくのは、Book
エンティティが含まれていないことです。これは理にかなっていますか?著者が本を借りることと一体何をしているのですか?代わりに、このプロセスを管理するために使用される新しい概念BorrowingCard
を導入しました(おそらく、図書館の本の表紙または裏表紙にある小さなポケットに挿入されたカードを覚えるのに十分古いものです)。
本を借りるのは簡単です。登録ユーザーがいることを確認します。次に、データストアをチェックして、ISNBを指定すると、最初に利用可能なBorrowingCard
を返します(ユーザーは必ずしもチェックしませんどの本をチェックします) out-ただし、このプロセスが直接行われている場合、システムはBookId
を使用できます)。次に、RegisteredUser
に新しいBorrowingEntry
を生成させ、必要な情報を記録します(暗黙的に時間を扱わないでください!)。最後に、新しいBorrowingEntry
を永続ストレージに記録します。
本を返すのはさらに簡単です。返される本に関連付けられているBorrowingEntry
を調べ、返されたものとしてマークを付け、保存します(RegisteredUser
を実行します) not返品を容易にする必要があります)。
ここでの重要な洞察は、「本を借りる」と「本を返す」はユースケースであり、アプリケーションの観点からは、ドメインにdetailsを実装します。
上記は、個々のエンティティのメカニズムを完全に説明することなく十分であると私は信じています。隙間を埋めさせていただきます。