序
本文次要钻研一下软件开发的 SLAP(Single Level of Abstraction Principle) 准则
SLAP
SALP 即 Single Level of Abstraction Principle 的缩写,即繁多抽象层次准则。
在 Robert C. Martin 的 <<Clean Code>> 一书中的函数章节有提到:
要确保函数只做一件事,函数中的语句都要在同一形象层级上。函数中混淆不同形象层级,往往让人蛊惑。读者可能无奈判断某个表达式是根底概念还是细节。更顽劣的是,就像破损的窗户,一旦细节与根底概念混淆,更多的细节就会在函数中纠结起来。
这与 Don’t Make Me Think 有殊途同归之妙,遵循 SLAP 的代码通常浏览起来不会太吃力。
另外没有循序这个准则的通常是 Leaky Abstraction
要遵循这个准则通常有两个好用的伎俩便是抽取办法与抽取类。
实例 1
public List<ResultDto> buildResult(Set<ResultEntity> resultSet) {List<ResultDto> result = new ArrayList<>();
for (ResultEntity entity : resultSet) {ResultDto dto = new ResultDto();
dto.setShoeSize(entity.getShoeSize());
dto.setNumberOfEarthWorms(entity.getNumberOfEarthWorms());
dto.setAge(computeAge(entity.getBirthday()));
result.add(dto);
}
return result;
}
这段代码蕴含两个抽象层次,一个是循环将 resultSet 转为
List<ResultDto>
,一个是转换 ResultEntity 到 ResultDto
能够进一步抽取转换 ResultDto 的逻辑到新的办法中
public List<ResultDto> buildResult(Set<ResultEntity> resultSet) {List<ResultDto> result = new ArrayList<>();
for (ResultEntity entity : resultSet) {result.add(toDto(entity));
}
return result;
}
private ResultDto toDto(ResultEntity entity) {ResultDto dto = new ResultDto();
dto.setShoeSize(entity.getShoeSize());
dto.setNumberOfEarthWorms(entity.getNumberOfEarthWorms());
dto.setAge(computeAge(entity.getBirthday()));
return dto;
}
这样重构之后,buildResult 就很清晰
实例 2
public MarkdownPost(Resource resource) {
try {this.parsedResource = parse(resource);
this.metadata = extractMetadata(parsedResource);
this.url = "/" + resource.getFilename().replace(EXTENSION, "");
} catch (IOException e) {throw new RuntimeException(e);
}
}
这里的 url 的拼装逻辑与其余几个办法不在一个档次,重构如下
public MarkdownPost(Resource resource) {
try {this.parsedResource = parse(resource);
this.metadata = extractMetadata(parsedResource);
this.url = urlFor(resource);
} catch (IOException e) {throw new RuntimeException(e);
}
}
private String urlFor(Resource resource) {return "/" + resource.getFilename().replace(EXTENSION, "");
}
实例 3
public class UglyMoneyTransferService
{
public void transferFunds(Account source,
Account target,
BigDecimal amount,
boolean allowDuplicateTxn)
throws IllegalArgumentException, RuntimeException
{
Connection conn = null;
try {conn = DBUtils.getConnection();
PreparedStatement pstmt =
conn.prepareStatement("Select * from accounts where acno = ?");
pstmt.setString(1, source.getAcno());
ResultSet rs = pstmt.executeQuery();
Account sourceAccount = null;
if(rs.next()) {sourceAccount = new Account();
//populate account properties from ResultSet
}
if(sourceAccount == null){throw new IllegalArgumentException("Invalid Source ACNO");
}
Account targetAccount = null;
pstmt.setString(1, target.getAcno());
rs = pstmt.executeQuery();
if(rs.next()) {targetAccount = new Account();
//populate account properties from ResultSet
}
if(targetAccount == null){throw new IllegalArgumentException("Invalid Target ACNO");
}
if(!sourceAccount.isOverdraftAllowed()) {if((sourceAccount.getBalance() - amount) < 0) {throw new RuntimeException("Insufficient Balance");
}
}
else {if(((sourceAccount.getBalance()+sourceAccount.getOverdraftLimit()) - amount) < 0) {throw new RuntimeException("Insufficient Balance, Exceeding Overdraft Limit");
}
}
AccountTransaction lastTxn = .. ; //JDBC code to obtain last transaction of sourceAccount
if(lastTxn != null) {if(lastTxn.getTargetAcno().equals(targetAccount.getAcno()) && lastTxn.getAmount() == amount && !allowDuplicateTxn) {throw new RuntimeException("Duplicate transaction exception");//ask for confirmation and proceed
}
}
sourceAccount.debit(amount);
targetAccount.credit(amount);
TransactionService.saveTransaction(source, target, amount);
}
catch(Exception e){logger.error("",e);
}
finally {
try {conn.close();
}
catch(Exception e){//Not everything is in your control..sometimes we have to believe in GOD/JamesGosling and proceed}
}
}
}
这段代码把 dao 的逻辑泄露到了 service 中,另外校验的逻辑也与外围业务逻辑耦合在一起,看起来有点吃力,按 SLAP 准则重构如下
class FundTransferTxn
{
private Account sourceAccount;
private Account targetAccount;
private BigDecimal amount;
private boolean allowDuplicateTxn;
//setters & getters
}
public class CleanMoneyTransferService
{public void transferFunds(FundTransferTxn txn) {Account sourceAccount = validateAndGetAccount(txn.getSourceAccount().getAcno());
Account targetAccount = validateAndGetAccount(txn.getTargetAccount().getAcno());
checkForOverdraft(sourceAccount, txn.getAmount());
checkForDuplicateTransaction(txn);
makeTransfer(sourceAccount, targetAccount, txn.getAmount());
}
private Account validateAndGetAccount(String acno){Account account = AccountDAO.getAccount(acno);
if(account == null){throw new InvalidAccountException("Invalid ACNO :"+acno);
}
return account;
}
private void checkForOverdraft(Account account, BigDecimal amount){if(!account.isOverdraftAllowed()){if((account.getBalance() - amount) < 0) {throw new InsufficientBalanceException("Insufficient Balance");
}
}
else{if(((account.getBalance()+account.getOverdraftLimit()) - amount) < 0){throw new ExceedingOverdraftLimitException("Insufficient Balance, Exceeding Overdraft Limit");
}
}
}
private void checkForDuplicateTransaction(FundTransferTxn txn){AccountTransaction lastTxn = TransactionDAO.getLastTransaction(txn.getSourceAccount().getAcno());
if(lastTxn != null) {if(lastTxn.getTargetAcno().equals(txn.getTargetAccount().getAcno())
&& lastTxn.getAmount() == txn.getAmount()
&& !txn.isAllowDuplicateTxn()) {throw new DuplicateTransactionException("Duplicate transaction exception");
}
}
}
private void makeTransfer(Account source, Account target, BigDecimal amount){sourceAccount.debit(amount);
targetAccount.credit(amount);
TransactionService.saveTransaction(source, target, amount);
}
}
重构之后 transferFunds 的逻辑就很清晰,先是校验账户,再校验是否超额,再校验是否反复转账,最初执行外围的 makeTransfer 逻辑
小结
SLAP 与 Don’t Make Me Think 有殊途同归之妙,遵循 SLAP 的代码通常浏览起来不会太吃力。另外没有循序这个准则的通常是 Leaky Abstraction。
doc
- Clean Code – Single Level Of Abstraction
- Clean Code: Don’t mix different levels of abstractions
- Single Level of Abstraction (SLA)
- The Single Level of Abstraction Principle
- SLAP Your Methods and Don’t Make Me Think!
- Levels of Abstraction
- Maintain a Single Layer of Abstraction at a Time | Object-Oriented Design Principles w/ TypeScript
- 聊一聊 SLAP: 繁多形象层级准则