Kent Beck Implementation Pattern Principles(3)-Symmetry
這禮拜要介紹的Principle: Symmetry(對稱性),乍聽之下有點抽象,但在深入理解後,Symmetry將會幫助我們有效的撰寫程式。
我們先來看看Kent Beck的定義:
Symmetry in code is where the same idea is expressed the same way everywhere it appears in the code.
如果把Symmetry放到程式的世界,則是在任何地方相同的想法都以相同的方式描述。
例如所有物件的Field都擁有相同的Life time、一個集群的函數都接收相同的參數。
為什麼要Symmetry呢?因為當程式碼能夠以Symmetry的方式呈現時,只要看部分的程式碼,即可快速的理解另一部分,如此一來可讀性便會大大提升。透過Symmetry來傳遞Communication的價值。
void process() {
insertKey();
count++;
closeDoor();
}
上述這段程式碼取自Kent Beck書中,大家能看出哪邊怪怪的嗎?如果以Symmetry的角度去看這段程式碼,我們會發現count++跟insertKey、closeDoor的抽象程度不一致(count++是實作細節,而insertKey、closeDoor是意圖表達明確的函數)。
既然我們知道這段程式碼違反了Symmetry,在我們修正他之前,我們先來探討這樣的程式碼有哪些缺點:
- 可讀性:抽象程度不一樣,不好理解,抽象程度越是一致可讀性越高,可以讓我們在不需理解瑣碎的實作細節情況下,了解程式要解決的問題
- 擴充性:因為不好理解,通常會導致後續接手的人直接Hard Code功能(如果程式很Clean,通常會讓人比較想好好修改他,因為看得懂且改得動),容易產生更多Bug
- 彈性:沒有適當的抽象化,會降低彈性,因為抽象化的好處就是能讓我們找到共同行為,然後透過Polymorphism的方式去減少判斷邏輯,讓系統容易擴充
void process() {
insertKey();
openDoor();
closeDoor();
}
經過修改後的程式碼,我們可以明確的看出,count++其實是在做開門的動作,透過簡單的extract method,可以讓程式碼的可讀性大大的提升,基本上一秒就能理解process在解決什麼問題。
Symmetry可以有效地幫助我們達到SRP,為什麼這樣說呢?因為當我們把所有的Method都以這樣的形式表達時,會發現Method數量越來越多(High Level、Medium Level、Low Level),這時就是給我們一種提示這個物件可能違反SRP,沒有妥善的劃分責任到適合的物件。
假設我們要把Student的資訊,寫入檔案:
public void save(Student student) {
StudentDTO dto = StudentDTOConverter.fromDomain(student);
String str = "";
try {
ObjectMapper Obj = new ObjectMapper();
str = Obj.writeValueAsString(dto);
}
catch (IOException e) {
throw new RuntimeException(e);
}
byte[] strToBytes = (str + "\n").getBytes();
try {
Files.write(Paths.get("./src/main/resources/dto.txt"),
strToBytes,
StandardOpenOption.APPEND);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
上述的程式碼,明顯有幾點違反了Symmetry Principle
- StudentDTOConverter的抽象程度(High Level Intention)跟其他部分的抽象程度不一致(Implementation Detail)
- 不能看到程式碼所要表達的意圖(Intention Revealing)
因此我們做了幾點修改
- 針對兩個try-catch區塊做Extract method,並且給他們Intention Revealing Name
修改後如下:
public void save(Student student) {
StudentDTO dto = StudentDTOConverter.fromDomain(student);
String jsonStr = toJson(dto);
write(jsonStr);
}
我們可以看到,修改後不必要的細節都去除,只剩下程式碼High Level的操作邏輯。
但是這樣會導致一個問題,這個Class明顯違反SRP
- 把Student儲存的責任
- 把StudentDTO轉換成Json格式的責任
- 把檔案寫進去特定路徑的責任
因此我們針對這兩個Method使用Extract Class,讓責任好好的被分配到適當的物件當中
public void save(Student student) {
StudentDTO dto = StudentDTOConverter.fromDomain(student);
FileUtility.write(JsonUtility.toJson(dto));
}public class JsonUtility {
public static String toJson(Object o) {
try {
ObjectMapper Obj = new ObjectMapper();
String jsonStr = Obj.writeValueAsString(o);
return jsonStr;
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
}public class FileUtility {
private static Path path = Paths.get("./src/main/resources/dto.txt");
public static void write(String jsonStr) {
byte[] strToBytes = (jsonStr + "\n").getBytes();
try {
Files.write(path, strToBytes, StandardOpenOption.APPEND); //Append mode
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
有人可能會問說為什麼要為了兩個Method抽了兩個物件,是不是有點過頭了?但是轉換Json格式和儲存不只有Save Student的Class需要可能還有其他Class需要,因此變成Utility Class增加了程式碼Reuse的程度。
從SRP的角度我們可以知道轉換Json Format和File Store是Different rate of change,因此需要抽離這兩個責任。
結論:Symmetry透過讓任何部分的程式碼抽象程度都維持一致(HIgh、Medium、Low),透過適當的抽象可以讓我們察覺物件責任分配不均(違反SRP)。