Cassandra Rant

09:57上午 三月 15, 2010 in category Java by ingramchen

昨天看了一篇 Cassandra 的 modeling 範例

WTF is a SuperColumn? An Intro to the Cassandra Data Model

這一篇我看兩次了,現在終於看懂一半了... NoSQL 號稱 schema free ,可以自由設計,可是遠比 Relational 還難設計很多。設計 Cassandra 的 'schema' 時,我們必需先想好要怎麼 query (你的 use case) ,然後才能下手。然後幾乎是一個 query 就要生一個專用的 'table' 給他用。

用比較的方式來想:如果我們在 RDBMS 裡有一個 table ,然後對這個 table 有三種 use case,在 RDBMS 裡我們就寫三個 SQL,然後加加 index 改進效能。這樣的需求在 cassandra 就變成要生出三個 'table' 專門來應付這三個 use case,而且,想當然爾,這三個 table 的資料全部 denormalize。

Cassandra 這樣做與 relational 相比有什麼缺點呢?最直接的是:我們得事先知道這三個 use case,不然設計不出來。而軟體的專案特性是什麼?需求只會一直改!而且還會隨時間不斷冒出來!Relational 可以讓我們在 '事後' 需求出現後,寫個 SQL 補個 index 就搞定了。 Cassandra 可不行啊,我們得為那個 query, clone 一整份 table 的資料, 然後如果有其他地方 update 的話,也要通通一起改 (都 denormalized 了嘛....)

NoSQL 這樣的概念完全無法適應客製軟體的開發,尤其是在 enterprise 裡,那種瞬息萬變的各種專案大小需求,用這篇 blog 的範例舉例:例如本來的用戶需求是:"我要列出某個有 sports tag 的所有 blog 文",當時我們用 nosql 設計好了。然後之後 woods 事件爆發,他跑來說,我要改成 "列有 sports tag, 但是沒有 woods 的 blog 文"

哇咧!用 Cassendra 是要怎麼改?只能再做一個 sports minus woods 的 'table' ,然後 insert 到 sports 的程式通通再改寫一遍。暈倒,想到就沒力!

但是!如果 blog 系統是給一億人用,那又是另外一回事了,我想RDBMS根本跑不起來。Cassandra 再難用也得用了。也許適合用 NoSQL 的 project 都有類似的特質:

  • 用戶超多,使用時間也長

    因此用戶其實也很抗拒改變,想想 facebook 改版多少人在叫了

  • 改變的動機來自於平台商內部,而不是用戶

    一般軟體是 "付錢的" 用戶不斷提需求的,所以要不斷的改,但 facebook/twitter 感覺就是平台商施捨給用戶的,我 (平台) 給多少功能,你 (用戶) 就用多少功能,誰叫你沒付錢呢?需求改變的頻率變成由平台商主導,所以改變數也可以大大減少吧。

想到這裡,我就覺得 NoSQL 並不會改變 IT 界的日常生態,也完全無法取代 SQL。只有在 mega web apps 這個圈子的人才有機會碰到,也許未來這會變常態,也許不會。

迴響[0]

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()。這是他想要的,所以他依樣寫了幾行:

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

迴響[8]

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]