聊聊软件开发的SLAP原则
序
本文主要研究一下软件开发的SLAP(Single Level of Abstraction Principle)原则 SLAP
SALP即Single Level of Abstraction Principle的缩写,即单一抽象层次原则。 在Robert C. Martin的<>一书中的函数章节有提到:
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
这与 Don"t Make Me Think[1] 有异曲同工之妙,遵循SLAP的代码通常阅读起来不会太费劲。
另外没有循序这个原则的通常是 Leaky Abstraction[2]
要遵循这个原则通常有两个好用的手段便是抽取方法与抽取类。 实例1public 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就很清晰 实例2public 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, ""); }实例3public 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[3] 有异曲同工之妙,遵循SLAP的代码通常阅读起来不会太费劲。另外没有循序这个原则的通常是Leaky Abstraction[4] 。doc
• Clean Code - Single Level Of Abstraction[5]
• Clean Code: Don’t mix different levels of abstractions[6]
• Single Level of Abstraction (SLA)[7]
• The Single Level of Abstraction Principle[8]
• SLAP Your Methods and Don"t Make Me Think![9]
• Levels of Abstraction[10]
• Maintain a Single Layer of Abstraction at a Time | Object-Oriented Design Principles w/ TypeScript[11]
• 聊一聊SLAP:单一抽象层级原则[12]
外部链接
[1] Don"t Make Me Think https://book.douban.com/subject/1440223/
[2] Leaky Abstraction https://khalilstemmler.com/wiki/leaky-abstraction/
[3] Don"t Make Me Think https://book.douban.com/subject/1440223/
[4] Leaky Abstraction https://khalilstemmler.com/wiki/leaky-abstraction/
[5] Clean Code - Single Level Of Abstraction https://www.c-sharpcorner.com/article/clean-code-single-level-of-abstraction/
[6] Clean Code: Don’t mix different levels of abstractions https://www.sivalabs.in/2013/12/clean-code-dont-mix-different-levels-of-abstractions/
[7] Single Level of Abstraction (SLA) http://principles-wiki.net/principles:single_level_of_abstraction
[8] The Single Level of Abstraction Principle https://dzone.com/articles/the-single-level-of-abstraction-principle
[9] SLAP Your Methods and Don"t Make Me Think! https://dzone.com/articles/slap-your-methods-and-dont-make-me-think
[10] Levels of Abstraction https://dzone.com/articles/levels-of-abstraction
[11] Maintain a Single Layer of Abstraction at a Time | Object-Oriented Design Principles w/ TypeScript https://khalilstemmler.com/articles/oop-design-principles/maintain-a-single-layer-of-abstraction/
[12] 聊一聊SLAP:单一抽象层级原则 https://droidyue.com/blog/2019/03/17/slap-single-level-of-abstraction-principle/