2011年11月23日 星期三

使用 Django 的雜感

換工作後大概很少會用到 Django, 在這對它做個了結吧。

先說結論

對於不熟 web 又想用 python 開發的人來說, Django 仍是首選, 主要的優勢有:

  1. 官方文件超級豐富, 網路上文件也豐富, 也有出一些書, 不過書上內容應該沒官網新, 看官網就夠了。不夠的話再看原始碼也比較方便。
  2. 社群龐大, 有許多 middleware 和 plugin 可用, 像多人合寫 web 一定會需要 database integration, South 相當好用。其它像用 Facebook / Google / Yahoo 等帳號登入, 也都有整合好的套件可用。
  3. 框架本身實作了許多 web 相關功能 (像 session、cookie、cache、傳輸 zip 後的內容), 可避免犯錯 (像是有擋 CSRF), 很多東西是新手從來沒想過的, 但框架都有考慮到並且實作好了。在使用的過程中可學到這些知識。
  4. 極佳的向下相容。官方的 roadmap 會明確提到那些功能已 deprecated, 並會在未來的那一版移掉, 有滿長的過渡期。使用者升級的負擔相當小。
  5. 提供一組規範, 切開 model、view、control, 還有 class / table 命名規則等, 減少團隊合作的問題。

當初選 Django 的主因是第一、二點, 第三點稍微有想到, 而第四、五點是使用後得到的驚喜。

說完結論後, 要開始碎碎念我的不滿, 請各位看倌記得, 即使如此, 我還是推薦不熟 web 又想用 python 開發的人使用 Django, 理由如前所述。

關於 template

Django 內建的 template 不好用, 效率也差, 甚至在 FAQ 裡有一項提到「I can't stand your template language. Do I have to use it?」。有些人可能覺得 template 的效率不是重點, 瓶頸會在 database。當我費盡心力減少 SQL、改 schema、改index、改寫 SQL 將 database 花的時間壓到極致後, 卻發現 template 怎麼縮都要 0.1s, 讓我很無力。更別提在關掉 i18n / l10n 前, template 要 0.2s。看著簡單的 template 內容, 很難理解為什麼這樣的東西要花到 0.1s。

此外, template 的語法很受限, 不過到 1.3 版後 include 多了 with 的語法方便代入子版面後, 變得好用許多, 不然有類似這種簡單需求時要另寫 templatetag, 實在是多餘又不易懂。相關心得見之前寫的文章。實在是換掉也為難, 不換也為難。

關於 ORM

先來個免責聲明, 若是需要頻繁地寫多個不同的小型網站, 用 ORM 是利多於弊, 可減少重覆的程式碼。以下的論點基於我的個人經驗, 需求是長期維護一個資料量大且有嚴苛速度需求的網站。

之前已寫過幾篇 ORM 心得, 在《Django 和 Python 操作 database 時的額外負擔》提到實測大量數據的情況有多慢, 注意我的使用情境有超過百萬筆資料, 若資料量沒那麼大, 這個負擔較無所謂。

《撰寫資料庫相關程式的心得》有提到使用 ORM 的成本。這裡要補充的是, 無論如何, 我們都需要分開「資料庫的操作」和「邏輯操作」。而在程式裡直接使用 ORM 並沒有隔離好兩者。

舉例來說, 在頁面裡使用 get_user("fcamel") 會比 User.objects.get(name="fcamel") 來得好。想想要如何針對這個頁面寫 unit test, 會發覺後者仍和 database 有很大的相依性, 不容易 mock。或著換個說法, get_user() 的抽象程度比直接使用 ORM 高。get_user() 裡是使用 ORM 還是下 raw SQL, 都和使用者無關。當需要連續幾個 ORM 操作以達成一個目的時, 另外包函式的優勢會更明顯。

若能接受上面的論點的話, 會發覺 ORM 的優勢又少了一點。所以, 我個人的看法是: 重點是必須另外包一層 API 存取資料, 供邏輯操作使用。所以, 對前端開發者來說, 是否使用 ORM, 影響不大。但對後端開發者來說, ORM 的缺點遠大於優點。ORM 最吸引人的地方是提供不錯介面隔離 database, 可以有彈性地存取資料並且不用太了解如何寫 SQL。但隨著使用經驗漸增, 會發覺這些優點並非事實。但若一直不去了解 database, 不會發覺付出的隱性成本。

題外話, 聽說 SQLAlchemy 相當強大, 不知實際用起來效果如何。我後來淡化使用 ORM 的場合, 加上需要使用 South (基於 Django ORM 的 plugin), 不方便換掉, 就沒有研究 SQLAlchemy 了。

關於 test

