聊聊软件开发的SLAP原则

序 本文主要研究一下软件开发的SLAP(Single Level of Abstraction Principle)原则
SLAP SALP即Single Level of Abstraction Principle的缩写,即单一抽象层次原则。
在Robert C. Martin的<>一书中的函数章节有提到:

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
这与 Don't Make Me Think 有异曲同工之妙,遵循SLAP的代码通常阅读起来不会太费劲。
另外没有循序这个原则的通常是Leaky Abstraction
【聊聊软件开发的SLAP原则】要遵循这个原则通常有两个好用的手段便是抽取方法与抽取类。
实例1
public List buildResult(Set resultSet) { List 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,一个是转换ResultEntity到ResultDto
可以进一步抽取转换ResultDto的逻辑到新的方法中
public List buildResult(Set resultSet) { List 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 = https://www.it610.com/article/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 = https://www.it610.com/article/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:单一抽象层级原则

    推荐阅读