什麼是 Feature Envy#

Feature Envy(依戀情結)是 Martin Fowler 在 Refactoring 中明列的程式碼異味(Code Smell)之一。Fowler 的定義是:

  • 一個函式對於某個 class 的興趣,高過於對自身 host class 的興趣

換句話說,這個函式整天「跟隔壁老王 line 來 line 去」,自己家的事卻不太關心。Fowler 指出這種迷戀最常發生在「資料」上。

當 A 類的某個方法老是去存取 B 類的資料來運算,會造成兩個問題:

  • B 的細節一旦改變,A 就被迫跟著改
  • B 想調整自己的資料存取方式時,必須遷就 A

最終結果是 A 與 B 緊密耦合,違反我們希望「總是一起變化的東西放在一塊兒」的原則。

解決之道#

既然 A 的方法對 B 的 feature 異常感興趣,乾脆讓它們在一起:把這個方法搬到 B 身上。

範例:碩士獎學金計算#

延續上一章的範例,看看 MasterScholarshipCalculator.calculate

@Override
public int calculate(Transcript transcript) {

    List courses = transcript.getCourses();

    if (courses.isEmpty()) return 0;

    double totalCredit = 0.001D;
    double totalWeightedScore = 0D;

    for (Course course : courses) {
        totalCredit += course.getCredit();
        totalWeightedScore += course.getScore() * course.getCredit();
    }

    double weightedAverage = totalWeightedScore / totalCredit;

    if (weightedAverage >= 90D) {
        return 15_000;
    } else if (weightedAverage >= 80D) {
        return 7_500;
    } else {
        return 0;
    }
}

這個方法絕大部分篇幅都在跟 Transcript 的資料對話:取出 courses、走訪 course 的 credit 與 score。對自己身上的邏輯只有最後幾個門檻判斷。明顯的 Feature Envy。

第一步:提取方法(Extract Method)#

對付 Feature Envy 的起手式是提取方法(Extract Method)。先把與外部資料糾纏的段落隔離出來,讓高階業務邏輯浮現:

@Override
public int calculate(Transcript transcript) {

    List courses = transcript.getCourses();

    if (hasNoCourses(courses)) return 0;

    double weightedAverage = calculateWeightedAverage(courses);

    if (weightedAverage >= 90D) {
        return 15_000;
    } else if (weightedAverage >= 80D) {
        return 7_500;
    } else {
        return 0;
    }
}

private double calculateWeightedAverage(List courses) {
    double totalCredit = 0.001D;
    double totalWeightedScore = 0D;

    for (Course course : courses) {
        totalCredit += course.getCredit();
        totalWeightedScore += course.getScore() * course.getCredit();
    }

    return totalWeightedScore / totalCredit;
}

private boolean hasNoCourses(List courses) {
    return courses.isEmpty();
}

提取方法請使用 IDE 的重構功能,事後跑一次測試確認沒壞。

抽完後,Feature Envy 的味道更明顯了:兩個被提取的方法從頭到尾都在跟 Transcript 身上的 courses 互動。

第二步:搬移方法(Move Method)#

成全它們,把方法搬到 Transcript 上:

@Override
public int calculate(Transcript transcript) {

    if (transcript.hasNoCourses()) return 0;

    double weightedAverage = transcript.calculateWeightedAverage();

    if (weightedAverage >= 90D) {
        return 15_000;
    } else if (weightedAverage >= 80D) {
        return 7_500;
    } else {
        return 0;
    }
}

現在 Transcript 可以對外提供完整的高階行為,Service 只需要跟它要結果,不必再碰資料細節。耦合度大幅下降。

重構是漸進的#

眼尖會發現這裡還留著 If 這個壞味道,沒關係。重構不是一次做完,而是一步一步漸進進行,有測試保護就不怕。

重構是行為的搬遷,不是大爆炸式的改寫。每一小步都跑測試,每一小步都讓程式更接近合理形狀。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10262774