內建的 django.test 相關模組不太好用, 而且執行 test 要先重建全部 table, 然後在每個 test case 前 truncate 全部 table, 效率不好。若能指定只 truncate 需要的 table, 可省下許多時間。通常 test case 是愈寫愈多, 實務上執行 test case 的效率相當重要。

使用 South 後更會有 production schema 和 test schema 不同的問題, 因為 production 用 South 建 schema, 中間可能用到客制化的 SQL 改 schema (如建 multiple column indexes), 但 django.test 不會呼叫 South, 而是用 Django 原本讀 models.py 建 schema 的方式。我後來改用自己寫的模組來建 test database, 沒研究後續發展, 不知後來是否有修正。

關於 coding style

Django 違反許多 PEP 8 的規則或 Python 精神, 像是:

  • 在程式碼裡面 import 別的 module (而非開頭), 並且有 circular import。
  • 有多種方法做一件事。我很討厭這點, 像自訂 login 的重導頁面卻沒成功, 除錯時很麻煩, 要搞清楚多種規則的執行順序, 才知道問題出在那。
  • 有許多 lazy initialization。我不確定這是否違反 Python 社群的 "explicit is better than implicit", 我個人不喜歡一堆 lazy initialization, 很難掌握程式的行為。

以上這些事對使用者有什麼影響? 當行為不合預期, 文件也看不出所以然時, 讀原始碼並加 log message 是滿有效率的除錯方法。上述都是我在研究功能 (像是如何使用 cache) 或除錯時, 讀原始碼遇到的困擾。

結語

除前面一再強調的「結論」外, 再多強調一下, 一個東西愈多人罵, 表示愈多人用, 出事也愈好處理。本篇不是建議別用 Django, 也不是反串推廣, 只是之前一年多的使用心得。

2011年9月18日 星期日

Problem Solving 的技巧 (3):因事制宜

不知不覺,這類文章可以寫到第三篇,感到頗意外的。前兩篇是:

昨天看到 TonyQ 在 Soft_Job 寫的文章:《Re: [閒聊] 你在開發程式時,是重視績效還是品質。覺得深有同感,摘錄幾段如下:

對我們來講很多其實可以是 nice to have 的東西, 都會被我們當成 must have。

這個判斷是經驗跟 domain 累積下來的,沒有公式,沒有法則, 做久了你就是會知道什麼架構之後會一直噴 exception ,

而且你還會知道等他出事時你一定沒辦法好好處理, 所以你要在這個當下把它處理掉。

...

觀察他們影響到哪些地方,你有沒有能力測試他們, 還有你的環境允不允許這件事情帶來的不穩定性。

一般來講,如果是我個人自己的專案,我非常不介意大改, 只要我後續還有時間可以處理這些出來的問題。

對公司或者客戶的專案,我會採取相對保守的態度。

這是對風險管理的策略問題。

在 Plurk 上貼了這篇文章後,Thinkerqrtt1 提出另一面看法,強調「早期發現,早期治療」的好處。於是決定借題寫一下自己的看法。

在討論這個議題前,我想先強調,為了方便聚焦討論,我們往往會先偋除一些條件,或是先依自己的假設開始論述。而有爭論的地方,往往卻是這些隱藏的前提。好比說「寫測試碼重不重要?」單單這樣一個命題,只能討論出很模糊的概念,不論支持與否,看起來都有些道理。但加入一些條件後,像是「這是長期開發並有多人參與的專案」或是「後天要交的雛型」,相信大家對此會有不同的答案,也會少一些分歧。相關的想法,可以參考《黑天鵝效應》這篇有摘要讀書心得,其中敘事謬誤和戲局謬誤啟發我這個觀點。

我的想法是,在達到必要需求的前提下,依個人以及團隊的能力,看看能提昇多少品質。問題在於,何謂 must have、何謂 nice to have,每個人的見解不同。爭議點在於,每個人的能力不同,導致評估的實作成本不同。一樣是 nice to have,有些被認同可以做,有些則否。

( ps. 這篇文章不討論規格不明確的問題,這是另一個大議題。 )

舉例來說,A 認為現在重構只能提升一點品質,卻要花兩天;B 認同 A 評估的品質,但 B 覺得只要花半天。所以 A 覺得不划算,而 B 持相反意見。從這樣的評估結果很難說 A 或 B 誰對誰錯,也許 A 沒重構經驗不信任重構,也可能 A 有豐富的經驗,估得比 B 準。

在體認到個人能力不同、習慣不同的前提下,對自己的要求是盡量提高自己的能力,多付出時間提高品質,減少日後維護成本並能練功,形成正向循環對其他人來說,看對方是那種人,是「過」或「不及」,再從另一個角度和對方討論。

