1

本文主要研究一下软件开发的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


codecraft
11.9k 声望2k 粉丝

当一个代码的工匠回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧,这样,当他老的时候,可以很自豪告诉世人,我曾经将代码注入生命去打造互联网的浪潮之巅,那是个很疯狂的时代,我在一波波的浪潮上留下...


引用和评论

0 条评论