聊聊软件开发的SLAP原则

本文主要研究一下软件开发的SLAP(Single Level of Abstraction Principle)原则

SLAP

SALP即Single Level of Abstraction Principle的缩写,即单一抽象层次原则。 在Robert C. Martin的<>一书中的函数章节有提到:

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。

这与 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

猜你喜欢

转载自juejin.im/post/7082951399058702343