乐趣区

关于arch:聊聊软件开发的SLAP原则

本文次要钻研一下软件开发的 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: 繁多形象层级准则
退出移动版