【问题标题】:DDD Approach to Access External Information访问外部信息的 DDD 方法
【发布时间】:2012-06-28 09:19:51
【问题描述】:

我有一个现有的银行应用程序类,如下所示。银行账户可以是 SavingsBankAccount 或 FixedBankAccount。有一个称为 IssueLumpSumInterest 的操作。对于 FixedBankAccount,仅当帐户所有者没有其他帐户时才需要更新余额。

这要求 FixedBankAccount 对象了解帐户所有者的其他帐户。如何按照 SOLID/DDD/GRASP/Information Expert 模式做到这一点?

namespace ApplicationServiceForBank
{

public class BankAccountService
{
    RepositoryLayer.IRepository<RepositoryLayer.BankAccount> accountRepository;
    ApplicationServiceForBank.IBankAccountFactory bankFactory;

    public BankAccountService(RepositoryLayer.IRepository<RepositoryLayer.BankAccount> repo, IBankAccountFactory bankFact)
    {
        accountRepository = repo;
        bankFactory = bankFact;
    }

    public void IssueLumpSumInterest(int acccountID)
    {
        RepositoryLayer.BankAccount oneOfRepositroyAccounts = accountRepository.FindByID(p => p.BankAccountID == acccountID);

        int ownerID = (int) oneOfRepositroyAccounts.AccountOwnerID;
        IEnumerable<RepositoryLayer.BankAccount> accountsForUser = accountRepository.FindAll(p => p.BankUser.UserID == ownerID);

        DomainObjectsForBank.IBankAccount domainBankAccountObj = bankFactory.CreateAccount(oneOfRepositroyAccounts);

        if (domainBankAccountObj != null)
        {
            domainBankAccountObj.BankAccountID = oneOfRepositroyAccounts.BankAccountID;
            domainBankAccountObj.AddInterest();

            this.accountRepository.UpdateChangesByAttach(oneOfRepositroyAccounts);
            //oneOfRepositroyAccounts.Balance = domainBankAccountObj.Balance;
            this.accountRepository.SubmitChanges();
        }
    }
}

public interface IBankAccountFactory
{
    DomainObjectsForBank.IBankAccount CreateAccount(RepositoryLayer.BankAccount repositroyAccount);
}

public class MySimpleBankAccountFactory : IBankAccountFactory
{
    public DomainObjectsForBank.IBankAccount CreateAccount(RepositoryLayer.BankAccount repositroyAccount)
    {
        DomainObjectsForBank.IBankAccount acc = null;

        if (String.Equals(repositroyAccount.AccountType, "Fixed"))
        {
            acc = new DomainObjectsForBank.FixedBankAccount();
        }

        if (String.Equals(repositroyAccount.AccountType, "Savings"))
        {
            //acc = new DomainObjectsForBank.SavingsBankAccount();
        }

        return acc;
    }
}

}

namespace DomainObjectsForBank
{

public interface IBankAccount
{
    int BankAccountID { get; set; }
    double Balance { get; set; }
    string AccountStatus { get; set; }
    void FreezeAccount();
    void AddInterest();
}

public class FixedBankAccount : IBankAccount
{
    public int BankAccountID { get; set; }
    public string AccountStatus { get; set; }
    public double Balance { get; set; }

    public void FreezeAccount()
    {
        AccountStatus = "Frozen";
    }

    public void AddInterest()
    {
        //TO DO: Balance need to be updated only if the person has no other accounts.
        Balance = Balance + (Balance * 0.1);
    }
}

}

阅读

  1. Issue in using Composition for “is – a “ relationship

  2. 实现业务逻辑 (LINQ to SQL) http://msdn.microsoft.com/en-us/library/bb882671.aspx

  3. 构建 LINQ to SQL 应用程序

  4. 使用 LINQ to SQL 探索 N 层架构 http://randolphcabral.wordpress.com/2008/05/08/exploring-n-tier-architecture-with-linq-to-sql-part-3-of-n/

  5. Confusion between DTOs (linq2sql) and Class objects!

  6. Domain Driven Design (Linq to SQL) - How do you delete parts of an aggregate?

【问题讨论】:

  • 您经常将“repository”拼写为“repositroy”
  • 嗨@Lijo - 你让我回答这个问题。 SonOfPirate 做得很好,我没有什么可以补充到他/她的回答中了。希望这会有所帮助...
  • 奇怪的是你有存储库对象和域对象——你的存储库不应该只返回域对象吗?
  • @mouters。我是存储库模式和 LINQ to SQL 的新手。那么,当 LINQ to SQL 生成 BankAccount 实体时,我应该如何将它们设为 SavingsBankAccount 和 FixedBankAccount?您能帮忙提供代码/示例/参考吗?

标签: c# .net design-patterns domain-driven-design solid-principles


【解决方案1】:

我注意到的第一件事是银行账户工厂的不当使用。存储库应该使用工厂来根据从数据存储中检索到的数据创建实例。因此,您对 accountRepository.FindByID 的调用将返回 FixedBankAccount 或 SavingsBankAccount 对象,具体取决于从数据存储返回的 AccountType。

如果利息仅适用于 FixedBankAccount 实例,那么您可以执行类型检查以确保您使用正确的帐户类型。

public void IssueLumpSumInterest(int accountId)
{
    var account = _accountRepository.FindById(accountId) as FixedBankAccount;

    if (account == null)
    {
        throw new InvalidOperationException("Cannot add interest to Savings account.");
    }

    var ownerId = account.OwnerId;

    if (_accountRepository.Any(a => (a.BankUser.UserId == ownerId) && (a.AccountId != accountId)))
    {
        throw new InvalidOperationException("Cannot add interest when user own multiple accounts.");
    }

    account.AddInterest();

    // Persist the changes
}