舉例來說,若團隊成員不熟或不認同 unit test。當下就要出貨了,這時硬要大家學習並實作測試,並不適合 (不如當「負面教材」,待下次專案的開頭提出討論)。但若成員有過相關經驗,同樣時程下,就能針對核心程式補些測試,花最小成本減少最多風險。當成員嘗到測試甜頭,不小心寫過多測試碼時,和對方討論優先順序,減少寫 C/P 值低的測試碼。

回頭看品質的問題,能力愈強、經驗愈豐富,增加 nice to have 的成本愈低,自然能前期處理,減少技術債。反之,在時程的考量下 (ship or die),能力和經驗不夠時,得選擇先放過一些潛在問題,日後仍有需求時,再花更多成本補救。先活下去,才有更多的本錢來還債。

因事制宜說來簡單,只有四個字而已。實務上卻需經年累月的經驗,不止要考量時程、未來需求變化等項目,也要留意每個人的能力和習性,才能在當下找到較為適當的平衡點。而增加經驗的方法,如同杜書伍在《打造將才基因》裡所言,除了比別人投入更多時間外,沒有其它的捷徑。

2011-09-25 更新

看到 Thinker 提了他的相關看法:《程式碼要清的多乾淨?》,裡面提了不錯的建議:

除了能力不同之外,個人的膽量、積極態度和價值觀也同樣影嚮著評估的差異。 我強調的是,積極態度和個人的能力呈正相關。
...
要評估自己的能力,並適度的承受犯錯的風險。
勇於嘗試、有控制性地犯錯,對於學習很有幫助。我覺得在我學習的前一大段生涯裡,過於謹慎而不敢犯錯,以致於學習速度較為緩慢。而我認識一些晚起步、卻成長相當快的朋友,都具有大膽嘗試、不斷犯錯的特質。他們快速地累積經驗,並培養出更多的膽識和更大的企圖心。

2011年8月7日 星期日

Python 的特別之處 (1)

從新手的眼中來看 Python,比較能看出 Python 和其它語言不同之處。最近有機會幫別人快速上手 Python,就順便整理一下我從中發覺 Python 較為突出的優點。

list、dictionary and string

平時 coding 最常用到的 container 就是 list 和 dictionary,另外也會常用到字串操作,Python 提供方便的方法來操作它們。string 可看成一個有實作 list 介面的類別,一些常用操作像是 slice:"abcd"[1:3] 回傳 "bc";負數的索引: "abcd"[-1] 回傳 "d";直接和 for-loop 整合在一起:

In [1]: for ch in "abcd":
  ....:         print ch
  ....:
a
b
c
d

讓存取這些常用資料型態輕鬆許多。

iterator

使用 iterator 比傳統的 for (i=0; i<n; i++) 來得清楚,Python 針對 iterator 下了不少工夫,提供好用的輔助函式,像是 enumerate 補足需要用到 index 的情況:

In [2]: for i, n in enumerate([1, 3, 5]):
  ....:     print i, n
  ....:
0 1
1 3
2 5

使用 zip 整合多個 list:

In [3]: names = ["John", "Marry", "Tom"]
In [4]: sexes = ["Male", "Female", "Male"]
In [5]: for name, sex in zip(names, sexes):
  ....:     print name, sex
  ....:
John Male
Marry Female
Tom Male

map, filter and reduce

任何使用過 map 的人,都會喜歡 map 輕巧的用法,來看幾個例子:

In [1]: map(int, ["12", "37", "999"])
Out[1]: [12, 37, 999]
In [2]: map(str, [12, 37, 999])
Out[2]: ['12', '37', '999']

int 是一個函式,將傳入的物件轉成整數;str 則是轉成字串。使用 map 可以將一個 iterator 轉為另一種 list。

另一個常見的情境是,從一個 list 裡篩選出需要的物件,比方說只留下偶數:

In [1]: numbers = [1, 2, 3, 4, 5]
In [2]: filter(lambda x: x % 2 == 0, numbers)
Out[2]: [2, 4]

或像 filter(lambda s: s.endswith('.py'), file_names) 只留下結尾為 ".py" 的字串。

除 map 和 filter 的重心放在轉換 list 之外,reduce 則是將 list 匯整成一個物件。有了這些函式,就能任意的操作 list,用以匯整或擴散資料容器。

比方說將一串數字加起來:

In [1]: numbers = [1, 2, 3, 4, 5]
In [2]: reduce(lambda x, y: x + y, numbers, 0)
Out[2]: 15

上面這個例子可以用內建的 sum 取代,來看另一個複雜點的例子,將一串 0、1 值合成一個整數:

In [1]: bits = [0, 1, 0, 0, 1]  # bits[i] 的值表示 2^i 的系數

