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