Get rid of getter and setter, toward domain driven design

03:02上午 十一月 08, 2009 in category Java by ingramchen

這是一個老題目囉,會再次寫是因為現在又多了一些經驗,有了新的想法。getter setter 這類的 property accessor 是相當常見的程式手段, 很多語言甚至內建。開發期間,我大部份的時間都在避免使用 getter setter,盡可能採用別的寫法,也希望別的開發人員能 follow。不過,有人反駁說,getter/setter 在語言或IDE都內建了,即然創作者設計出這個功能來,不就是要鼓勵開發者運用這個功能嗎?為何有方便的工具/寫法卻不去用咧!?針對這種問題,請各位看一下下面的例子:

So, 哪一個 method 比較容易維護、好用?這還用問嗎?這個道理很淺顯的 -- 語言提供給我們語法、給我們自由發揮,但不代表可以愛怎麼用就怎麼用。就像菜刀可以切一盤好菜、也可殺人一樣,工具用錯了就是個災難。Getter and setter 是一項工具,可惜太容易被誤用,反而變成 bug 的根源。

實例

用實例解說最有說服力,讓我們開始吧:今天客戶有一個需求,希望統計用戶到網站進行購物的行為 -- 用戶如果完成了購物的程序,就給予正面的評價;如果用戶買到一半,購物車放了一堆東西,不刷卡半途就跑掉了,這時就扣用戶的評價。工程師聽到需求之後,發現執行上有困難,因為用戶直接關閉網頁時,server 並不會知道,所以沒辦法馬上做負評的動作。與客戶討論後,雙方都同意採取變通的辦法:當用戶開始購物後,先預扣他的評價,直到他完成購物後才補回。如果他沒完成購物,則預扣的部份可以在他下次進來購物時再扣回,或是用 batch 在後端每天跑一次。

工程師很快就設計出 table:

而他開始在他的 shopService 這個 facade 加上這個新功能:

按需求,我們在開始購買 startShopping(), 和結清 endShopping() 時,安插了對評價的加減。開始購買時會先預扣個評價, 購買完成後則會把預扣值清掉。如果開始購買前,發現用戶還保有預扣值,表示他之前沒有完成整個購買程序,這時我們要先扣回來。

ok, 到目前為止沒什麼問題。這種寫程式的方法是 Transaction Script,把要做的事通通堆在 facade 的 method 裡直接一口氣做完就是了, 這是最多人寫程式的方式,但其實處處是破洞。第一個問題就是 facade 裡的 method 越來越長了,跟真正購買無關的事多佔據了好幾行。這裡已經把 database 的操作搬到了 dao 去了,不然更是累贅。第二個是 rating / withhold 的值沒有做任何保護,任何拿到 userRating 物件的人都可以恣意修改。而且 withhold 和 rating 間值的變化必須按照一定的程序來做的,這個也沒有保護到。

有經驗的 Java developer,遇到這些問題,馬上善用 IDE 強大的 extract method refactoring,整理了一下程式碼:

整理過後,startShopping, endShopping 變得比較清爽了,然後 rating 有了最小值的保護 (最小是0), withhold 的值也加了 contract 的保護 -- 不可為負值,這樣好像還不賴,工程師 refactoring 完之後,收工換做下一個 task。然而.... 挑戰是從新的需求來才開始的....

一個月過後...

新功能上線一個月之後,客戶說在下次購買前才將預扣值扣回太慢了,希望可以在用戶一登入,馬上就將預扣扣回,這樣用戶一進來就可以依照最新的評價值,推薦不同的產品。工程師 B 是個 database guy,在收到這個 task 之後,他先去看了 table。發現有 table user_rating 和 withhold 這個欄位,也很快的找到對應的 UserRating class,而且還有 Dao 可用耶,興沖沖的在 UserService login 的地方就加了這幾行:

well, 這回工程師 B 犯了程式碼重覆的毛病,不過他可不知道程式有重覆到,他自己測試時也都正常 (他反覆測了二、三次)。但是上線後的隔天.... 客戶發現有用戶的評價開始變成負值了,網頁更出現奇怪的事,像是推薦到不該推薦的東西。而另一頭用評價來算 VIP 客戶折扣的地方,負評的人通通誤判成 VIP,一堆人莫名奇妙打六折,於是另一場 鄉民大戰 DELL 的戲碼又再度上演....

