Tuesday, June 3, 2014

Quantifying Domain Model versus Transaction Script

I've been conducting training classes (with Orange and Bronze) that cover topics like TDD, design patterns (GoF), patterns of enterprise application architecture (based on PoEAA book by Martin Fowler), and others. And a question keeps coming up about comparing (and quantifying) the benefits of domain model pattern compared to transaction script. So, I thought I'd post an explanation here.

Note that the Transaction Script pattern is not bad. Fowler himself says that there are virtues to this pattern:

The glory of Transaction Script is its simplicity. Organizing logic this way is natural for applications with only a small amount of logic, and it involves very little overhead either in performance or in understanding.

It's hard to quantify the cutover level, especially when you're more familiar with one pattern than the other. You can refactor a Transaction Script design to a Domain Model design, but it's harder than it needs to be.
However much of an object bigot you become, don't rule out Transaction Script. there are a lot of simple problems out there, and a simple solution will get you up and running faster.

(PoEAA p.111-112)

Here, I used a simple banking example to illustrate the difference between Transaction Script and Domain Model patterns in organizing domain logic. Then, I'll use metrics like method lines of code, and cyclomatic complexity.

Banking Example

In the banking example, we shall implement a very simple money transfer, where an amount is transferred from one account to another.

Money Transfer Overview

The MoneyTransferService shall be implemented in two ways: one using Transaction Script, and another using Domain Model.

public interface MoneyTransferService {
  BankingTransaction transfer(
      String fromAccountId, String toAccountId, double amount);
}

Two Implementations of Money Transfer Service

Transaction Script

Using a Transaction Script design, the domain logic for transferring money between two accounts is all placed inside the MoneyTransferService implementation.

public class MoneyTransferServiceTransactionScriptImpl
      implements MoneyTransferService {
  private AccountDao accountDao;
  private BankingTransactionRepository bankingTransactionRepository;
  . . .
  @Override
  public BankingTransaction transfer(
      String fromAccountId, String toAccountId, double amount) {
    Account fromAccount = accountDao.findById(fromAccountId);
    Account toAccount = accountDao.findById(toAccountId);
    . . .
    double newBalance = fromAccount.getBalance() - amount;
    switch (fromAccount.getOverdraftPolicy()) {
    case NEVER:
      if (newBalance < 0) {
        throw new DebitException("Insufficient funds");
      }
      break;
    case ALLOWED:
      if (newBalance < -limit) {
        throw new DebitException(
            "Overdraft limit (of " + limit + ") exceeded: " + newBalance);
      }
      break;
    }
    fromAccount.setBalance(newBalance);
    toAccount.setBalance(toAccount.getBalance() + amount);
    BankingTransaction moneyTransferTransaction =
        new MoneyTranferTransaction(fromAccountId, toAccountId, amount);
    bankingTransactionRepository.addTransaction(moneyTransferTransaction);
    return moneyTransferTransaction;
  }
}

The Account entity is merely a bag of getters and setters.

// @Entity
public class Account {
  // @Id
  private String id;
  private double balance;
  private OverdraftPolicy overdraftPolicy;
  . . .
  public String getId() { return id; }
  public void setId(String id) { this.id = id; }
  public double getBalance() { return balance; }
  public void setBalance(double balance) { this.balance = balance; }
  public OverdraftPolicy getOverdraftPolicy() { return overdraftPolicy; }
  public void setOverdraftPolicy(OverdraftPolicy overdraftPolicy) {
    this.overdraftPolicy = overdraftPolicy;
  }
}
The OverdraftPolicy is an enumerated type.
public enum OverdraftPolicy {
  NEVER, ALLOWED
}

Domain Model

Using a Domain Model design, the domain logic for transferring money between two accounts is spread across. This keeps it simple and easier to maintain.

