什麼是 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