Getter and setter are evil

第一位工程師當初在設計時,並沒有去思考物件封裝的問題,他習慣性的就寫了 getter setter,將 UserRating 內部所有的欄位全部暴露出來,而他所做的有關預扣加評價的邏輯,以及評價最低值的保護,則放在完全不相干的 ShopServiceImpl 裡。你說他做錯了什麼嗎?看看上面的程式碼,其實也十幾行而已。十幾行的程式他一下就寫完了,又加了中文註解。而且他也遵循了 "用最簡單的方法完成任務" 的精神,不多做無謂的設計。他會想這樣後續維護不會很難吧?但是以結果論,的的確確是造成了災難...

怎麼辦?

答案在被大家遺忘在學校中的 OOP 裡 -- 將資料封裝並且定義物件的行為。

也在 FP (functional programming) 裡 -- 不會改變的資料最安全 (prefer Immutability)

接下來將移除萬惡 getter setter,按照上述的精神,示範一種較為強韌的設計,這並不是唯一解,也不是最好解,只是我個人開始用這樣的 style 寫程式,分享給大家參考:

我們給了 UserRating 兩個新的行為,一是 prepareWithhold(int amount) 二是 giveRating(int amount),專門打造給 ShopService 呼叫。這樣 ShopService 一樣很乾淨,而且也清楚在幹嘛 (不用寫什麼註解了),接下來看一下大改造之後的 UserRating:

UserRating 是 Entity,有 id,這個跟原來一樣。不過原來的版本他是有五個欄位的,但這裡將預扣值 (withhold) 和評價值 (rating) 這兩個欄位包在一塊,變成一個獨立的 value object 'WithholdableRating' 了。為何要特意包在一起?原因是我觀察到 withhold 和 rating 這兩個值,常常一起改變,它們必須按照某種規則操作,而且其值域也有所限制。我可以在 UserRating 裡寫一堆 if 來管理這兩個值的關係。 但是東一個 if 西一個 if 還是很亂,沒有辦法讓程式 "speak itself"。這裡特意將 WithholdableRating 設計成 Immutable object ,每一次改變值就重新產生新物件,而且它的 method 都有對應到需求 -- '預扣'、'扣回舊的預扣'、'清除舊的預扣'、'增加評價'。 如果讀者注意的話,這四個字眼在我們上面的討論中已出現過幾次。

上面蓼蓼數行的 WithholdableRating 可不是隨便寫寫的,這個物件是不會 '崩壞' 的。 首先,它無法被 construct。我們只能拿到 INITIAL 這個 constant 開始操作,因此在程式中無法建立任何不按規矩的 WithholdableRating。 其次 withhold(amount) 這個 method 不能連續呼叫兩次,這是另一層的防衛。另外也限制 withhold 和 rating 這兩個值不可為負值。 而 rating 的最低值則保護在 private constructor 裡。WithholdableRating非常的小且單純,可以想像它的 unit test 有多好寫了吧。最後一點,眼尖的讀者應該已經看出其實還是有幾個漏洞,第一個是 default constructor,另一個是 rating 和 withold 這兩個欄位不是 final。所以嚴格來說它並不是真正的 Immutable Object --- 這一切都是向 JPA/Hibernate 妥協....

Anyway, WithholdableRating 實際用起來怎麼樣呢?看看 UserRating.prepareWithhold() 吧,裡面寫了一段 '話' :

'先扣回舊的預扣、再做預扣'

另一個 method userRating.giveRating() 則說了 -- '清除舊的預扣、再給予評價':

像白話文一般的程式,簡單到了一個極致。

好了,總結一下,新版的程式完成幾個目標 -- 程式碼自己說了自己做了什麼;不用註解;沒有散在各地的 if;WithholdableRating 牢不可破;Easier Unit Testing。

