從一個例項再談 SRP (單一職責原則)
單一職責原則是面向物件程式設計六大設計原則SOLID的第一大原則,那麼它到底講的是什麼意思呢?我們先看定義:a class should have only a single responsibility;也可解釋為One class should be changed for only one purpose。前面那句看起來不難理解啊?上次聽了參加一個SOLID專項培訓,聽得雲裡霧裡,一直思考不出核心在哪?今天從一個例子去探討SRP思想在程式碼中的具體表現。
question:
簡單說明一個問題:輸入一個郵編或者條形碼,校驗是否正確?並轉換成條形碼或郵編輸出。
我首先想到的是流程是:輸入->校驗->轉換->輸出
時序圖:
看上去感覺也還行,那現在就開始寫程式碼了。
新建一個類Validator:
public class Validatior { private static final String ZIP_CODE_PATTERN = "\\d{5}(_?\\d{4})?"; private static final String PRE_AND_END_FIX = "|"; private static final int NINE_AND_TEN_BIT_ZIPCODE_BARCODE = 52; private static final int FIVE_BIT_ZIPCODE_BARCODE = 32; public static boolean validateCode(String code) { if (isZipCode(code) || isBarCode(code)) return true; return false; } static boolean isZipCode(String code) { if (code.matches(Validatior.ZIP_CODE_PATTERN)) return true; return false; } static boolean isBarCode(String code) { if(code.length()==Validatior.FIVE_BIT_ZIPCODE_BARCODE || code.length()==Validatior.NINE_AND_TEN_BIT_ZIPCODE_BARCODE) if(code.startsWith(Validatior.PRE_AND_END_FIX) && code.endsWith(Validatior.PRE_AND_END_FIX)) if() return true; return false; } }
當我寫到這裡的時候發現不對勁了,怎麼不對勁了呢?我這個if條件準備去判斷條形碼是否能轉換成郵編?那直接去建立一個類Converter去試著轉換一下不就可以了嗎?反正校驗完還不是要轉換的?問題就來了,轉換這件事做了兩次,而且本來我們想分離校驗器和轉換器,這樣子不就相當於耦合在一起了嗎?這明顯違背了單一職責原則。校驗器做了兩件事情,而它本應該只做校驗這件事。原因處在哪兒呢?--我想了一下問題出在這,所謂的校驗器和轉換器是我空想出來的,現實根本據不存在兩個東西。
分析:哪裡違背了單一職責原則?
1.雖然這是一個校驗器,它包含了郵編和條形碼校驗的實現;而郵編的校驗和條形碼的校驗可能會產生變化;
2.當不管是郵編還是條形碼本身結構發生改變(兩者改變不同步),校驗器的實現都會發生改變;
3.同理轉換器也是存在和校驗器一樣的問題;
4.在clean code的bad shell 中說到,這是屬於Shotgun Surgery(散彈式改動);一個change會引發多個class的改動。當然我們也可以把每一個實現都移動到另一個地方,得到耦合的分離,但先不這麼做。 換個思路,現實存在的是條形碼和郵編。根據這個思路,我重新畫出時序圖。
差不多就這樣的順序了,郵編的分別進行校驗和轉換。我剛建立一個ZipCode(郵編)的類,又覺得郵編和條形碼不是相互獨立的關係,相互之間都能唯一確定。於是我改為建立一個Code類去處理所有的事情。
public class Code {
public static boolean isZipCode(String zipCode) {}
public static boolean isBarpCode(String BarCode) {}
public static String convertZipCodeToBarCode(String zipCode) {}
public static String convertBarCodeToZipCode(String BarCode) {}
}
剛開始實現就覺得這很荒謬,Code一個類把所有的事都幹了。這就是過大類的問題,裡面包含了郵編,條形碼,還有郵編條形碼的對映關係。這三方不管任意一方有變化,Code類都會變化?單一職責原則的又一解釋:One class should be changed for only one purpose。這太明顯地違反了原則,這樣子寫程式碼會導致後期維護相當困難。
我又按照上面的時序圖,把物件分為下列幾個類:
CodeRelationShip:
public class CodeRelationShip { static final HashMap<Integer, String> CODE_MAP = new HashMap<>(); static { CodeMap.CODE_MAP.put(0, "||:::"); CodeMap.CODE_MAP.put(1, ":::||"); ... } ... }
BarCode:
public class BarCode {
public static boolean isBarpCode(String BarCode) {...}
public static String convertBarCodeToZipCode(String BarCode) {...}
}
ZipCode:
public class ZipCode {
public static boolean isZipCode(String zipCode) {...}
public static String convertZipCodeToBarCode(String zipCode) {...}
}
CodeConvertor
public class CodeConvertor {
public String ShowCodeResult(String input) throws Exception {
if(ZipCode.isZipCode(input)) {
return ZipCode.convertZipCodeToBarCode(input);
}
if(BarCode.isBarCode(input)) {
return BarCode.convertBarCodeToZipCode(input);
}
throw new Exception("Input Error!");
}
}
我感覺已經大功告成了,邏輯也沒問題,抽象得感覺也挺好。每個類做的事情又那麼地簡單明瞭,感覺自己已經充分地實現了單一職責。如果我沒發現下面的問題,我還是想不到要寫這篇文章的。
接下來是比較關鍵的地方了!
----------------------------------------------------------------------------------------------------------------
我又開始實現相關功能:
ZipCode:
public class ZipCode {
private static final String ZIP_CODE_PATTERN = "\\d{5}(_?\\d{4})?";
public static boolean isZipCode(String zipCode) {
if (zipCode.matches(ZIP_CODE_PATTERN))
return true;
return false;
}
public static String convertZipCodeToBarCode(String zipCode) {
if(isZipCode(zipCode)) {
StringBuffer barCode = new StringBuffer("|"); //"|"是條形碼的前後綴
Integer total = 0;
Integer value = 0;
for (int i = 0; i < zipCode.length(); i++) {
value = Integer.valueOf(zipCode.substring(i, i+1));
total += value;
barCode.append(CodeMap.CODE_MAP.get(value));
}
barCode.append("|");
}
當我寫到這裡的時候我又感覺有那麼一點變扭, 變扭的地方在哪呢? “|”是條形碼的字首,而在郵編的類裡面卻出現了它的身影,這屬於什麼,在耦合的分類裡面這屬於最高層次的耦合,內容耦合,郵編類依賴了條形碼類的內部屬性。在Clean Code裡面這是屬於Divergent Change(發散式變化)。因為郵編類受到本身結構的影響,也受到條碼類結構的影響。這就違反了單一職責原則。那好辦啊,既然郵編和條形碼都會用到“|”,我們把 "|" 移到CodeRelationShip類裡面去維護不就可以了嗎?的確這樣子做降低了耦合,只是降到了全域性耦合的程度。但事實是,郵編類本不需要這個屬性,甚至在郵編類裡面,我都不想看到“|”一絲的身影。
我的解決辦法是:
BarCode:
public class BarCode {
public static boolean isBarpCode(String BarCode) {...}
public static BarCode convertZipCodeToBarCode(ZipCode zipCode) {...} //原本在ZipCode裡面的方法
}
ZipCode:
public class ZipCode {
public static boolean isZipCode(String zipCode) {...}
public static ZipCode convertBarCodeToZipCode(BarCode barcode) {...} //原本在BarCode裡面的方法
}
為什麼這麼處理?實際上這個處理很符合現實邏輯,郵編轉條形碼,本來返回格式就是應該由條形碼自身決定,而不是由郵編類決定。反之也是一樣的。參考案例:Sting 轉 Int : 方法不在String裡面,而是Interger.valueOf(String s);Int 轉 String :方法是String.valueOf(int i)。有人又要問了,當然我自己也問自己。 int 轉 String 還有一個方法 Integer.toString(); 我認為這是可以理解的,因為全JAVA的類都有一個toString()方法。String 類的地位已經屬於基本資料型別的地位了,全世界都公認了它的格式。那麼Integer 有個toString()方法就很容易理解了。
如果你確定郵編類的的結構永遠不會更改,條形碼還有一種實現:
BarCode:
public class BarCode {
public static boolean isBarpCode(String BarCode) {...}
public static BarCode convertZipCodeToBarCode(ZipCode zipCode) {...} //原本在ZipCode裡面的方法
public static ZipCode toZipCode(BarCode barCode){...}
}
具體怎麼實現,得看業務的情況,一切脫離相關業務的架構與實現都是畫蛇添足, 甚至是系統的累贅。
最後寫出的程式碼應該是:
- 每當郵編有變化,只需要改動郵編的一個地方;
- 每當條形碼有變化,只需改動條形碼的一個地方;
- 每當他們的對映關係有變化,只需要改動對映關係類的一個地方。
- 而郵編與條形碼則應該相互獨立,儘量低耦合。
結論:單一職責原則本身是設計原則最重要的一個原則,它就是軟體的可拓展性,可維護性的基礎。它本身要求類足夠小,但類本身也需要有相關業務含義(或者叫現實含義)。表現現形式就是,一個變化儘量對應一個 Class的改變。一個Class的行為儘量只對應一種變化。為什麼叫一種變化,不叫一個變化呢?以後有了例項再說。
第一次寫部落格,歡迎意見,歡迎討論。
我貼出BarCode類,也是這幾個類中最複雜的一個類的一個實現:
BarCode:
/**
* @author wolf-J
*/
public class BarCode {
private static final String PRE_FIX = "|";
private static final String POST_FIX = "|";
private static final int BARCODE_MAP_ZIP_NUMBER = 5;
private String value;
public BarCode(String value) throws Throwable {
if (isBarCode(value))
this.value = value;
else
throw new Exception("Please input a right BarCode!");
}
public static boolean isBarCode(String code) throws Throwable {
if (code.startsWith(PRE_FIX) && code.endsWith(POST_FIX)) {
if (validateBodyNumberLength(code) && validateKey(code))
return true;
}
return false;
}
public static BarCode convertZipCodeToBarCode(ZipCode zipCode) throws Throwable {
StringBuilder barCodeValue = new StringBuilder();
barCodeValue.append(BarCode.PRE_FIX);
barCodeValue.append(convertToBody(zipCode));
barCodeValue.append(BarCode.POST_FIX);
return new BarCode(barCodeValue.toString());
}
private static String convertToBody(ZipCode zipCode) {
String bodyNumber = zipCode.getValueNumber();
StringBuilder body = new StringBuilder();
Integer total = 0;
for (int i = 0; i < bodyNumber.length(); i++) {
String singleNumber = Character.toString(bodyNumber.charAt(i));
body.append(CodeRelationShip.getValuefromCodeMap(singleNumber));
total += Integer.valueOf(singleNumber);
}
String validationKey = CodeRelationShip.getValuefromCodeMap(Integer.toString(Math.abs(10 - total % 10)));
return body.toString() + validationKey;
}
private static boolean validateBodyNumberLength(String code) throws Throwable {
String bodyNumber = convertNumbers(getBody(code));
return CodeRelationShip.ZIPCODE_NUMBER_RANGE.contains(bodyNumber.length());
}
private static boolean validateKey(String code) throws Throwable {
String bodyNumber = convertNumbers(getBody(code));
String keyKumber = convertNumbers(getValidationKey(code));
Integer bodyTotal = 0;
for (char c : bodyNumber.toCharArray()) {
bodyTotal += Integer.valueOf(Character.toString(c));
}
return ((Integer.valueOf(keyKumber) + bodyTotal) % 10 == 0) ? true : false;
}
private static String getBody(String code) {
return code.substring(code.indexOf(PRE_FIX) + 1, code.lastIndexOf(POST_FIX) - BarCode.BARCODE_MAP_ZIP_NUMBER);
}
private String getBodyNumber(String barcodeValue) throws Throwable {
return convertNumbers(getBody(barcodeValue));
}
private static String getValidationKey(String barCodeValue) {
return barCodeValue.substring(barCodeValue.lastIndexOf(PRE_FIX) - BarCode.BARCODE_MAP_ZIP_NUMBER,
barCodeValue.lastIndexOf(POST_FIX));
}
private static String convertNumbers(String body) throws Throwable {
StringBuilder numbers = new StringBuilder();
for (int i = 0; i < body.length() / BARCODE_MAP_ZIP_NUMBER; i++) {
String singleValueNumber = CodeRelationShip
.getKeyfromCodeMap(body.substring(i * BARCODE_MAP_ZIP_NUMBER, (i + 1) * BARCODE_MAP_ZIP_NUMBER));
if (singleValueNumber == null || singleValueNumber.isEmpty())
throw new Exception("convert fail!");
numbers.append(singleValueNumber);
}
return numbers.toString();
}
public String getBodyNumber() throws Throwable {
return getBodyNumber(value);
}
public String getValue() {
return value;
}
}