抽象的精神#

抽象是物件導向的核心原則:以「做什麼」(what)為核心,把「怎麼做」(how)藏起來。當設計使用案例與初始類別時,模型應該能讓人手動、半自動或自動實作;如果一個概念抽象到讀者完全無法想像,就要用原型或範例補上落地畫面。

Jerry Weinberg 對這組指南的觀察:核心精神是不要丟棄資訊。把價格寫成 double 會丟掉「這是錢」的資訊,把多個概念硬塞同一名稱會丟掉「它們其實不同」的資訊。

先求拆,再考慮合(Splitters vs. Lumpers)#

把一個概念當成兩件事,總比把兩件事當成一個概念安全。

  • 同名指兩件事:難以察覺、容易引發誤解。後續想拆開,必須逐處檢視「這個用法是哪一個概念」。
  • 兩名指一件事:可能囉嗦但不致命。後續若想合併,只要做一次全域取代。

Splitters Can Be Lumped More Easily Than Lumpers Can Be Split——把兩個概念合在一起,遠比把已合在一起的概念拆開來得容易。

判斷依據:

  • 試著為一個名稱寫一句話定義;寫不出來就拆。
  • 名稱可主觀,但設計者與客戶必須先達成一致。
  • 確認兩名一義的同義字後,可宣告為同義詞繼續使用。

群聚資料(Clumping)#

把多個彼此相關的屬性聚成一個有名字的概念,是抽象化技術,不是把雜物丟進同一袋。

  • Addressline1citystatezip 等屬性合成一個概念,方便傳遞、減少重複參數。
  • 群聚應該包含行為,而非只有資料;只有資料的類別容易變成「資料污染」型別。
  • 群聚(Clumping)≠ 混為一談(Lumping):前者整合相同概念的多屬性,後者用同名指不同概念。

Clump Data So That There Is Less to Think About——群聚能降低需要同時記住的概念數量。

全力使用抽象資料型別(ADT)#

設計與描述問題時,避免原始型別。每個有語意的數值都能變成 ADT:

原始用法建議型別內含意義
int countCount不可為負,可有上限
double priceDollar內建幣別、千分位、四捨五入規則
int ageAge0–150 之間
int speed_limitSpeedLimit5–80 mph
double elevationElevation0–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」或純數字版本。
  • phoneNumbersocialSecurityNumber:即使格式相似,仍是兩個語意不同的型別,不可混用。
  • 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 從「單筆租借」擴張為「同一客戶當下多筆租借」時,作者選擇建立全新的 MultipleRentalMultipleRentalContractDTO,而不是修改原有的 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意義變了就建立新名稱