public class MoneyTransferServiceDomainModelImpl
      implements MoneyTransferService {
  private AccountRepository accountRepository;
  private BankingTransactionRepository bankingTransactionRepository;
  . . .
  @Override
  public BankingTransaction transfer(
      String fromAccountId, String toAccountId, double amount) {
    Account fromAccount = accountRepository.findById(fromAccountId);
    Account toAccount = accountRepository.findById(toAccountId);
    . . .
    fromAccount.debit(amount);
    toAccount.credit(amount);
    BankingTransaction moneyTransferTransaction =
        new MoneyTranferTransaction(fromAccountId, toAccountId, amount);
    bankingTransactionRepository.addTransaction(moneyTransferTransaction);
    return moneyTransferTransaction;
  }
}

The Account entity contains behavior and domain logic. Notice how it contains #debit(double) and #credit(double) methods, and not just getters and setters.

// @Entity
public class Account {
  // @Id
  private String id;
  private double balance;
  private OverdraftPolicy overdraftPolicy;
  . . .
  public double balance() { return balance; }
  public void debit(double amount) {
    this.overdraftPolicy.preDebit(this, amount);
    this.balance = this.balance - amount;
    this.overdraftPolicy.postDebit(this, amount);
  }
  public void credit(double amount) {
    this.balance = this.balance + amount;
  }
}

The OverdraftPolicy has two implementations that contain logic. Based on business rules, the OverdraftPolicy implementations throw exceptions to prevent the Account balance from being debited.

public interface OverdraftPolicy {
  void preDebit(Account account, double amount);
  void postDebit(Account account, double amount);
}
public class NoOverdraftAllowed implements OverdraftPolicy {
  public void preDebit(Account account, double amount) {
    double newBalance = account.balance() - amount;
    if (newBalance < 0) {
      throw new DebitException("Insufficient funds");
    }
  }
  public void postDebit(Account account, double amount) {
  }
}
public class LimitedOverdraft implements OverdraftPolicy {
  private double limit;
  . . .
  public void preDebit(Account account, double amount) {
    double newBalance = account.balance() - amount;
    if (newBalance < -limit) {
      throw new DebitException(
          "Overdraft limit (of " + limit + ") exceeded: " + newBalance);
    }
  }
  public void postDebit(Account account, double amount) {
  }
}

Metrics

Now here are some of the metrics (via Eclipse Metrics Plugin).

Transaction ScriptDomain Model
MetricMaximumMaximum
McCabe Cyclomatic Complexity52
Number of Classes46
Method Lines of Code259
TotalTotal
Total Lines of Code8296

Now here are the metrics screenshots for transaction script
and domain model.

Conclusion

The resulting overall lines of code are almost the same. The Domain Model pattern produces more classes, and simpler methods.

There are more things to compare than just lines of code and cyclomatic complexity. For example, the Domain Model pattern needs more OO design skill, and Transaction Script pattern is so easy to implement.

The good thing is, there's no need to make a decision up-front. One can always start with a Transaction Script (i.e. do the simplest thing that could possibly work), and when complexity starts to set in, it can be refactored to have richer domain entities, and work its way to using a Domain Model pattern.

Let me know (via comments) if anyone wants to see the code. I can upload it to GitHub.

7 comments:

  1. HI thank you for the post.
    What if as my application grows, I start with a lot of transaction scripts , and then extract the duplicated part into domain entities? (Letting code re-usability decide the specific code belongs to transaction scripts or domain model. )
    Do you think this is a good strategy?

    ReplyDelete
    Replies
    1. Thanks for asking, 阿川.

      Yes, you can start with several transaction scripts. Then extract duplicate code (or refactor) into common methods. These methods can possibly reside in the domain entities (using proper responsibility assignment principles -- GRASP https://en.wikipedia.org/wiki/GRASP_%28object-oriented_design%29 and other patterns).

      Delete
  2. Be interesting to see how the ratios/comparison changes as the application grows.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. For Domain model, is it a good idea to write behavior method in entity class such as debit, credit methods in Account class? Should we create a business class for Account, then use mapper to map data between them?

    ReplyDelete
  5. Excellent Post, changed my perception to Transaction Script and Domain Model

    ReplyDelete