Encapsulation in Domain Driven Design models

asp.net-mvcdomain-driven-designencapsulationentity-framework

I am using EF Code First and I had a model like below.

public class Account
{
    [Required]
    public string AccountNo { get; set; }
    [Required]
    public decimal Balance { get; set; }
}

I used a Service class to withdraw and deposit amount to Account. Then I came across this and realised that Account is Anemic. So I happily added Withdraw and Deposit methods to Account.

public class Account
{
    [Required]
    public string AccountNo { get; set; }
    [Required]
    public decimal Balance { get; set; }
    public void Withdraw(decimal amount)
    {
        // TODO: Check if enough balance is available
        Balance -= amount; 
    }
    public void Deposit(decimal amount)
    {
        Balance += amount; 
    }

}

I was very happy with this until I realised that this does not implement encapsulation correctly. You should not be able to modify the Balance directly – you should use Withdraw and Deposit methods. But in my solution you can modify the Balance directly.

But I cannot make Balance a readonly property as I am using EF code first approach and readonly properties won't result in the column being created in the database table.

I thought of leaving Account as anemic and create a higher level Account class which implements encapsulation and Withdraw/Deposit methods. But in that case the higher level class would not have validation data annotations and I won't get the free functionality provided by the framework.

What is the solution?

Best Answer

There is a fundamental difference between your Data Access Object (DAO) and your Business Object (BO). Your DAO is a one-to-one mapping of what your data store looks like. In this case, for your AccountDAO object, the AccountNo and Balance properties will have getters and setters. Your Account object - the business layer objects - will not look the same as your DAO.

// This is a DAO
public class AccountDAO {
   public string AccountNo { get; set; }
   public decimal Balance { get; set; }
}

// This is a business object
public class Account {
   private readonly string accountNo;
   private readonly decimal openingBalance;
   private readonly List<decimal> adjustments; // This should probably be a list of transactions.

   public class Account(string accountNo, decimal openingBalance) {
      this.accountNo = accountNo;
      this.openingBalance = openingBalance;
      this.adjustments = new List<decimal>();
   }

   public string AccountNo {
      get { return accountNo; }
   }

   public decimal Balance {
      get { return openingBalance + SumOfAdjustments(); }
   }

   public void Deposit(decimal amount) {
      adjustments.Add(amount);
   }

   public void Withdraw(decimal amount) {
      decimal testBalance = Balance - amount;
      if (testBalance < 0.0m)
         throw new AccountBalanceException("Account Balance for " + accountNo + " is less than $0.00.");

      adjustments.Add(-1.0m * amount);
   }

   private decimal SumOfAdjustments() {
      decimal sum = 0.0m;
      foreach (var adjustment in adjustments) {
         sum += adjustment;
      }
      return sum;
   }
}

// Let's come up with a good way of performing actions on an account.    
public class CashDepositActivity {
   private readonly Account account;
   private readonly decimal amount;

   public CashDepositAction(Account account, decimal amount) {
      this.account = account;
      this.amount = amount;
   }

   public void ExecuteAction() {
      // You should probably have some logging here.
      account.Deposit(amount);
   }
}

public class BalanceTransferActivity {
   private readonly Account sourceAccount;
   private readonly Account targetAccount;
   private readonly decimal amount;

   public BalanceTransferActivity(Account sourceAccount, Account targetAccount, decimal amount) {
      this.sourceAccount = sourceAccount;
      this.targetAccount = targetAccount;
      this.amount = amount;
   }

   public void ExecuteAction() {
      sourceAccount.Withdraw(amount);
      targetAccount.Deposit(amount);
   }  
}

This is just an example, of course, and it is by no means an end-all solution. I know tools like EF have myriad examples where the data object, business object, and view/edit object are all the same object, passed from the database to view. In my experience, that works great on small projects where there is not a lot of business logic. However, it breaks down as your business rules and access restrictions grow in complexity.