聊一聊SLAP:單一抽象層級原則

  • 2020 年 2 月 10 日
  • 筆記

作為程式設計師,我們總是和方法打交道,不知不覺都會接觸Long method(方法體較長的方法),不論是自己寫的還是他人寫的,而Long method(長方法)往往是問題的體現,代表著程式碼有一種壞的味道,也意味著需要對這段程式碼進行重構處理。

長方法的問題通常表現在

  • 可讀性很差
  • 復用性差
  • 難以調試
  • 難以維護
  • 冗餘程式碼多

既然長方法不好,那麼我們就應該寫short method(短方法),但是什麼樣的方法才算短方法呢,有什麼衡量呢?

行數限定

首先我們想到的可能是限制方法的行數,是的,有人說是20行為宜,有人說是10行最佳,眾說紛紜,無一定論。

但是行數限定也有問題

  • 沒有具體的行數限定
  • 行數限定如果執行,可能會比較死板

顯然除了行數之外,我們需要一個更加明確無爭議的避免長方法產生的方法,比如今天我們提到的 SLAP(單一抽象層原則)。

定義

SLAP 是 Single Level of Abstraction 的縮寫。

關於SLAP的一些具體解釋

指定程式碼塊的程式碼應該在單一的抽象層上。

其實關於定義最難理解的應該是抽象層,其原因可能在於

  • 我們接受著各種非黑即白,非善既惡的教育和熏陶
  • 對事物做抽象化,不是一下子達到另一個極端的抽象描述。
  • 抽象可以是循序漸進,分層的。

舉一個最簡單的例子,在中學時期我們學習英語,大概聽過一個這樣類似的短句」美小圓舊黃法國木書房」,這是為了輔助在英語中快速排列定語順序的記憶技巧總結。

在英語(或其他語言)中

  • 對名詞主體增加定語(名詞,形容詞)修飾,使得主體更加具體
  • 反之對主體刪除定語(名詞,形容詞),會使得主體更加抽象

比如我們對「美小圓舊黃法國木書房」 逐步刪除定語,大致會產生這樣的抽象層

  1. 美小圓法國木書房
  2. 舊黃法國木書房
  3. 法國木書房
  4. 法國書房
  5. 書房

我們回歸編碼,來看一個例子

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