Quantifying Domain Model versus Transaction Script
source link: http://lorenzo-dee.blogspot.com/2014/06/quantifying-domain-model-vs-transaction-script.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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.
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);
}
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;
}
}
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 ModelMetricMaximumMaximumMcCabe Cyclomatic Complexity52Number of Classes46Method Lines of Code259TotalTotalTotal 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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK