聊聊軟體開發的SLAP原則

codecraft發表於2022-04-11

本文主要研究一下軟體開發的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

相關文章