0

我正在构建一个图书馆应用程序。假设我们有一个要求,让图书馆中的注册人员在某个默认时间段(4 周)内借书。

我开始使用以下代码调用 AggregateRoot 对我的域进行建模Loan

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

如您所见,我正在使用静态工厂方法来创建我的 Loan 聚合根,其中我传递了借书的身份和将要借书的用户身份。我应该在这里传递对这些对象(书籍和用户)的引用而不是 id 吗?哪种方法更好?如您所见,我的 Book 实体也有一个属性,它指示一本书的可用性(InStock属性)。我是否应该在下一个用例中更新此属性,例如在 的处理程序中LoadCreatedEvent?还是应该在我的 AggregateRoot 中更新它?如果它应该在我的聚合中更新,我应该传递整个书籍参考,而不仅仅是一个 ID 以便能够调用它的方法_book.Borrow()

我被困在这一点上,因为我想用 DDD 方法做得非常正确。还是我从错误的角度开始做,我错过了一些东西或以错误的方式思考?

4

2 回答 2

1

DomainEvents是在同一域内处理的内存中事件。

您一起提交或回滚整个“事务”。将域事件视为 DTO,它需要保存与域中刚刚发生的事情相关的所有信息。因此,只要您拥有该信息,我认为您是否通过 Id 或整个object.

id尽管该信息足以将信息传递给DomainEventHandler.

此外,请参阅Microsoft Docs中类似场景的此示例,其中它们仅传递UserIdCardTypeId与域事件中的所有其他相关信息一起传递。

public class OrderStartedDomainEvent : INotification {
public string UserId { get; }
public int CardTypeId { get; }
public string CardNumber { get; }
public string CardSecurityNumber { get; }
public string CardHolderName { get; }
public DateTime CardExpiration { get; }
public Order Order { get; }

public OrderStartedDomainEvent(Order order,
                               int cardTypeId, string cardNumber,
                               string cardSecurityNumber, string cardHolderName,
                               DateTime cardExpiration)
{
    Order = order;
    CardTypeId = cardTypeId;
    CardNumber = cardNumber;
    CardSecurityNumber = cardSecurityNumber;
    CardHolderName = cardHolderName;
    CardExpiration = cardExpiration;
} }
于 2020-04-04T21:50:04.067 回答
1

您的示例代码中有几件事看起来很可疑:

第一个,贷款执行以下操作:

loan.Book.Borrow();

但它没有提到 Book,乍一看,它似乎也不应该。

第二个,您的 Book 实体似乎有很多职责:保存书籍信息,如作者、标题、主题、保存库存信息和管理借阅状态。这对于聚合来说是太多的责任,更不用说聚合中的实体了。这也引出了一个问题,一本书真的属于贷款吗?看起来很奇怪。

我会建议,重新考虑你的聚合并尝试给它们一个单一的目的。以下内容仅作为您可以进行的思考类型的示例,而不是提议的设计:

首先,在某处拥有一本书是有意义的,它包含书籍信息。您可以完全独立于系统的其余部分来管理图书信息和作者信息。事实上,对于书店、图书馆、出版公司和电子商务网站,这部分看起来几乎相同。

当您对库进行建模时,拥有类似 LibraryItem 之类的东西可能是有意义的(领域专家会知道正确的词)。这个项目可能有一个类型(书、DVD、杂志等)和实际项目的ID,但它不关心标题、描述等,ID 就足够了。潜在地,它还将项目的位置/排序存储在库中。此外,此项目可能具有某种状态,比如 Active/Retired。如果它处于活动状态,则该项目存在于库中。如果它退休了,它就不再存在了。如果您有同一本书的多个项目,您只需创建更多具有相同 BookId 的项目,并且如果可以使用条形码识别具体的实体书,例如,每个项目都将具有唯一的代码,所以您可以通过扫描条形码找到它。

现在是贷款,对我来说,它基本上是一个 ItemId 加上一个 CustomerId(不确定他们是否在这个域中被称为客户)。每次客户想借一个物品,用户都会找到该物品(可能是扫描条形码),然后找到客户。此时,您必须使用 CustomerId、ItemId、日期创建贷款,我想说的不多。这可以是其自身的聚合,也可以仅由 Item 管理。根据您选择的实施方式会明显不同。请注意,您不会重复使用贷款。您将在某处获得可以查询的 Loans 列表,并且该列表不会在 Item 聚合中。这个聚合只需要确保同一项目的2个贷款不允许同时进行。

好吧,这是一个相当长的初步解释来回答您的问题:您需要在允许您借书的同一聚合中管理 InStock。它必须是事务性的,因为您要确保一本书不会一次被多次借阅。但是,不要将一个聚合传递给另一个聚合,而是设计你的聚合,以便它们一起拥有正确的数据和职责。

于 2020-07-04T20:41:15.580 回答