抽象的精神#
抽象是物件導向的核心原則:以「做什麼」(what)為核心,把「怎麼做」(how)藏起來。當設計使用案例與初始類別時,模型應該能讓人手動、半自動或自動實作;如果一個概念抽象到讀者完全無法想像,就要用原型或範例補上落地畫面。
Jerry Weinberg 對這組指南的觀察:核心精神是不要丟棄資訊。把價格寫成
double會丟掉「這是錢」的資訊,把多個概念硬塞同一名稱會丟掉「它們其實不同」的資訊。
先求拆,再考慮合(Splitters vs. Lumpers)#
把一個概念當成兩件事,總比把兩件事當成一個概念安全。
- 同名指兩件事:難以察覺、容易引發誤解。後續想拆開,必須逐處檢視「這個用法是哪一個概念」。
- 兩名指一件事:可能囉嗦但不致命。後續若想合併,只要做一次全域取代。
Splitters Can Be Lumped More Easily Than Lumpers Can Be Split——把兩個概念合在一起,遠比把已合在一起的概念拆開來得容易。
判斷依據:
- 試著為一個名稱寫一句話定義;寫不出來就拆。
- 名稱可主觀,但設計者與客戶必須先達成一致。
- 確認兩名一義的同義字後,可宣告為同義詞繼續使用。
群聚資料(Clumping)#
把多個彼此相關的屬性聚成一個有名字的概念,是抽象化技術,不是把雜物丟進同一袋。
Address把line1、city、state、zip等屬性合成一個概念,方便傳遞、減少重複參數。- 群聚應該包含行為,而非只有資料;只有資料的類別容易變成「資料污染」型別。
- 群聚(Clumping)≠ 混為一談(Lumping):前者整合相同概念的多屬性,後者用同名指不同概念。
Clump Data So That There Is Less to Think About——群聚能降低需要同時記住的概念數量。
全力使用抽象資料型別(ADT)#
設計與描述問題時,避免原始型別。每個有語意的數值都能變成 ADT:
| 原始用法 | 建議型別 | 內含意義 |
|---|---|---|
int count | Count | 不可為負,可有上限 |
double price | Dollar | 內建幣別、千分位、四捨五入規則 |
int age | Age | 0–150 之間 |
int speed_limit | SpeedLimit | 5–80 mph |
double elevation | Elevation | 0–60,000 ft |
優點:
- 焦點放在「能做什麼」,而非「怎麼表示」。
- 內建驗證與顯示規則。
- 可在 GUI 自動依型別產生對應控件(例如
FileName旁的瀏覽按鈕)。
When You’re Abstract, Be Abstract All the Way——抽象就抽到底,不要用原始型別描述資料。先全部明確型別化,必要時再退回原始型別;反向走會非常痛苦。
Dollar 範例#
class Dollar
{
Dollar multiply_with_rounding(double multiplier);
Dollar add(Dollar another_dollar);
Dollar subtract(Dollar another_dollar);
String to_string();
};實作方式可彈性:
- 支援運算子重載的語言可直接使用
+、-。 - 不支援的語言可改成方法名稱或建立
typedef。 - 即使內部用
double,至少在變數命名上把資訊帶進去(如double price_in_dollars)。
字串不是萬用包(Most Strings Are More Than Just a String)#
宣告 CommonString 為「沒有任何驗證、格式或語意限制」的純字元集合,再依語境產生更精確的型別:
state:限制為合法縮寫的State,可由官方清單驅動。zipcode:使用ZipCode,把表現形式抽象掉,未來可改顯示「12345-6789」或純數字版本。phoneNumber與socialSecurityNumber:即使格式相似,仍是兩個語意不同的型別,不可混用。fileName:禁用\ / : * " ? < > |等字元,可在 GUI 自動加上 browse 按鈕。encodedWebString:標示已做 URL 編碼,避免被惡意未編碼字串繞過驗證。
Most Strings Are More Than Just a String——把
String視為原始型別,屬性應宣告為對應的 ADT。
不要讓魔法常數溜進程式#
任何具語意的數值都應該命名:
Dollar RENTAL_LATE_FEE = 3.00;- 將
3.00寫死在程式中,日後查找時無法分辨它是「逾期費」還是其他無關金額。 - 純粹的初始化值(陣列起始索引 0)通常不需符號名。
- 若值會變動,應放進設定機制(XML、設定檔、資料庫表),程式只透過符號名稱查詢。
Never Let a Constant Slip into Code——所有值都該有符號名稱。
程式間用文字,程式內不用文字#
文字(text)是程式之間最佳的共同語言:
- 不同系統的
double表示可能不同,但文字格式(CSV、XML 等)能跨系統互通。 - 文字可由人類測試者讀取,方便建立資料導向測試。
但程式內部不要繼續用文字流通:
- 字串資料一進入系統就應立刻轉成對應的 ADT 或列舉。
- 若字串與既定型別不符,錯誤會在轉換時就被攔截,而不是擴散到後續處理。
- 即使語言不支援列舉,也可用具有
to_string()/from_string()方法的類別模擬。
To Text or Not to Text——文字屬於程式之間,不屬於程式之內。
用集合替換群組#
在類別模型中區分兩種聚合:
- 群組(Group):純陣列或鏈結串列,例如
RentalEvent[] rental_history。 - 集合(Collection):擁有自己介面與行為的類別,能在整體層級提供操作。
當對群組的操作不只是傳遞或迭代,就把它升格為集合:
class RentalHistory
Count number_of_rentals()
Dollar total_revenue()
Days short_rental()
Days longest_rental()集合分離了「物件的使用」與「物件的儲存」。
CDDiscCollection介面隱藏實作可能用了哪一種儲存(記憶體集合、資料庫、文字檔)。
If It Has Collection Operations, Make It a Collection——集合分離物件使用與儲存,並隱藏聚合操作的實作。
不要更動既有名稱的意思#
當需求增長,常會誘惑你「把現有名稱意義延伸一下就好」。但別人已經依現有意義做出設計,貿然延伸會破壞共識。
例:當 RentalContract 從「單筆租借」擴張為「同一客戶當下多筆租借」時,作者選擇建立全新的 MultipleRental 與 MultipleRentalContractDTO,而不是修改原有的 RentalContract:
class SingleRentalDTO
Timestamp rental_start_time
Timestamp rental_due_time
CommonString cd_release_title
PhysicalId cd_disc_physical_id
class MultipleRentalContractDTO
SingleRentalDTO[] single_rentals
Name customer_name
PhoneNumber customer_day_phone_number
Figure 12-1. Multiple-rental interface
Don’t Change What It Is——意義變了就建立新名稱,不要改寫舊名稱的意義。同樣道理也適用於方法:新增容易、改變既有契約困難。
抽象總結#
| 指南 | 訊息 |
|---|---|
| Splitters Can Be Lumped More Easily Than Lumpers Can Be Split | 先拆比先合更安全 |
| Clump Data So That There Is Less to Think About | 群聚相關屬性,降低概念數 |
| When You’re Abstract, Be Abstract All the Way | 不要用原始型別描述資料 |
| Most Strings Are More Than Just a String | 字串只是底層型別,語意該明說 |
| Never Let a Constant Slip into Code | 給有意義的值符號名稱 |
| To Text or Not to Text | 文字屬於程式間,不屬於程式內 |
| If It Has Collection Operations, Make It a Collection | 群組升級為集合類別 |
| Don’t Change What It Is | 意義變了就建立新名稱 |