序
本文主要研究一下軟體開發的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:單一抽象層級原則