兩個常一起出現的壞味道#
這一章談兩個常一起出現、互為因果的程式碼異味(Code Smell):原始型別偏執(Primitive Obsession)與資料群(Data Clump)。
Primitive Obsession#
物件導向程式喜歡把相關的值與行為「封裝」在物件裡,透過介面與外界互動。但現代語言提供的基本型別(int、long、double 等)沒有物理意義,沒有商業邏輯,也沒有行為。
當需要表達清楚的物理意義時,過度依賴基本型別會使程式失去表達力:
- 用
long表示時間點:準確可靠,但失去「時間」的概念 - 用兩個
double表示經緯度:能描述位置,但兩個數字老是被迫綁在一起
好的命名能稍微緩解,但無法取代值物件(Value Object)能提供的行為與商業邏輯。
Data Clump#
資料群(Data Clump)來自 Refactoring 書中的描述,意思是「就像小孩子,喜歡成群結隊地待在一塊兒」。例如:
- 市內電話與區域號碼
- 平面座標的 X 與 Y
- API 網址的 domain name 與 port
- 經度與緯度
它們的共通特性是「老是一起出現」。一旦某個方法的介面拿它們當參數,使用者就被迫一再重複寫類似的程式碼。
難兄難弟#
這兩個壞味道常互為因果:
- 經緯度本身是基本型別 ➡️ Primitive Obsession
- 為了表達「位置」這個概念,兩個數字被迫一起出現 ➡️ Data Clump
- 在物件之間傳遞「位置」時,又佔掉兩個參數位置
Uncle Bob 在 Clean Code 中曾建議方法參數最理想是 0 個,最多不要超過 3 個。一組經緯度一出現就佔掉兩個參數位置。
解決之道#
當多個基本型別老是一起出現代表一個商業概念,就把它們封裝成新的物件。如果這個物件出現在介面上,稱為 Parameter Object。
範例:線上簽到的距離檢查#
學校要做線上簽到功能,需要驗證學生位置是否離教室夠近:
public class DistanceChecker {
public boolean checkDistance(long courseId, double longitude, double latitude) {
Course course = courseRepository.find(courseId);
ClassRoom classRoom = course.getClassRoom();
double classRoomLongitude = classRoom.getLongitude();
double classRoomLatitude = classRoom.getLatitude();
double distance = distanceCalculator.calculate(
longitude, latitude, classRoomLongitude, classRoomLatitude);
return distance < 50D;
}
}可讀性不佳,介面充斥基本型別。先補上測試:
class DistanceCheckerTest {
@Test
void closed_enough() {
DistanceChecker distanceChecker = new DistanceChecker(
dummy_repository(9527L, 0D, 0D),
dummy_calculator(1D, 1D, 0D, 0D, 49D));
assertTrue(distanceChecker.checkDistance(9527L, 1D, 1D));
}
@Test
void too_far() {
DistanceChecker distanceChecker = new DistanceChecker(
dummy_repository(9527L, 0D, 0D),
dummy_calculator(99D, 99D, 0D, 0D, 51D));
assertFalse(distanceChecker.checkDistance(9527L, 99D, 99D));
}
}測試很煩,數字一堆。這是好事,因為剛寫完就發現難用,是重構的最佳時機。
提取參數物件(Extract Parameter Object)#
把經緯度封裝成 Position:
public class DistanceChecker {
public boolean checkDistance(long courseId, Position studentPosition) {
Position classroomPosition = courseRepository.find(courseId)
.getClassRoom()
.getPosition();
double distance = distanceCalculator.calculate(studentPosition, classroomPosition);
return distance < 50D;
}
}抽取請務必使用 IDE 提供的重構功能,節省時間並避免出錯。
還會帶出其他壞味道#
消除 Data Clump 與 Primitive Obsession 後,介面表達力立刻變好。但短短幾行還能找到其他味道:
- Message Chains:取得
classroomPosition時連續呼叫多層 getter,違反迪米特法則 - Feature Envy:
distanceCalculator從頭到尾只在跟Position互動,距離應該叫Position自己算
壞味道是改不完的。找個舒服的平衡點即可,重要的是有測試保護,何時想再回頭改都可以。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10263319