那麼,讓我們時光倒流。當工程師 B 遇到這樣的程式,他這一次會怎麼處理呢?首先他也是找到了資料庫 rating 和 withhold 的欄位,不過他找到 UserRating 時,大概會愣個一下吧?居然只有兩個 method 可用,而且看名字也不是他想要的,他想想只好自己動手加吧。往 UserRating 裡面一挖,又看到 WithholdableRating,這回他愣比較久了。如果他之前沒碰過 Immutable 的概念,那他不是馬上找救兵,就是會破口大罵吧?不論如何,在 withholdableRating 上他可以看到非常白話的 method: deductPreviousWithhold()。這是他想要的,所以他依樣寫了幾行:

這一次,大家晚上都睡了個好覺....

迴響[3]

A Little Functional Programming Experience

04:56下午 十月 24, 2009 in category Java by ingramchen

很久沒寫 blog 了,沒為什麼,就是懶而已... 另一方面則是沒什麼感想可以寫。過去一兩年來,寫的程式比較雜,也多學了 haskell, python, actionscript 和 scala 幾個語言。haskell, scala 和 python 我都是寫來玩的,actionscript 則是工作上需要寫遊戲,硬著頭皮去學。actionscript 到了 3.0 之後,越來越像 java,所以一開始寫起來沒什麼障礙,總之當做 java 用,大概都會對。

後來學了 scala 之後,想寫個 toy project 實際運用 FP (functional programming),看看能體會到什麼。不過實在沒什麼題目可以玩... 就開始打起 actionscript 的主意。actionscript 其實也算是部份的融合 OOP 和 FP 的概念,所以像是 filter/map/foreach/function as object... 這些基礎設施都有具備。而近期又有一個單人開發遊戲的機會,對我來說這是個很好的練功實證的機會 -- 能在真正的產品開發上使用 FP 的方式進行,沒有比這個方法更能學到真正的內容了。(作作 tutorial、寫個 toy project 永遠只是隔靴騷癢、半瓶水,不會學到什麼精神...)

Well, 目前該專案已經尾聲。寫了這一兩個月,我跟所有品嘗過 FP 的 imperative programmer 一樣,腦袋瓜硬生生的轉了180度。現在看到 collection 的想法完全不同了 -- Think transformation instead of iteration。第一個最大的差別是想的時間比較久,你必須思考 collection 在各個 step 的轉變,這有點像是寫 SQL 時的那種思考方式 (寫 SQL 也要花很多時間...)。第二個差別在於當然是 FP 寫出來的程式少了很多無謂的變數,關於這點其他 blog 和書有更好的著墨,所以這裡就不提了。第三個差別是 bug 很明顯的變少,使用 FP 方式處理 collection 的相關程式,如果出了錯,那多半是對需求理解有誤,而不是程式不按照心理所想的邏輯運作 (就是心理想的是一回事,寫出來的,因為少加了個 1,或是在錯的地方 break,造成不預期的錯誤)

受限於 actionscript 提供的 FP 功能有限,有一部份處理 collection 仍然用 iteration 的方式來寫。這剛好提供了一個絕佳比較的機會 -- 我發現 FP 的方式想的比較久,但完成的比較快。imperative 則是寫的很快,但是 debug 卻很久。這樣看來兩者在開發時間上都算平手啊!想加快開發時間,不是說選了其中一個方法就可以了。你選了 FP,你就得花時間熟練 FP 的思考方式,以縮短思考時間;選了 imperative,你就得熟悉 iteration 法常犯的錯誤,然後練到 "本能性的避開"、進而減少出錯的機會。而就維護性來說,FP 的方式對已經懂得人很好維護的,然而罩門是懂 FP 的人不多... 而 imperative 雖然大家都會寫,但是看到 i,j,k... foo.length-1, break, continue到處亂交錯,這樣的程式也要想老半天才知道要在哪裡下刀啊。所以維護性也算平手吧。

然後... 重要的一點是, FP 沒辦法讓你不用再寫 unit test 。該寫的 test 還是跑不掉的。FP 雖然減少了許多想法轉換到程式的錯誤,但是沒辦法避免想法錯誤。hmm... 講的太玄嗎?對於沒有 FP 經驗的人,大概不能體會我上面說的,但你可以用 SQL 去理解 -- 試想,有人可以 SQL 一次就寫對嗎?