In [2]: reduce(lambda x, (i, b): x | (b << i), enumerate(bits), 0)
Out[2]: 18

list comprehension

map 和 filter 雖然方便,要用到 lambda 或是混合使用時就沒那麼好讀了。Python 提供一個重量級的武器 list comprehension 來解決這問題。比方說留下偶數並乘以三再加一:

In [1]: numbers = [1, 2, 3, 4, 5]

In [2]: [n * 3 + 1 for n in numbers if n % 2 == 0]
Out[2]: [7, 13]

綜合以上的語法,可以輕鬆地寫出易懂的 quick sort

def qsort(numbers):
    if len(numbers) <= 1:
        return numbers                                                                                     
    pivot = numbers[0]
    rest = numbers[1:]
    smaller = [n for n in rest if n <= pivot]
    larger = [n for n in rest if n > pivot]
    return qsort(smaller) + [pivot] + qsort(larger)

對於習慣 C、C++、Java 世界的人來說,應該不曾看過這麼直覺易懂的 quick sort 吧。

tuple

tuple 是一個很妙的資料結構,它和 list 的主要差別是它是唯讀的,Python 裡鮮少有這種唯讀物件。不過它較易發覺的好處是被用在 Python 的 parallel assignment 和函式傳回值。

於是在 Python 裡可以這麼寫:

a, b = b, a # swap

Python 在看到 b, a 時會產生一個 tuple 表示 (b, a),再透過 tuple 達到 parallel assignment

函式也可以一次「傳回多個結果」:

In [1]: def divide_and_mode(a, b):
   ...:     if b == 0:
   ...:         return None, None
   ...:     return a / b, a % b
   ...:

In [2]: divide_and_mode(7, 3)
Out[2]: (2, 1)

In [3]: a, b = divide_and_mode(7, 3)

In [4]: a
Out[4]: 2

In [5]: b
Out[5]: 1

原理一樣是先轉成 tuple 再傳回,再視等號左側放什麼,決定要存成 tuple 或做 parallel assignment

2012-01-25 更新

應該沒什麼力氣更新續篇,在這裡簡短描述一下,有興趣的人可以找看看相關介紹。

with

在 Python 2.6 後,支援用 with 管理資源。像讀檔案可以用 with 的方式寫:

# 印出所有使用者的 id
with open('/etc/passwd') as fr:
    for line in fr:
        print line.split(':')[0]  

在進入 with 的 block 前,會呼叫 file object 的 __enter__ 以獲得 file descriptor;在離開 block 前會呼叫 __exit__ 關掉 file descriptor。即使中間呼叫了 sys.exit() 或丟出 exception,仍會執行到 __exit__,不用擔心會漏關。方便用在許多情境 (比方說 lock / unlock、自動 flush output buffer),易讀易用。

內建常用函式庫

除上述的基本資料結構和 string 外,還有 sqlitejson等。

簡單不易出錯的語法

舉幾個寫 C 可能發生的問題,但在 Python 的語法下則不會發生:

if (condition);
{
    // BUG!! 這裡的程式一定會被執行
}
if (x < 60)
    number_of_fail++;
    total_fail_score += x; // BUG!! 這行每次都會執行

另外,由於 Python 的 condition 只能是 expression,不能是 assignment。不會有 if (x -= 3 < 0) 這種 bug。

ipython

有 ipython 方便快速試語法、試函式庫還有開發程式。ipython 比簡單的 interactive interpreter 強大許多。

2011年7月9日 星期六

使用 Mockito 更輕鬆地寫 unit test

vgod 提到可以用 PowerMock 來換掉 static、private、final 的方法,還有 Mockito 用起來很順手。於是大概看了一下相關文件:

我花了一些時間看了 Mockito 範例和作者設計的思路,覺得很有意思。 《behind the times: Mocks and Stubs aren't Spies》 提到在寫測試碼時,替換掉實際互動的元件,有四種不同層級的輔助元件: dummy、stub、spy、mock:

  • dummy: 什麼事也不做。
  • stub: 依據輸入傳回物件,藉此控制後續的邏輯。(相對來說) 不在意被呼叫的方式,像是何時被呼叫、呼叫了幾次等。
  • spy: 用來確認該物件如何被使用。比方說呼叫 cursor 物件的 commit() 前有沒有先呼叫 execute()。
  • mock: 同stub + spy,既需要傳回物件供待測方法使用,也在意它如何被呼叫。

寫測試碼常遇到的困擾是:mock 太囉唆了。這篇提供一個小例子對照 MockitoEasyMock 的差異。《should I worry about the unexpected?》 解釋 mock 之所以囉唆,是因為它管太多了,導致加新功能時,常常行為沒錯,卻無法通過舊的測試。使用 spy (Mockito 的主要功能) 就不會有這種困擾。大多情況我們並不在意是否每一個物件都要如預期般執行,只要關鍵的幾個步驟沒錯即可。

