l

2013年3月1日 星期五

Private Method不該有複雜的邏輯嗎?

Mar. 01 14:45~16:01

image

前幾天寫了一篇《要不要幫Private Method寫單元測試?》,有位熱心的鄉民提出一個問題:

為何private method內不該有複雜的邏輯呢?憑什麼《JUnit Recipes》的作者認為只有public method 才有資格擁有複雜的內部邏輯呢?因為作者有這樣的可見性歧視,所以才會有「只有public method應該測試,private method如果需要測試,很可能他本來應該被public才對」這樣的論調。

在討論這個問題之前,先補充說明幾點:

  • 《JUnit Recipes》的原意應該不是說private method「不須測試」,而是private method應該透過public method來測試即可,不需要「直接」測試private method。
  • 在前述的假設之下,如果private method的邏輯非常複雜,則要透過測試public method來涵蓋到private method的所有可能執行路徑(至少要考慮到主要的執行路徑)很可能讓測試案例變得很難寫或是過於複雜
  • 此時回頭思考一下,為什麼這個private method的邏輯會如此的複雜?有沒有可能是在設計這個類別的時候,責任沒有劃分好,以至於這個類別做了太多事情(擔負太多責任)。這些責任,有一部分剛好是目前的客戶端所關注的,所以被設計成public method。而有些責任,有可能並非客戶端主要關注的焦點,或是設計者還沒有認知到這些責任應該要擺在哪裡。此時設計者有可能就把這些責任當成類別實作的細節,將其放在private method之中。在這種情況之下,private method就很有可能會過於複雜以至於很難透過測試public method來測試到private method。這種設計的結果可視為一種警訊,提醒設計者注意是否有改善設計之處。例如思考這些private method有沒有可能應該要另起爐灶,設計成另外一個類別的public method。但就好像打噴嚏可能是感冒的前兆一樣,但並不是說只要是打噴嚏就一定是感冒。所以private method很複雜只是一種潛在設計問題的症狀,並不是說只要是private method有著複雜的邏輯一律都要抽離出來變成另一個類別的public method。

***

換一個角度來思考。很多年前Teddy當任「物件導向分析與設計」這門課的助教,發現有不少學生的設計,長成下面這個樣子。

螢幕快照 2013-03-01 下午3.16.40

 

Teddy:不是有規定要用物件導向的方式來設計系統嗎,你的設計怎麼還是程序導向的思考?

學生:我有用物件導向設計啊。物件導向不是告訴我們要設計類別,並且把資料行為一起封裝在類別之中嗎?我的系統有設計類別啊,就叫做System,裡面也有很多資料與行為。你看,我設計了20個public method給使用者直接呼叫,另外有100個private method用來隱藏實作細節。

Teddy:你的private method很多都超過1,000行了耶?

學生:對啊,這些都是實作細節,反正這些行為我套用資訊隱藏的做法,將它們設計成private method,所以就算是這些private method再怎麼 混亂 改變,也不會影響外部的使用者。

Teddy:好,很好,你物件導向學得不錯嘛!零分,下一位惱怒

***

物件導向設計強調透過「物件互動」來完成工作,因此物件介面設計的好或不好,成為影響物件導向系統設計品質的主要原因。要一開始就設計出良好的物件互動模式與物件介面,說實話並非易事。了解物件導向設計技術與設計原則是一種方法,學習設計模式(design pattern)是一種方法,從測試的角度來檢視設計也是一種方法(所以TDD是一種設計技巧)。

看完本篇先不必急著開罵或是馬上把你的private method抽離到另一個類別之中。請花一點點時間找一下鄉民們手邊已有的程式,看看那些「過於忙碌(有著複雜邏輯)」的private method,如果把它們抽離到其他類別之中,原本的系統是否會變得更容易理解與測試。

***

友藏內心獨白:物件導向技術還真的不太容易學啊。

