iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 19
0
自我挑戰組

再戰軟體工程系列 第 18

『你儂我儂的程式碼』 -- 談Code Smell 之 Feature Envy

我們在前面的兩篇文章中,各自提到了程式的『波動拳』與『大量參數』兩種降低可讀性的程式寫法。然而,大部分時候,『可讀性』並不是最嚴重的問題,他只是不高明而已,『耦合度』才是比較嚴重的問題。兩隻程式間,或是兩個模組間彼此依賴程度高,會導致整體可擴張性降低。如同我在本系列的文章中一再強調的:

『高耦合』在你的產品還很小時看不出傷害,等你越長越大,才知道要痛,那時已經很難醫了。

今天我打算再舉一個例子來看看程式壞味道的影響,並試著解決他。先前我們看到的波動拳是有礙閱讀的,那會讓你容易改錯邏輯,而大量參數則是使你容易因為給錯參數而影響結果。今天要看的是不太影響閱讀,但是會提高耦合度的程式壞味道: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(),就能得到一個字串,上面有我所有聯絡人的資訊,而且還是格式化過的,長得就像這樣:

https://ithelp.ithome.com.tw/upload/images/20180102/201074290KH0lIQZC9.png

負責任如我,當然要附上測試嚕:

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);

    }
}

跑一下測試:
https://ithelp.ithome.com.tw/upload/images/20180102/20107429kDr1seINCh.png
過了,酷!打完收工。
https://ithelp.ithome.com.tw/upload/images/20180102/2010742968dw2iLwLc.jpg

...回來!接著下來才是重頭戲!

這裡就出現我們常見到的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


上一篇
『化主動為被動』 -- 談IoC與DI/DL
下一篇
『程式都解耦合了,那測試呢?』 -- 談測試解耦合神器:Mock技術
系列文
再戰軟體工程30

尚未有邦友留言

立即登入留言