注意:FindById 应该只接受 ID 参数而不是 lambda/Func。您已经通过名称“FindById”指示了如何执行搜索。将“accountId”值与 BankAccountId 属性进行比较的事实是隐藏在方法中的实现细节。如果您想要使用 lambda 的通用方法,请将方法命名为“FindBy”。

如果所有实现都不支持该行为,我也不会将 AddInterest 放在 IBankAccount 接口上。考虑一个公开 AddInterest 方法的单独 IInterestEarningBankAccount 接口。如果您将来添加其他支持此行为的帐户类型,我还会考虑在上述代码中使用该接口而不是 FixedBankAccount 以使代码更易于维护和扩展。

【讨论】:

  • 谢谢...关于 if (_accountRepository.Any(a => (a.BankUser.UserId == ownerId) && (a.AccountId != accountId))) 的问题。当我们这样做时,BankAccountService 对象会进行业务逻辑处理。有没有其他方法可以解决这个问题?
  • 就个人而言,我认为这是合适的,因为您的服务正在处理多个帐户(在这种情况下)。这正是域服务应该做的。您的域对象不应包含此逻辑,因为它是更高级别的要求,需要了解多个域对象。另外,该帐户与其他帐户的唯一关系是所有者,这可能会导致您考虑添加所有者聚合根,但我认为这在这种情况下不合适,因为所有者确实与利益规则。我就是这样做的。
  • 谢谢。考虑到所有者聚合根,我理解将逻辑保留在应用程序服务中的理由。但是,您能否提供一个参考,在哪里添加聚合根是合适的以及如何实现它?如果你可以用代码更新答案,那就没有了。
  • 聚合根背后的基本原理是当您拥有不/不能/不应该存在于根上下文之外的对象时。在带有 OrderLine 子对象的 Order 聚合的经典示例中,在父 Order 的上下文之外拥有 OrderLine 是没有意义的。在您的情况下,情况并非如此。但是,您可能会发现将 Transaction 建模为 BankAccount 根的子对象是有意义的。
【解决方案2】:

通过阅读您的要求,我会这样做:

//Application Service - consumed by UI
public class AccountService : IAccountService
{
    private readonly IAccountRepository _accountRepository;
    private readonly ICustomerRepository _customerRepository;

    public ApplicationService(IAccountRepository accountRepository, ICustomerRepository customerRepository)
    {
        _accountRepository = accountRepository;
        _customerRepository = customerRepository;
    }

    public void IssueLumpSumInterestToAccount(Guid accountId)
    {
        using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
        {
            Account account = _accountRepository.GetById(accountId);
            Customer customer = _customerRepository.GetById(account.CustomerId);

            account.IssueLumpSumOfInterest(customer);

            _accountRepository.Save(account);
        }
    }
}

public class Customer
{
    private List<Guid> _accountIds;

    public IEnumerable<Guid> AccountIds
    {
        get { return _accountIds.AsReadOnly();}
    }
}

public abstract class Account
{
    public abstract void IssueLumpSumOfInterest(Customer customer);
}

public class FixedAccount : Account
{
    public override void  IssueLumpSumOfInterest(Customer customer)
    {
        if (customer.AccountIds.Any(id => id != this._accountId))
            throw new Exception("Lump Sum cannot be issued to fixed accounts where the customer has other accounts");

        //Code to issue interest here
    }
}   

public class SavingsAccount : Account
{
    public override void  IssueLumpSumOfInterest(Customer customer)
    {
        //Code to issue interest here
    }
}
  1. Account 聚合上的 IssueLumpSumOfInterest 方法需要 Customer 聚合来帮助决定是否应发放利息。
  2. 客户汇总包含帐户 ID 列表 - 不是帐户汇总列表。
  3. 基类“Account”有一个多态方法 - FixedAccount 检查客户是否有任何其他帐户 - SavingsAccount 不执行此检查。

【讨论】:

    【解决方案3】:

    2 分钟扫描答案..

    • 不确定为什么需要 2 个 BankAccount 表示 RepositoryLayer.BankAccountDomainObjectsForBank.IBankAccount。隐藏持久层耦合一个..只处理服务中的域对象。
    • 不要传递/返回 Null - 我认为这是个好建议。
    • finder 方法类似于从集合列表中选择项目的 LINQ 方法。您的方法看起来想要获得第一个匹配项并退出。在这种情况下,您的参数可以是简单的原语 (Ids) 与 lambdas。

    总体思路似乎是正确的。服务封装了这个事务的逻辑——而不是域对象。如果这种情况发生变化,只有一个地方需要更新。

    public void IssueLumpSumInterest(int acccountID)
    {
        var customerId = accountRepository.GetAccount(accountId).CustomerId;
    
        var accounts = accountRepository.GetAccountsForCustomer(customerId);
        if ((accounts.First() is FixedAccount) && accounts.Count() == 1)
        {
           // update interest    
        }
    }
    

    【讨论】:

      【解决方案4】:

      让我觉得奇怪的事情:

      • 您的IBankAccount 有一个方法FreezeAccount,但我认为所有帐户都会有非常相似的行为?或许需要一个 BankAccount 类来实现某些接口?
      • AccountStatus 应该是一个枚举?如果帐户是“Forzen”,会发生什么?

      【讨论】:

      • 每种类型的帐户都有自己的方式来实现 FreezeAccount 操作。
      • @Lijo 但看起来还是会有很多共同点。
      猜你喜欢
      • 2014-11-05
      • 1970-01-01
      • 2011-08-23
      • 1970-01-01
      • 1970-01-01
      • 2012-02-28
      • 2011-09-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多