那麼,有什麼結論呢?FP 比 imperative 好? imperative 比 FP 好?我的看法是看 context 吧。你們的團隊是否有多數的人願意學習 FP 呢 (不用會,只要有動機肯學)。如果答案是肯定的,那麼團隊可以開始 shift 到 FP 的寫作方式。一開始開發肯定會變慢,但是到後期因為 debug 時間減少,反而會後來居上,這是值得的。但如果你的團隊沒幾隻貓想學 FP,那麼你就打消念頭吧,FP 留給自己增加功力就好,因為你寫的 FP 再好也沒人會維護,到時反而變負擔。

技術是給人用的,好壞還是看使用的人吧。

ps. 對於 FP ,我目前只有嘗試基本的 collection 操作,FP 當然還包括更多其他東西 (如 tail recursion),所以這篇的心得不適合當做一個通盤的參考依據。(沒人會認真看待吧... 沒人..)

迴響[3]

Risk

01:05上午 十一月 02, 2008 in category Java by ingramchen

風險?在金融危機期間,這可是個閃亮亮的題目。可惜這一篇不是你想的那樣,如果你是對金融危機題材有興趣而誤闖, 那你可以直接關掉了。這一篇仍然跟過去一樣,是一篇科技宅男的思路軌跡、一些理財上零零碎碎想法的小整理。(自 high 文~~)

我對風險這個字開始有感覺,始自於去年開始學一些理財相關的事,之前的生活,就是程式+程式+程式。其實現在還是 (笑)。 那時剛開始接觸,和熱心的同事們一直討論著股市/基金的東南西北,也曾作過靠股市致富的白日夢。不過這白日夢沒幾個禮拜就醒了,原因還蠻單純的, 只是看了綠角的部落格,然後花了一個週末k一本書。相較許多人是在市場中失利,搞到賠光光才恍然大悟。我真的覺得自己幸運到不行。

那本讓我醒來的書是 The four pillars of investing,作者是 William Bernstein。中文譯名叫 "投資金律",雖然中文版已經絕版了, 好在圖書館還借的到。本書提到投資的四個支柱,分別是風險、歷史、心理學、產業。書中一開頭深入討論 報酬來自於風險,這讓我大有矛塞頓開之感。

沒有風險就沒有報酬,更高的報酬來自更高的風險,然而,高風險卻不等於高報酬。

喔喔喔,我真的想大喊 eureka!! 人生在世,遇到這種開竅的時刻真的不多。相信很多人,包括我,遇到了這個 moment,都會像阿基米德狂喜般地從浴室衝出來,大聲告訴世人自己的發現。我剛看完這本也像是瘋子一樣,遇到朋友、同事就狂推薦....... 不過....... 當我轉述 Bernstein 的一句話給朋友:

投資不能賺錢,只能讓你不會變窮

....well, 我很清楚的看到對方臉上三條非常粗的黑線.... "風險" 和 "賺錢" 這兩檔事,大部份的人都會想聽後者吧? 長期推廣被動指數化投資的綠角也曾提到 -- "能夠接受類似觀念的人,跟他/她的成長背景和經歷很有關係。" 嗯,我後來想想也是如此。

anyway... 這篇不是投資金律的讀後感+推薦文,所以也不細談了。從那時候起,風險就一直掛在我心頭,從此思考任何事,總是會不自覺的拿風險過濾一下。而我現在處在正在創業的公司,風險這兩個字,纏得我更緊了。

有看過流言終結者 (Discovery Myth buster) 嗎?身為一個 tech geek,mythbuster 很自然地成為我定期收看的節目。 裡面的兩位主持人,分別是 Adam 和 Jamie,各自浸淫特效數十年,在節目中完成一項又一項的精彩流言驗證。有一點值得觀察的是 Adam 生性 活潑大膽,愛冒險,較無厘頭。而 Jamie 則是完全相反:嚴謹負責,知識豐富,重視風險。 想像如果這節目只有 Adam 一個人的話,以 Adam 橫衝直撞的作風,那這節目肯定是場災難,沒多久一定在火災/水災中結束。反之只有 Jamie 的話, 缺乏神來一筆的新意,太過沉悶,節目收視率只會跌跌落。兩個人的組合正好 perfect match,創造了長紅的收視率。