12 則留言:

  1. 要不要抽離(因為承擔過多責任)與是不是private是兩個不同的概念,只要是non-trivial的func,就應該進行測試,只要是non-trivial func也同時要考慮抽取出責任,直到"感覺"對了為止,跟是不是private的關係不大。

    回覆刪除
  2. 我贊成 Mars 所說的,這本來就是風牛馬不相關的東西。
    照這篇文章所說,請問,難道 public 裡面有 1000 行以上的實作內容就不是設計的警訊?只有 private 內有 1000 行才危險?
    我就覺得列出的第一點:private 一定要透過 public 來進行測試不可以直接測試,這是作者自己挖洞給自己跳,才會引發錯誤的 visibility 決策。
    public /private 是處理相依性的策略,怎麼會跟解決程式複雜度有關?
    Teddy 大哥舉了 ATM 的例子,那我也舉個其他例子。
    假如我今天撰寫一個 storage class,像是 memcached 之類的服務,提供了 getter / setter 這樣給外部存取的機制,但因為產品安全性考量,所以內部決定自己實作加解密能力,並且為了空間使用率考量,也實作了壓縮解壓縮的能力。
    encrypt / decrypt 和 compress / uncompress 我該不該寫單元測試驗證?當然應該,那是有點複雜的東西,我要測試才能確定資料是否可以正確被還原。
    然而,這些實作的特性我不想給外界依賴,因為日後改寫與變動的機會很大(隨著安全性考量提升或是空間與時間效率的考量改變),且這些並不列舉在 product feature 內, consumer 只要知道他可以存放和提領資料就夠了。

    請問我難道要只因為要做單元測試而把 encrypt / decrypt / compress / uncompress 這些不想讓外部依賴的東西都 public 出來嗎?

    再澄清我的想法一次:可見性是處理相依性的策略手法,跟複雜度無關。過渡複雜無論出現在 public / private 都該一視同仁,都要被處理。不應該 public 就容許他可以複雜到上千行就當做沒看見。

    private 因為太複雜導致難以測試,那是作者自己挖洞給自己跳,誰叫他認為「private 應該要透過 public method 來測試」呢?直接測試 private 根本就沒有難以測試的問題啊。

    這樣你懂我的意思嗎?

    回覆刪除
  3. 我覺得這是設計上的考量。private method做加解密的工作,我的看法是原本的class至少負責兩件工作,當然你把它視為原本邏輯的一部分那就是只做一件工作。但是這樣的設計如果導致兩件工作(我的看法)互相影響,改了A會不會影響到B,就會增加日後maintain的困難。

    如果考量是不想給外界依賴,private method不是唯一的方法,用interface也可以減少耦合性。

    我不認為作者挖洞給自己跳,是這樣的設計符合OO的原則,比較好maitain,是很寶貴的建議。

    回覆刪除
  4. @William:
    "但是這樣的設計如果導致兩件工作(我的看法)互相影響,改了A會不會影響到B,就會增加日後maintain的困難。"

    我想你說 AB 互相影響應該是指同一個 class 內有兩個 method 分別是 method A 和 method B,你擔心這樣維護困難對吧?

    如果今天依照作者意見改為 class A 會呼叫 class B 一樣是交互影響,難道可以簡化原本 A call B 的維護困難?

    更慘的是,原本 B 是 private method B,我可以肯定只有這個 class 會受影響,現在變成 public class B,可能被影響的範圍我已經無法估計和肯定了。

    請問哪個才是真正難以維護的狀況?

    透過 interface 減少耦合,那是 consumer 的 option,producer 是無法控制的。當你產出的 lib 已經有 public class,你就無法強制規範其他 developer 一定要透過你宣告實作的 interface 來調用你。那完全只能依賴 consumer app developer 的成熟度來掌控。

    可能我自己是做 infra-framework library 的人所以特別敏感吧,實務上我知道要調整自己 release 過的 public interface 的難處有多高。

    回覆刪除
  5. 我覺得這件事應該回到原本的主題,學員問:“要不要幫Private Method寫單元測試?”
    但是作者並沒有真正回答這個問題,而是說:「如果你覺得你的 private method 需要被測試,或許是你的 design 有問題喔,你最好回家看一下!」

    如果 design 沒問題呢?那這個問題的答案到底是什麼?該不該替 private method 寫單元測試呢?

    design “或許”有問題是作者一廂情願假設,所以作者並沒有真正回答到這個問題,而是顧左右而言他。

    但是對書籍的讀者或是學員而言,他拿到這個答案,回家一看這個 private method 真的很想測試測試,反而導出了「原來我這樣的 design 是錯誤的」這樣非功能性的結論。

    ATM 的例子因為這些 method 都還在 BLL ( Business Logic Layer ) 層級,所以剛好從 SRP 角度可以合用。但是本來 public method 就是為了支援商業邏輯界面而準備的,底下很多技術面的複雜細節,例如 Protocol hand-shaking, failover, Fault-Tolerate,甚至 error handling, retry 這些非商業邏輯的複雜實作,確實有他必要的測試來驗證,而這些能力也的確不容易透過商業邏輯的 public 入口來觸發這些 robust feature.

    但為了測試因此要把 Protocol hand-shaking, failover, Fault-Tolerate,甚至 error handling, retry 變成 public 我認為疑慮非常大。

    回覆刪除
  6. 難得這一篇引起比較多的討論,之前寫的技術文章好像都沒獲得什麼迴響...Orz。

    幾件事補充說明一下:

    (1) 我以前寫程式針對比較複雜的private method是會直接幫它寫單元測試的,說實話以前我並不會特別覺得這樣有什麼不對,只是覺得透過 reflection來測試private method不太方便而已。

    (2) 部落格文章中所謂「private method有著複雜的邏輯」,也不是說這個private method一定要幾百行以上,或是要有N層的nested結構,或是有非常複雜的條件判斷式。我只是單純從可否透過測試public method就可(不必太費功夫)涵蓋private method的角度來判斷這個private mehtod是否過於『複雜』。

    (3) 前幾天我看到 《JUnit Recipes》對於是否要直接測試private method的看法,我覺得蠻有道理的。此時我回想以前開發的軟體,的卻有不少情況,當我將 public method 的實作整理成呼叫若干個 private method之後,得到了這邊所謂的『比較複雜的 private method。當時我並沒有覺得這樣的現象有什麼問題,但是現在回想起來,有不少情況是因為當初的設計讓一個類別負擔太多責任。

    (4) 還有一個觀念是我在文章中沒有提到的,就是「public method 並不等於 published method」。先撇開測試的問題不談,大部分的人都認為,一但把method變成public,日後要在修改這個method的介面,便會影響到很多caller。這樣的觀點是把public method等同published method,如果可以將者兩者區分,在設計上(與設計上)會有很多彈性。請參考Martin Fowler 2002年在IEEE Software 寫的這篇文章:http://martinfowler.com/ieeeSoftware/published.pdf。舉個例子,Eclipse也套用了這樣的觀念,被放在 internal package 的 public class/method,從程式語言的角度來看,雖然是屬於 public interface,但從設計者的角度來看,並不是 published interface。也就是說,放在 internal package的類別介面很可能會一直變動,若客戶端堅持要直接呼叫,日後介面改變的風險請自負。

    (5) 軟體設計原本就沒有標準答案,我在部落格中也僅是提出我的「個人看法」。我覺得我文章中提出關於是否要測試private method的看法應該已經講了很清楚了啊 XD:
    A. 請先考慮透過 public method 來測試 private method。
    B. 如果透過 public method 來測試 private method很難,請檢視一下該類別是否負擔太多責任。
    B.1: 如果是,請重構。
    B.2: 如果自己認為不是,請找朋友幫你確認一下 XD。
    B.3: 如果真的、真的不是,那答案不是呼之欲出了嗎?要嘛就是寫稍微複雜一點的測試案例,還是透過public method來測試private method;要嘛就是直接測試 private method。
    C. 如果還不清楚,可參考這篇文章 http://www.artima.com/suiterunner/private.html (感謝Joey Chen提供補充資料)

    打了這麼多字,應該可以再生出另一篇blog文章 XD。

    回覆刪除
    回覆
    1. 對第四點的個人心得:我自己寫Java程式的時候,當一個class開始超過400行時(不包括註解),我就會開始覺得它有『過份複雜』的危險。
      對一個可能過份複雜的class,最常採取的策略就是:
      0. 先分析這個class的責任在各個generic 需求構面上的實現:(Serialization, Object[eq,hash,clone], synchronization, app-lifecycle, Global Resource usage(thread, connection...))是否都有清楚、明確的作法?而這些作法的實做是否一定得要在這個class裡?因為這些東西通常會有可以抽的部份。
      1. 先找有沒有適合抽成 static method的private method 再看適不適合往外扔,通常字串處理、IO處理相關的一旦夠大都一定會有夠通用適合往外扔的。
      2. 觀察成員變數在method裡的使用是否有clustering的跡象,當某幾個變數使用的時候就總是在一起,那這些變數通常具有邏輯上的模型意義可以聚合起來給一個身份。

      幾個常見的聚合可能:
      1. Repository:member fields是為了表示一種資料結構,通常可以包起來給個XxxRepository,然後Operation給好。
      2. Context:這個class會跟application中存在的某種flow(或稱execution)互動,於是class 裡多個methods的 params具有生命週期上的一致性(甚至根本就都是同一票變數的集合),那這時候就會適合聚合成Context Object,另一個觀察重點是,通常Context一出現,會發現有很其他class的args都應該採用它作為method args。

      這樣整理完,就產生了一些Utility method(public static),還有一些Helper class(Repository, Context)
      ,這些東西很多我們都不希望Jar外頭的人拿去用,但這些東西有自己的邏輯、也都應該要被測試,而該抽的抽完後,現在他們很好測了。

      刪除
    2. 很棒的建議,謝謝分享。

      刪除
  7. 受教了。

    published interface 和 public interface 這樣的區分,雖然不是每個公司環境都能施行這樣的管理方式,但卻也是個不錯的概念和思考。

    回覆刪除
  8. 是跟C++中的 PIMPL 一樣的意思嗎?

    回覆刪除
  9. Hi Pajace,

    我不知道C++中的 PIMPL 是什麼東東...Orz...不過剛剛google了一下,PIMPL和本篇文章要談的東西應該是不一樣的喔。

    回覆刪除
  10. Teddy已經說得很清楚了,我還是回應一下Tom以及提出一些意見。
    根據"program to an interface",物件之間應該相依於抽象觀念,是可以減少複雜度。Class A直接呼叫Class B,這件事要好好考慮是不是要這樣做。
    我覺得應該聆聽unit test告訴你什麼,測試程式很難寫,花很多時間,架構很難改變,都是警訊。既然用unit test建立安全網,在有限時間內多做嘗試,也許對問題會有不同觀點。

    回覆刪除