之前會排斥使用 mock 的原因為:

  1. 沒有和真正的物件互動,不夠踏實。
  2. 寫起來很囉唆。
  3. 囉唆就算了,稍微改改程式還很容易出錯。

第一點是 trade-off,有時候是不得不做的必要之惡。在看到 spy 的概念後,發覺它少了後兩項缺點。之後需要用到「mock」時,再來用 Mockito 看看。Mockito 另一個好處是,它有提供各種語言的版本,可以學一套語法走天下。

2011年6月2日 星期四

ego-post!! 即時同步編寫 wiki code 和顯示 html 畫面

Ego

對一個時常寫 blog 的人來說, 好的編輯介面相當重要。以前寫過 td-post 以節省重覆輸入同樣連結的時間。但 td-post 有個問題, 每次改完文章又要重執行一次。我常常修來修去, 用起來有點彆手。後來偷懶, 就改用 blogger 的編輯介面。

但 blogger 的所見即所得並不太準, 有時會包含 div 有時不會; 有時會多空行; 複製網路上的文字時, 常會不小心連帶複製到文字的格式, 之後不方便修改。於是得切到 blogger 的 raw html 編輯介面修正這類問題, 再切回來繼續寫, 還挺麻煩的。

去年 Scott 和強者學弟 Will 幫高中學弟妹寫了個 on-line judge。server 端用 Google App, client 端網頁裡嵌了 telnet client, 連到綁好的 local server (透過 QEMU 執行), 用來即時開發程式、編輯和提交程式。並有一個所見即所得的 wiki 編輯器, 用來編題庫。整個工程相當驚人 (或著該說是.......吃飽太閒)!

整個專案有不少地方值得深入玩玩, 而我最有興趣的, 是所見即所得的 wiki 編輯器。畢竟, 用 putty 連到 VirtulBox 裡的 Ubuntu 開發還是方便許多。但 wiki 編輯器卻沒其它替代品。

昨天晚上錯過早睡的時機, 自暴自棄地開始拼湊程式, 今天晚上再改一改就有個不錯的雛型。寫好的東西放在這裡, 有興趣的人可以看看。如同大家所猜, 這篇文章是用 ego-post 打出來的。剛好前陣子在看《料理鼠王》練英文, 就順便用 Ego 命名了。

待做事項
  • 提供新的語法用來貼程式碼。

2011-06-02 更新: 有圖有真相, 附上 screenshot:


2011-07-09 更新: 補上自動記錄 link 和發文到 blogger 的功能, 並在 project 首頁補上如何執行。
2012-08-07 更新: 隔了一年多, 終於受不了補上 auto save 的功能。

2011年4月2日 星期六

Problem Solving 的技巧 (2):別把解法當作問題定義

《真正的問題是什麼?你想通了嗎?》裡提到一點重要的觀念,別把別人的解法當作問題。最近累積了不少實例,比較能清楚地表達這個想法。

「把解法當作問題」的意思以及它帶來的負面影響

在分配工作時,為了減少溝通成本,常會隱藏一些訊息,只告訴合作同伴他「應該」知道的事,而省略了原始的動機和考量。問題在於,實際執行的人通常比對方清楚細節,很多問題在執行後才會浮現。但分配到工作的人不清楚前因後果,即使查覺不對勁,也無法 (或不願) 做出進一步修正。

好一點的情況,在分工前會先進行討論。然而,討論常會陷入各執一詞的局面,討論雙方作法優缺點後,會發覺找不到共識。實際上這只是眾多解法的兩個提案而已,若深入討論雙方作法背後的動機,各自預先假定的前提,會發覺尋找共識並不困難,畢竟,雙方是在解同一個問題。

在需求一樣,假設一致的情況下,兩個頭腦清楚願意討論的人,沒道理找不到共識,只是我們習慣省略前提,也沒先釐清需求,直接討論解法,才會覺得有個看不見的牆擋在中間,對話難以有交集,我不認同你認同的點,你也不認同我認同的點。

當我開始不預設立場後,看清楚很多事,我第一件事不是評斷大家作法的好壞,而是先問這樣做的考量為何?從考量的點會問出需求,從需求和考量的點會發覺矛盾,從而發現隱藏的假設。接著就能討論假設是否成立,或是不成立又會如何?很可能所有的點會重新洗牌,然後聚焦出更清楚的需求。

按部就班從需求討論各種作法,尋找大家認同的假設,接著在假設下收斂可能的作法,或是列出有影響但還不確定的點,然後評估何時由誰來釐清不確定的點,再來做更細的決定。這樣做下來,通常會有大家都滿意的成果,互動的過程中也會相處愉快。

