尤川豪   ·  1週前
298 貼文  ·  215 留言

OOP 程式開發:用一個簡單的數學公式來幫忙設計類別

我一直覺得 OOP 很困難、設計類別很困難。

需要根據經驗、遵守一些原則才能寫出比較好的程式碼。

產出的程式碼也只有其他 senior 看得懂為什麼優質,junior 連分辨好壞都不容易。

也就是大家只能依靠質化分析,用文字描述為什麼好、為什麼壞。

我一直想找一個比較量化分析的方式,在工作時輔助設計類別。

於是我設計了一個簡單的數學公式,今天跟大家分享一下。

內聚係數 (Cohesion Ratio)

要替 OOP 類別打分數聽起來有點荒唐,但我設計了一個簡單的量化分析方法。

我把這個量化分析算出來的值稱為「內聚係數」,英文叫做 Cohesion Ratio(下文以 CR 代稱)。

CR 的計算方式非常簡單,就是一個類別裡面:

(每個方法用到的屬性數量加總)/(屬性的數量 * 方法的數量)

舉例來說,如果今天有一個類別長這樣:

class Apple
{
    private $propertyX;
    private $propertyY;
    private $propertyZ;

    function methodA()
    {
        // blah
    }
    function methodB()
    {
        // blah
    }
    function methodC()
    {
        // blah
    }
    function methodD()
    {
        // blah
    }
}

那麼這個類別屬性的數量是3,方法的數量是4。所以 CR 的分母就是 3 * 4 = 12

如果 methodA 用到了 propertyX,methodB 用到了 propertyX 跟 propertyY,methodC 用到所有屬性,methodD 沒有使用到任何屬性,那麼 CR 的分子就是 1 + 2 + 3 + 0 = 6

便可以算出來 CR 就是 6 / 12 = 0.5

如果每個方法都用到了所有屬性,那麼 CR 就會是 (3 + 3 + 3 + 3) / 12 = 1

如果每個方法都沒有用到任何屬性,那麼 CR 就會是 (0 + 0 + 0 + 0) / 12 = 0

你可以用這個公式幫任何一個類別打分數,分數越高,代表它是一個越好的類別!

CR 最高就是 1,最低就是 0

實務上大概不可能寫出 CR = 1 的類別,但要避免寫出 CR 接近 0 的類別。總之 CR 越高越好。

接著讓我使用這個「內聚係數」來評論、量化分析一些開發情境吧!

God Object 上帝物件

這是一個知名的 anti-pattern,也就是一個類別負責了過多任務,變成極為肥大的類別。

這種類別的屬性很多、方法也很多,而且大部份的方法都只用到少數幾個屬性。

根據內聚係數的公式,你會發現這種類別的 CR 一定非常低,非常接近0

該如何改善呢?根據內聚係數的公式,你會發現:把類別拆小就可以了。

把相關的 property 跟 method 放在一起,很輕易就能拆出多個 CR 更高的類別。

當你這麼做的同時,通常很神奇地,類別命名會自然浮現。

Long Method 過長方法

有時候你已盡力用上所有最佳原則、遵循 SOLID 指引,但還是有些 method 很長。

理性上你覺得這太長了,需要改寫,但感性上又覺得它們的確是一整串連續整體的邏輯。

該怎麼辦呢?

像這樣的 code,裡面出現的變數一定很多,每段任務分別用到部份的變數。

你可以分兩個步驟改寫:

步驟一、先把每段任務拆成各自的多個小 method,把用到的變數傳進去。

你會發現同樣的好幾組變數,反覆傳到好幾個 method,似乎讓 code 變得更糟糕了!

步驟二、把這些反覆在傳的變數,變成類別的 property。

你會發現很多 method 不再需要依靠參數,直接跟 property 互動即可。

並且因為這些 property 反覆在多個 method 被使用,這時 CR 也跟著提高了!

很簡單吧!改寫之後更可讀、更好維護,輕鬆改善 long method 的問題!

話說回來,那把出現的所有變數都變成 property 如何?

你會發現有些變數只在一兩個 method 被使用,提昇為 property 反而會導致 CR 下降!

