1. 程式人生 > >從一個例項再談 SRP (單一職責原則)

從一個例項再談 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){...}
}

具體怎麼實現,得看業務的情況,一切脫離相關業務的架構與實現都是畫蛇添足, 甚至是系統的累贅。

最後寫出的程式碼應該是:

  1. 每當郵編有變化,只需要改動郵編的一個地方;
  2. 每當條形碼有變化,只需改動條形碼的一個地方;
  3. 每當他們的對映關係有變化,只需要改動對映關係類的一個地方。
  4. 而郵編與條形碼則應該相互獨立,儘量低耦合。

結論:單一職責原則本身是設計原則最重要的一個原則,它就是軟體的可拓展性,可維護性的基礎。它本身要求類足夠小,但類本身也需要有相關業務含義(或者叫現實含義)。表現現形式就是,一個變化儘量對應一個 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;
	}

}