我們在前面的兩篇文章中,各自提到了程式的『波動拳』與『大量參數』兩種降低可讀性的程式寫法。然而,大部分時候,『可讀性』並不是最嚴重的問題,他只是不高明而已,『耦合度』才是比較嚴重的問題。兩隻程式間,或是兩個模組間彼此依賴程度高,會導致整體可擴張性降低。如同我在本系列的文章中一再強調的:
『高耦合』在你的產品還很小時看不出傷害,等你越長越大,才知道要痛,那時已經很難醫了。
今天我打算再舉一個例子來看看程式壞味道的影響,並試著解決他。先前我們看到的波動拳是有礙閱讀的,那會讓你容易改錯邏輯,而大量參數則是使你容易因為給錯參數而影響結果。今天要看的是不太影響閱讀,但是會提高耦合度的程式壞味道:Feature Envy。
一樣,我們從需求開始。
我有一本電話簿,裡面有一些聯絡人,聯絡人存著姓名、email,與電話。每當我需要時,就要得到特定格式的『聯絡人清單』。
需求很清楚,於是事不宜遲,我們來速速做完他:
// Contact.java
public class Contact {
private String name;
private String email;
private String phoneNumber;
public Contact(String name, String email, String phoneNumber) {
this.name = name;
this.email = email;
this.phoneNumber = phoneNumber;
}
// ...後略
}
// PhoneBook.java
public class PhoneBook {
List<Contact> contacts;
public PhoneBook() {
this.contacts = new ArrayList<>();
}
public String generateFormattedPrint(){
String result = "";
for (Contact contact : contacts){
result += contact.getName() + ": ";
result += contact.getEmail() + " | ";
result += contact.getPhoneNumber() + ". ";
result += "\n";
}
return result;
}
// ...後略
}
這樣很單純吧?我只要呼叫generateFormattedPrint(),就能得到一個字串,上面有我所有聯絡人的資訊,而且還是格式化過的,長得就像這樣:
負責任如我,當然要附上測試嚕:
public class PhoneBookTest {
@Test
public void testGenerateFormattedPrint() throws Exception {
// Prepare data
PhoneBook phoneBook = new PhoneBook();
phoneBook.addContact(new Contact("Kuma", "kuma@aaa.bbb", "123456789"));
phoneBook.addContact(new Contact("Maku", "maku@ccc.ddd", "987654321"));
// Execute method
String result = phoneBook.generateFormattedPrint();
// Check result
String expected = "Kuma: kuma@aaa.bbb | 123456789. \nMaku: maku@ccc.ddd | 987654321. \n";
assertEquals(expected, result);
System.out.println(result);
}
}
跑一下測試:
過了,酷!打完收工。
...回來!接著下來才是重頭戲!
這裡就出現我們常見到的Feature Envy壞味道了。雖然generateFormattedPrint()可以順利完成交辦任務:印出格式化過的通訊錄,但是在這裏面大量地使用了Contact類別的method。事實上,它裡面使用的method全部都是Contact提供的。這就是標準的Feature Envy了。這件事情嚴重地違反了物件導向的『單一職責原則』。缺點是,聯絡人的內容與輸出格式一旦有換,我要同時更動聯絡人與電話簿。這就怪啦!聯絡人的內容更動,關電話簿什麼事?
對啊!耦合度高的程式,最大的缺點就是你會常常做一些『甘我____事』的事情啊。
怎麼改?很簡單,既然每行的內容全部都是Contact提供的,那我為什麼不讓Contact自己幫我組格式就好?電話簿只要負責在適當的地方斷行就可以了。
於是我改成這個樣子:
// Contact.java
public class Contact {
// ...前略
public String generateFormattedPrint(){
String result = name + ": ";
result += email + " | ";
result += phoneNumber + ". ";
return result;
}
}
// PhoneBook.java
public class PhoneBook {
List<Contact> contacts;
public PhoneBook() {
this.contacts = new ArrayList<>();
}
public String generateFormattedPrint(){
String result = "";
for (Contact contact : contacts){
result += contact.generateFormattedPrint();
result += "\n";
}
return result;
}
// ...後略
簡單來說,PhoneBook只管行與行中間的間隔,行的內容就交給Contact來管。這樣一來,我如果要改每個聯絡人的內文格式,只要管Contact就可以了。而在TestCase完全不改的情況下,結果一樣是對的。(再一次,證明了測試案例對於重構的重要性。)
於是PhoneBook與Contact之間的高耦合度已經被我們解開,這下真的可以打完收工了。
本文程式範例:https://github.com/bearhsu2/ithelp。