冒險創新和風險控管本質上完全相反,但個人認為是同樣重要的。只偏創意,就像是只有 Adam 的衝勁,但是卻沒有像 Jamie 那樣嚴謹的意識在把關。 沒有考慮風險,等於車子沒裝煞車,這樣真的到的了目的地嗎?

回到理財... 風險控管在理財中,我們可以透過資產配置和分散投資來達到降低風險。一般講資產配置都是提到將資產分散到不同的標地。 不過我在另一篇文章 (找不到了...) 中讀到一個特別的觀念,文中將資產配置的精神推到更廣,它將人本身也算是成是一個資產,必需一起納入配置中:

  • 如果任職於穩定的大公司 or 公務員,那麼你的投資項目應當增加小公司的比例,藉由多冒些風險提高投資的報酬
  • 如果任職於小公司,那麼應當多投資已開發國家的大公司,因為不景氣時大量小公司倒閉。不僅自己待的小公司倒了,連帶持有其他小公司的股票也受損。
  • 如果你身在上市公司,握有公司股票,那麼應當減少自己公司的持股,提高持有其他產業的股票。 因為如果公司倒了,不僅失業,原持有的股票也成壁紙。這是雙重打擊,風險太高。

把自己當成配置的一部份,風險控管將更加的完整。

常常會聽到有人會說,投資股市債市,不如投資自己。well... 投資自己是值得去做的事,像是投資自己多學習能夠增加一技之長, 的確降低了失業的風險,但如果 "只" 投資在自己一人身上,那就完全沒有分散到風險,這是很危險的事。 自行創業的人算是這類人的極致,他們的資產自然是全部集中在自己的新創事業上,而且,幾乎是不成功便成仁。 在這麼高的風險下要存活,我想風險的控管勢必是重要的一環。至於怎麼做,我沒有經歷過創業,自是一概不知。 但我猜也許是靠人脈來分散風險吧?在危機時能夠靠人脈度過難關,能撐得越久,就越有機會成功。畢竟創業的成功與否,機運佔很大的部份。

接下來做個題目吧,利用分散的方式對自己實做風險控管:

國家經濟:我目前居住在開發中國家,台灣。賺取台幣薪資,台灣經濟未如已開發國家穩定, 本身風險已高。因此必須多持有已開發國家債券,及大公司的股票,並且減低持台股,台幣的比例, 當台灣發生危機時,工作本身雖然已受創,但最少還可避免資產損失。

居住威脅:台灣與中共之間一直保持著奇妙的平衡,這平衡能夠撐多久實屬未知。 並不是說平衡倒向哪一方,風險就特別大,而是 "未知" 本身才是風險。對未來越不確定,風險越高。 相較日本、澳洲、美國、歐洲等地,台灣目前風險實是大的多。最直接降低風險的方法就是狡兔三窟。 有本事拿綠卡,沒本事也要有國外的親友可以暫時依靠。

工作類型:目前我在科技創業的小公司裡奮鬥,雖然只是員工,並沒有面臨自己創業那種巨大的風險, 但風險也是挺高的了。所以在資產上小型公司的持股必需減少。網路類的持股也要降低。

結語

其實上面聊到的很多只是純技術面的考量.... 真正的大絕招應該是 "下流志向" (註) 提到的互助來降低風險 -- 如果在別人危險的時候,你願意伸出援手,而你危險的時候,有人願意幫你,整體的風險自然就大幅降低。 簡單講就是人脈。不過這種大絕招宅男是很難想像的。

註:下流志向是一本討論日本教育的書,主要探討現在學生學習的心態,以及繭居族的問題,有不少洗腦袋的新觀點。 好吧,我承認當時是看到下流兩個字就去翻了一翻.....

迴響[3]