以開發網站為例

舉例來說,A、B 要合寫一個網站,A 想用 PHP 直接寫,因為他覺得大家都會寫 PHP,寫起來也快;B 想用 Python + Django,因為他覺得日後比較好維護。

A 和 B 的想法都沒錯,但是一討論就陷入死結,這是很吊詭的事,兩個人想法都對,也在做同一件事,為什麼不會有共識?

這表示上述的論點一定有其中一點是錯的,仔細思考會發覺,其實 A 和 B 「沒有在討論同一個問題」,也沒有在同樣的立足點 (假設) 上討論。

  • A 可能假設時程很趕,沒有時間慢慢從頭學 Django,若他不會 Python,還得多學 Python,而且他可能不想學新東西,覺得 PHP 用起來沒什麼問題,為什麼要自找麻煩用別的作法?
  • B 可能假設學習 Python + Django 很簡單,即使時程很趕也還好,或是時程可以再談,沒有人說一定要什麼時候出來,在 B 認為可以來得及做完的前提下,B 認為要認真考量維護的事。

從 A 和 B 隱藏的假設可看出一些衝突:學習新東西花的時間成本、對於時程的認知、是否需要重視維護。而這些假設大概和 A、B 各自過去的經驗有關,比方說 B 收拾過用 PHP 開發的爛攤子,A 沒有和多人共同開發過稍具規模的專案。若 A、B 能和對方說明自己的前提,就有機會順著前提再討論為何有這樣的前提,彼此較能接受對方的看法。

然後再對照目前的需求,會發覺有些前提不再成立,或是更為重要。像是 A 當初收拾的爛攤子是別的因素造成的,可能是時程太趕或開發者習慣不好,主因不是 PHP。

將這些事都攤出來討論後,會發覺幾個立基不牢靠的推論:

  • 為了方便維護,要用 Python + Django。
  • 為了快速開發,要用 PHP。

這是將需求和解法綁在一起討論,但是實際的情況是,方便維護的作法不只有 Python + Django,快速開發的作法也不只有 PHP。視開發人員的經驗,將兩者反過來陳述也可能成立。

所以,真正考量的點應該是:

  • 先確定時程和功能需求,才能判斷開發速度要多快。
  • 確定日後是否需要擴充,擴充的程度和時程,從而判斷維護的成本。

然後列出相關選項,需注意的是,要依現有人員的能力列,而不是「一般」的認知。若時程不是太緊,可以先排時間評估各種方案,像是 Python 不只有 Django 一家 framework,也許可以考慮 Flasky 或 Pyramid;PHP 也不是沒 framework,若願意犠牲多一點執行速度,可以研究 PHP 的 framework;或是另找別的路,用 Ruby on Rails 或用 Java 體系的解法。

要注意的是,過猶不及,當考量的點過於發散時,不確定的點太多,沒有心力一一確認,討論會流於空泛。這時要列好需求,依現有人員的能力做些假設,先達成局部共識再繼續討論,比較容易聚焦。像是因為內部人員最熟 PHP 和 Python,用這兩者風險較低,所以只考慮這兩個體系的解法。

結語

類似的例子很常發生在日常生活的討論裡,不限於技術方面。隱藏的前提不見得都是技術議題,人性是很複雜的。關鍵在於別把別人提的解法當問題,自己也別預設立場著重在說明解法,而沒說明問題需求和自己的假設 (前提)。

在討論過於發散時,要先取得共識排除一些考量。即使共識的原因只是「不為什麼,我們都覺得不重要」,也是不錯的作法。可以先聚焦往後討論,待討論到後面有更明確的想法,或是再和第三個人討論時,可能會查覺原本的前提有問題,這時再回頭修正它們,重新沙盤推演一遍,得到更確實的解法。有了紮實的共識,清楚各項決策的前因後果後,之後遇到各種變動,都能迅速明確地處理。

2011年2月19日 星期六

DRY 的缺點以及測試碼的衝突

這篇是看到 Pylons (Pyramid) 的 《Unit Testing Guidelines》 後寫的心得。

以前我覺得每件事都有標準答案,或是所謂的「Best practice」。後來才發覺這是很嚴重的錯誤認知。因為希望能簡單地處理事情,而一廂情願地認定有「標準答案」,結果忽略了許多反面的訊息。《百人百觀》系列裡道出我的心態轉變。

以 DRY (Don’t Repeat Yourself) 來說,這是資訊人奉為終旨的鐵則,可應用到各種情境。這裡我們先縮小範圍,討論 DRY 對於寫程式的影響。它的優點顯而易見,只需要改一處程式,不會因漏改程式而產生 bug。重覆的程式碼容易造成 bug,複製貼上是常見的主因,甚至有 paper (CP-Miner) 提出方法自動偵測這種 bug。

