聊一聊SLAP:單一抽象層級原則
- 2020 年 2 月 10 日
- 筆記
作為程式設計師,我們總是和方法打交道,不知不覺都會接觸Long method(方法體較長的方法),不論是自己寫的還是他人寫的,而Long method(長方法)往往是問題的體現,代表著程式碼有一種壞的味道,也意味著需要對這段程式碼進行重構處理。
長方法的問題通常表現在
- 可讀性很差
- 復用性差
- 難以調試
- 難以維護
- 冗餘程式碼多
既然長方法不好,那麼我們就應該寫short method(短方法),但是什麼樣的方法才算短方法呢,有什麼衡量呢?
行數限定
首先我們想到的可能是限制方法的行數,是的,有人說是20行為宜,有人說是10行最佳,眾說紛紜,無一定論。
但是行數限定也有問題
- 沒有具體的行數限定
- 行數限定如果執行,可能會比較死板
顯然除了行數之外,我們需要一個更加明確無爭議的避免長方法產生的方法,比如今天我們提到的 SLAP(單一抽象層原則)。
定義
SLAP 是 Single Level of Abstraction 的縮寫。
關於SLAP的一些具體解釋
指定程式碼塊的程式碼應該在單一的抽象層上。
其實關於定義最難理解的應該是抽象層,其原因可能在於
- 我們接受著各種非黑即白,非善既惡的教育和熏陶
- 對事物做抽象化,不是一下子達到另一個極端的抽象描述。
- 抽象可以是循序漸進,分層的。
舉一個最簡單的例子,在中學時期我們學習英語,大概聽過一個這樣類似的短句」美小圓舊黃法國木書房」,這是為了輔助在英語中快速排列定語順序的記憶技巧總結。
在英語(或其他語言)中
- 對名詞主體增加定語(名詞,形容詞)修飾,使得主體更加具體
- 反之對主體刪除定語(名詞,形容詞),會使得主體更加抽象
比如我們對「美小圓舊黃法國木書房」 逐步刪除定語,大致會產生這樣的抽象層
- 美小圓法國木書房
- 舊黃法國木書房
- 法國木書房
- 法國書房
- 書房
- 房
我們回歸編碼,來看一個例子
private boolean validateUser(User user) { //檢測郵箱是否合法 String ePattern = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-][email protected]((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$"; java.util.regex.Pattern p = java.util.regex.Pattern.compile(ePattern); java.util.regex.Matcher m = p.matcher(user.email); if (!m.matches()) { return false; } //檢測密碼是否合法 if (user.password.length() < 8) { return false; } else { for (char c : user.password.toCharArray()) { if (!Character.isLetterOrDigit(c)) { return false; } } } //return true if it goes here. return true; }
上面的程式碼
- validateUser 方法用來校驗用戶的合法性
- 方法體的前6行程式碼做的事情是校驗用戶的email地址是否合法
- 方法體的後幾行的程式碼,用來校驗用戶的密碼是否合法
上面程式碼存在的問題是
- validateUser 方法中暴露了校驗email和密碼的具體實現
- validateUser 應該只關心校驗email和密碼的抽象(第一層抽象),而不是具體實現(第二層抽象)
- 很明顯validateUser 違背了SLAP原則
解決方法
- 將違背SLAP原則的程式碼做提取,形成獨立的方法
所以按照SLAP原則修改之後的程式碼應該類似於
public class UserValidator { public static final String EMAIL_REGULAR_EXPRESSION = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-][email protected]((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$"; public static boolean validateEmail(String email) { Pattern p = Pattern.compile(EMAIL_REGULAR_EXPRESSION); return p.matcher(email).matches(); } public static boolean validatePassword(String password) { if (password.length() < 8) { return false; } else { for (char c : password.toCharArray()) { if (!Character.isLetterOrDigit(c)) { return false; } } } return true; } } private boolean validateUserSLAP(User user) { return UserValidator.validateEmail(user.email) && UserValidator.validatePassword(user.password); }
常見的違背SLAP的程式碼場景和情況
注釋或空行分割的方法體
//注釋1 程式碼片段1 //注釋2 程式碼片段2 //注釋3 //程式碼片段3
上面的程式碼
- 注釋或空行分割的程式碼片段處理相對獨立邏輯,可以抽象成獨立的方法
- 上面的程式碼如果不處理,往往隨著時間的推移,會使得所在的方法膨脹,進而形成上面的長方法
for循環體內部程式碼
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; }
上面for循環體內部的程式碼,處理了將ResultEntity轉化成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; }
回調
除此之外,回調方法也是容易形成長方法的重災區,這一點無需再多舉例。
答疑
應用SLAP 會導致更多的短方法,維護成本更高了吧
首先,必須承認,SLAP應用後,會產生一些短方法,但是關於維護成本提升,這一點還是需要考究的。
因為
- 短方法的提取產生,會使得方法更加具有原子性,職責更加單一,更加的符合Unix的哲學 Do one thing, and do it well。
- 短方法的復用性更強,使得編碼更加便捷
- 短方法可讀性更強,更加便於理解
- 實踐表明,SLAP應用後,維護成本應該是降低的。
所以,不要畏懼,短方法的產生,應該是喜歡上短方法。
SLAP 的縮寫
SLAP是Single Level of Abstraction的縮寫,不是Same Level of Abstraction,?
References
- https://dzone.com/articles/slap-your-methods-and-dont-make-me-think
- http://principles-wiki.net/principles:single_level_of_abstraction