1

sequence

This article mainly studies the SLAP (Single Level of Abstraction Principle) principle of software development

SLAP

SALP is the abbreviation of Single Level of Abstraction Principle, which is the single abstraction level principle.
In the function chapter of Robert C. Martin's book <<Clean Code>> it is mentioned:

To ensure that a function does only one thing, the statements in the function must all be at the same level of abstraction. The jumble of different levels of abstraction in functions can often be confusing. The reader may not be able to tell whether an expression is a basic concept or a detail. Worse yet, like a broken window, once the details get mixed up with the underlying concepts, more details get tangled up in the function.

This is similar to Don't Make Me Think , where SLAP-compliant code is usually not too strenuous to read.

In addition, there is usually Leaky Abstraction without this principle.

To follow this principle, there are usually two useful means to extract methods and extract classes.

Example 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;
}
This code contains two levels of abstraction, one is to convert resultSet to List<ResultDto> in a loop, the other is to convert ResultEntity to ResultDto

The logic of converting ResultDto can be further extracted into a new method

 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;
}
After this refactoring, the buildResult is very clear

Example 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);
        }
    }
The assembly logic of the url here is not at the same level as other methods, and the reconstruction is as follows
 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, "");
}

Example 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
        }
    }
}    
}
This code leaks the dao logic into the service, and the verification logic is also coupled with the core business logic, which seems a bit laborious. It is reconstructed according to the SLAP principle as follows
 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);
    }    
}
After the refactoring, the logic of transferFunds is very clear. First, check the account, then check whether there is an excess, then check whether the transfer is repeated, and finally execute the core makeTransfer logic

summary

SLAP is similar to Don't Make Me Think , and code that follows SLAP is usually not too hard to read. In addition, there is usually Leaky Abstraction without this principle.

doc


codecraft
11.9k 声望2k 粉丝

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


引用和评论

0 条评论