但是 DRY 的缺點呢?造成 client codes 之間的相依性,迫使所有 client codes 共用同一介面,這帶來不少問題:

  • 寫錯共享程式時,影響不止一份程式。
  • 最簡單的情境得配合最複雜的情境使用,增加簡單情境的維護成本。即使介面設計的很完善,不需更改呼叫方式,執行時勢必多了一些檢查手續,或在空間上做了些妥協,提高時間和空間的成本,各種 framework 是最好的例子。
  • 承上,像 Django 的 session 為了能存各種 object,選擇以 dict 表示 session,直接序列化 session 物件存到資料庫或檔案裡。為了簡化實作並提供無限的空間存 session 資料,用 MySQL 時選擇用 LONGTEXT 以儲存無限制大小的資料,造成每次取資料都要從 disk 讀。在大量使用者連入的時候,這會是個問題。
  • 變更一處 client codes 的需求,可能會影響共享程式的介面。選擇相下向容的話,介面會變複雜,可能會多一些選擇性參數。邏輯變複雜,共享程式容易寫錯,client code 使用方式也變複雜。
  • 承上,選擇改變介面的話,需找出影響到的 client codes。對 dynamic typing 的語言來說,這是件苦差事,甚至無法 100% 保證沒有遺漏。

如同《Problem Solving 的技巧》裡說的,每個方法都有帶來的好處,也有帶來的壞處,也有針對壞處所做的後續修補。關鍵在於弄清楚現在的需求,明白各項設計的優缺點,配套作出一連串的設計,以獲得整體的最大效益。比方說用 VCS 切 branch 可以減少介面相容問題,不過會多出維護 branch 的成本,那是另一個議題了。

自從意識到 DRY 帶來的成本後,我覺得有些困惑,因為它不再是 100% 正確、用了一定好的原則。在寫測試碼時,我感到更困惑,若測試碼也變複雜,之間有相依性,那誰來保證測試碼是正確的?更何況一個具有完備測試碼的專案,測試碼和產品碼的比例將近 1:1,在量如此大的情況下,測試碼的邏輯太複雜的話,測試碼容易出錯,會造成不少問題。我體驗過測試碼寫太複雜而造成測試碼有錯,因測試碼出錯而誤以為產品碼有錯,結果費了更多力氣才找出錯誤 (程式碼變兩倍)。也體驗過在 setUp 或其它初始化部份出錯,造成訊息混亂,無法掌握錯誤的源頭。後來就不知不覺地將測試碼寫得很簡單,也漸漸減少犯這些錯的機會。

昨天看到 Pylons (Pyramid) 的 《Unit Testing Guidelines》後,才串起過去的經驗,發覺問題的源頭在於 DRY 並不適合用在測試碼,但是 DRY 已成為根深蒂固的習慣,壓根兒就不會想到將重覆程式碼抽出整理成跨 method / class / module 的行為,反而是妨礙測試碼品質的元兇。該篇文章有精闢的說明和例子,推薦大家參考。其中有些規則,現在還不能掌握使用後的優缺點,之後再抽時間讀讀 Pyramid 的原始碼,應該能學到一些東西。

2011年1月8日 星期六

撰寫資料庫相關程式的心得

我是用 MySQL + Django,處理的資料量有小有大。資料量大的情況下,通常有上萬筆,甚至會到上億筆。相關心得大概分成四類,依實務經驗記錄一下心得。

是否應該使用 ORM?

我是使用 Django ORM,以下指的 ORM 問題可能不適用全部 ORM framework,但我猜大部份應該是半斤八兩。

剛開始不熟 SQL 時,很喜歡用 ORM,ORM 有些學習門檻,不過習慣後用起來相當順,也容易閱讀程式。但是在好寫好讀的背後,卻犠牲掉極大的效率。原因有幾點:

  • 需要大概了解 ORM 產生的 SQL,才知道如何寫出有效率的操作。比方說用到 foreign key 時,可以在取物件時順便 join。若沒特別處理,預設行為是參考到關聯物件才取資料,於是取一萬個物件並讀取它們的關聯欄位,就會多下一萬次 SQL。
  • 即使了解 ORM 各項操作避免一些地雷寫法,ORM 不見得能產生最快的操作方式。明顯的缺點是讀寫 N 個物件時,很可能會轉成 N 次 SQL,而不是一次。
  • ORM 為了提供一致的抽象介面,沒有支援各家 DBMS 完整的語法,減少一些最佳化的機會。如缺少批次操作,以及使用 force index、決定 join order、技巧性地用 IN 不用 range query 等。
  • 即使 SQL 沒有問題,產生 object 的時間成本比自己執行 SQL 取資料來得高 (見這篇),資料量大的時候會變成瓶頸。