內聚係數的量化分析明確告訴你:這樣不太好!

基於 Active Record Pattern 的 model

雖然一大堆 web 框架預設使用 Active Record Pattern 作為 domain modeling 的方式,但你會發現這方式問題還不小。

相關的 class 屬性幾乎被綁定成跟資料庫 table 的欄位一樣。

本文提到的利用 CR 協助類別設計的技巧就很難幫上忙。

這也是 Active Record Pattern 評價兩極的主要原因之一。

所以 google keyword: active record pattern anti oop 才會有一堆相關文章。

Service Object

那些基於 Active Record Pattern 進行 domain modeling 的社群因此會提倡 service objects 的使用。

這些 service object 類別通常都沒有 property,只有一些把 entity 作為參數、純粹描述商業邏輯的 method。

雖然是把 code 拆分到多個類別了,但你會發現,CR 的分子分母都是 0,這種類別有內聚性可言嗎?

好像不該寫出這些類別?但我覺得這些 service object 很好用阿,怎麼辦?

其實 OOP 講究把相關的資料跟行為放在一起,像這種「單純描述行為」的類別,從 OOP 的角度來看的確很差勁。

這些類別給人 Procedural Programming 的感覺似乎更多一些。

所以 google keyword: service object anti oop 才會有一堆相關文章。

其實也沒關係,反正現代程式設計本來就是各種 Programming Paradigm 都摻雜了一些。

如果說一定要從 OOP 的角度去反省,那你不妨這樣去想:

這些 business logic 我急著要上線使用,暫時不知道屬於 domain modeling 的哪裡,所以沒有內聚性可言,暫時都先做成 service object 擺著。未來當我發現某些參數重複出現在一些 service object 時,我可以再把他們一起重寫成一個獨立的類別,到時這些類別就會有不錯的 CR 值了!

通常到了那個時候,公司更確定自己的經營方向、工程師更確定 domain modeling 該怎麼設計,所以 CR 自然會通通提昇!到時你做的類別命名也不再是 XXXService 、XXXManager 這種含糊結尾,你會很容易找到一個很滿意的名稱!

目前這種 CR = 0/0 的類別,就當作是一種權宜之計吧!

話說回來,既然這些 CR = 0/0 的類別已經不太 OOP 了,你大可發揮創意,把這些商業邏輯放在你爽的地方就好。

  • 可以利用程式語言的 trait/mixin 特性來寫這些純行為的邏輯。

  • 直接開一個檔案裡面只有一堆 function 根本不寫 class 也可以。

  • 找個類別寫一大堆「靜態方法」也可以。

反正本來就沒有內聚性可言,只是找個地方寫的權宜之計,還有很多 service object 之外的作法。

可以當成是進行到一半的 domain modeling,未來有機會再完成領域建模吧!

(其實零除以零的結果並不是零,所以 CR 在此處也沒有否定 service object。它只是說:我無言了)

結論

OOP 程式設計易學難精,本文提到的內聚係數與量化分析只是一種輔助工具。

經典的各種程式設計原則、SOLID 原則、設計模式,都還是需要花時間乖乖學習。

實務上也沒有什麼真的 CR = 0.6 以上才算及格之類的數字,CR 數字大小終究是另一個依賴經驗的主觀問題。

我個人通常在設計類別實在很卡關、實在很趕時間的時候,才會拿內聚係數來思考一下。

因為它很無腦簡單,就是屬性跟方法拆一拆、搬一搬而已,相關的命名會自然浮現。

有機會你也可以試試看,用內聚係數幫你的類別們做個簡單的量化分析。

順利提高係數的時候,真的會有一種感覺:我的類別內聚性還不錯哦!

  分享   共 7,344 次點閱
按了喜歡:
共有 0 則留言
還沒有人留言。歡迎分享您的觀點、或是疑問。
您的留言
尤川豪
298 貼文  ·  215 留言

Devs.tw 是讓工程師寫筆記、網誌的平台。隨手紀錄、寫作,方便日後搜尋!

歡迎您一起加入寫作與分享的行列!

查看所有文章