有時候,我們會看到一些寫得特別長的函式,這些函式會寫這麼長,往往不是一開始的時候它就是這樣子的。常常是這樣子的,在初期,這個函式的規模並不大,但是因為它的角色吃重,就像是個很有用的函式,因此,使得眾多客戶端程式碼對它產生依賴,也因為這樣,對它的需求也持續擴增,程式設計者時常會選擇基於這個函式繼續做擴充,在上面繼續添加功能,所以它的長度愈來愈長。

這樣的函式原本只提供比較特定的用途,但是隨著客戶端程式碼需求的增加,有一些以它為基礎所衍生出來的變化產生,因此,在增加長度的同時,它同時從一個特殊化的版本變成了更一般化的版本。很多時候,這同時增加了引數列表(argument list)的長度,因為通常會透過增加函式引數列表中的引數個數,讓函式提供更多樣化、更一般化的功能。

不只是以函式為個體,同樣的情況也會發生在類別上。一個在初期只是小小規模的類別,會因為它的好用,而獲得客戶端的青睞,使得眾多客戶端程式碼對它產生倚賴,但同時,隨著吸引更多的需求,在它自身原先實作不能滿足的情況一下,程式設計者選擇持續對它擴充,不是增加部份函式的長度,就是增加函式的數量。最後,這個類別的規模就愈來愈大,程式碼的長度也愈來愈長。

出現巨型類別的原因

在物件導向的設計裡,有些人設計一個類別是基於它的作用,或說功能。例如,我需要有一個可以驗證使用者身分的程式模組,所以我創造出一個類別,讓它提供認證的功能。

或許在一開始,它只能提供檢查 ID、密碼的功能。不過,隨著需求的增加,系統需要提供一個可以讓忘記密碼的使用者重新設定密碼的功能。由於這個需求和「密碼」有關,所以設計者在這個時候做了一個決定,就是把這個新功能和現成類別中在概念上最接近的合併起來。所以這個類別的介面上就被增加了一些新函式,用以提供以下功能,包括了:發送重新設定密碼的電子郵件、驗證重設密碼的請求是否合法、以及重新設定密碼、……等等,所以這個類別的規模就擴增了。

隨著概念上相似的需求愈來愈多,設計者也都決定要把這些需求納入這個現成的類別裡,於是這個類別愈長愈大、愈長愈肥。這種情況,相信在許多人的開發經驗中都不陌生。愈是在概念上容易被關聯到的類別,它就愈有可能愈變愈大。漸漸的,系統中開始出現一些巨型的類別,它們提供豐富而且多樣的功能,所以它們本身的程式碼長度也長,而且因為它們功能多,所以有更多的客戶端運用到它們,也因此相依於它們。如果試著繪出系統中類別間的相依關係圖,你會發現這些類別都有眾多的「in-degree」,也就是指向它們的連結,代表相依於它們的類別數。

從功能的實作來看,這樣做沒有什麼太大的問題,程式的行為不會因此受到什麼影響,但是考量更多進一步的因素時,這樣子的設計仍然存在一些問題。

設計時,應思考每個類別所承擔的「責任」是否過多過重

功能繁多、功能強大,通常就意謂著「責任繁多」、「責任沉重」。我們在設計時,有時候只想到功能、作用,卻忘了思考「責任」。

一個類別因為有作用,所以就有相對應的責任。責任和能力通常是對應的,也就是說,有愈多的能力,通常就會有愈多的責任。而且正如前段所言,它的能力的增長通常都是因為責任增加的關係,因為想要依賴它的類別變多了。

一個類別的責任有那些?一般來說,可以分為三種。第一種是該類別能夠執行的動作,例如一個類別可以重新設定使用者密碼,這就是一種動作。第二種是該類別所持有的知識或資訊,第三種則是該類別足以做出影響其他類別的決定。當其他類別以上述的三種形式倚賴特定類別時,這個類別就有了對其他類別的責任。

每個類別勢必都必須負擔起系統中部份的責任,否則它就沒有存在的價值和意義了。但是一個類別的責任不應該過重、過多。一個責任太重、太多的類別會有什麼缺點呢?

首先,它不容易被理解。因為它交雜了太多的責任在裡頭,使得程式碼的閱讀者沒辦法輕易的了解它究竟擔負了那些類型的責任,因為和它有關的概念太多了。

不容易理解,通常就不容易測試。它的責任多,意謂著供客戶端使用的函式(即我們常說的public method)就多,通常也就可能有相對增多的內部實作函式(即我們常說的private method)。這麼一來,各個函式之間的關係就有可能更為複雜,因為它們之間可能存在交互呼叫的錯綜關係,這自然不利於測試。

不容易理解、不好測試,很容易就衍生出不好修改、不好維護的結果。因為沒辦法很直覺地理解它,想要修改它就不會太容易。一個程式碼超過一千行的類別,光是要找到你想找到的程式碼片段都不是件容易的事,即使利用一些工具,像是自動列出所有函式、並且可以直接跳躍至該函式的功能,光是展開的函式列表都可能很長。

修改的困難不止於此,因為責任多,和其他的類別相依性就高,一旦自己做了修改,其他類別就有可能被波及到。而且,當別的類別發生了變動時,自己必須修改的機會也會提高,因為當類別本身過大時,自己的實作就多、呼叫到其他類別函式的機會就大,因此,當其他類別發生變動時,就可能也會因此而受到影響。除了這些問題之外,一個責任過多的類別,可以運用它的情境可能會較少,也會影響到它在各種不同的應用情境中被使用的機會。一般來說,我們希望程式碼被重複使用的機會高,但是責任愈多、愈龐大的類別,就有可能降低被重複使用的機會。

在重構的方法裡,太大的類別、太長的函式、太多引數的引數列表,都是需要被重構的對象。而它們都有可能是責任過多所造成的。

當一個類別因為接收到新需求,而成了一個被選定做為基礎以利擴充的對象時,設計者不能只從功能及作用面去思考,同時也應該要思考「責任」。包括了:它原始被賦予的責任究竟是什麼?新增加的責任是否符合原始的責任概念?增加新的責任之後,這個類別的責任會不會太多了?若是太多,就應該考慮做對應的設計,重新整理類別間的關係,包括增加必要的新類別。

降低對於特定類別的依賴,不只是為了平衡,也是為了分散風險

一個健康的類別生態系,應該是一個類別責任分配均衡的系統,沒有責任特別吃重的類別,也沒有責任微乎其微的類別。當然,這並不是說每個類別的規模都要完全均衡,總是會有重要跟較為不重要的類別,只是其中的差異不能太大。

我們有時候可以看到長度超過一千行的類別,那都是明顯偏大了,往往都需要拆解成若干個更小規模的類別,來分擔其責任,同時也分散其他類別對它的依賴,以及它對其他類別的依賴。

「超級類別」,也就是責任過重的類別,就跟「超人」一樣,有著拯救世界的責任,但是只要它有了狀況,整個世界也都會受到影響。我們不需要「超級類別」,我們需要的是每個類別均衡貢獻心力的世界。

作者簡介


Advertisement

更多 iThome相關內容