我一開始寫的專案全用 ORM。第二個寫的大部份用 ORM 但遇到一堆難解的效率問題。第三個寫的開始刻意減少 ORM 操作。最後則是全面禁用 ORM。原因很簡單,弄懂 ORM 操作並做最佳化的時間,比直接寫函式封裝 SQL 操作多,而且最後達成的效率又較差。除此之外,複雜的 ORM 操作可能有 bug 或是令人誤會,導致取出不對的資料,看 ORM 產生的 SQL 才明白問題出在那。

愈懂 MySQL 後,愈覺得 ORM 不順手,最後就改成寫模組封裝 SQL 操作。結論是,若有意願硬啃 DBMS 相關知識的話,將時間投資在所用的 DBMS 上,會比學習 ORM 操作和理解背後運作方式划算。

也許有人會質疑不用 ORM 會增加換 DBMS 的成本,我沒這樣的經驗不清楚用了 ORM 能省下多少成本,相較於前述的問題,整體來說是否划算。至少我會選擇先專精一個 DBMS,還有自己寫模組隔離應用層邏輯和資料庫操作,減低轉換 DBMS 的成本。

Database migration tool

雖然我上面將 Django ORM 說得很慘,但是用 Django ORM 搭配 South 到是滿不錯的。South 是 Django 的 migration tool,提供一個框架維護資料庫的變動,並且可以偵測 Django model 的變化,產生對應改變 schema 的操作。在說明 South 的優點前,要先談談為何需要用 database migration tool。

使用 database migration tool 有兩個好處:

  • 記錄目前這版程式用的 database schema。既然程式碼需要版本記錄,database schema 當然也要一併記錄,才能確保每版都能正常運作。
  • 方便其他組員更新資料庫。更新程式碼後執行 database migration,就能擁有和其他人同步的資料庫。

當然,有很多方式可以達成以上目的,像是每次更新 schema 就 dump schema,並存成一個 SQL 檔存在 VCS 裡。我沒這樣做過,不知會有什麼大問題。目前只想到幾個小問題:不方便多人同時修改 schema,之後要 merge schema 可能會比較麻煩,特別是改到同一 table 時。不方便追踪 schema 各步的轉換,像是加入 table A、B、C 以支援功能 X。但是回頭翻 VCS log 似乎也能滿足這個需求。唯一無解的大概是有些情境拆開 schema 執行會比較有效率。像是先建好 table、填完實體資料後,再建 covering index。

若改成維護多個 SQL 檔,第一個起始 SQL 產生基本 table,後面的 SQL 都是「schema diff」,則方便多人同時開發。但要人工產生 schema diff 有點辛苦,沒記好每個操作,手動改完 table 後,要回頭比對差別才能寫出 schema diff,容易出錯並增加確認的成本。

除了滿足基本需求外,South 另外提供下列功能:

  • 提供單線前進的 migration 方式。能在各版本之間前進、後退。
  • migration 分成 schema migration 和 data migration。並且提供偵測 Django model 變化自動產生 schema migration 的程式碼。data migration 只是空殼,由工程師自己填程式碼。
  • 提供修改 schema 的 API,像是加減欄位、加減 index。

使用 South 的額外好處是,可以避開 Django model 的限制,像是不支援多欄 index、不支援使用不同的 MySQL Engine。用 South 的話,只要自己在 schema migration 裡用 alter table 修改即可。

不清楚別家 database migration tool 怎麼運做的,感覺這方面的工具有很大的發揮空間,值得了解一下各家工具提供的功能。目前遇到的最大困擾是,無法明確看出那些 migration 有相依關係,更新資料或程式時,不方便只執行有影響到的範圍,若更新在很前期的 migration,就得回溯到前面再重跑。

包工具箱

將資料庫操作和應用層邏輯分離的好處應該不用多說,使用統一的介面有其它好處,目前覺得最實用的是可以寫 try catch 自動記下所有出錯的 SQL,再依參數決定要吃掉 exception 或丟回應用層。由於出錯的 SQL 都有被 log,程式出錯時可以馬上找到有問題的 SQL,縮短除錯時間。

單元測試

我原本是用 Django 內建的方式重建測試資料庫,但是最近開始用 multiple database 後,遇到一些問題。由於我在 South 裡做了一些不合 Django 規定的操作,不想花時間理解 Django model 和 test 詳細的運作方式,最後決定自己寫簡單的模組來建置測試環境,速度也會比較快。還在小規模的試用中,看看之後能不能投多點時間打穩這塊,再來寫心得。