tag:blogger.com,1999:blog-4510514888913359232024-03-08T19:34:32.937+08:00fcamel's blog「有些人說:『你怎麼能夠活著而無知?』我不知道他們是什麼意思。我從來都活者,也從來都很無知。那容易得很。我想知道的是你如何能什麼都知道。」 -- 理查 · 費曼fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.comBlogger479125tag:blogger.com,1999:blog-451051488891335923.post-90422804306002427342014-05-10T14:49:00.000+08:002014-05-10T17:43:12.768+08:00從不同程式語言的異同思考問題的本質<p><a href="http://fcamel-fc.blogspot.tw/2009/07/blog-post_22.html">《學程式語言的樂趣》</a>描述我早期學程式語言的事,後來幾年多了一些心得 ( Python, Haskell, C++, Objective-C ),之後有機會再補上。這裡想寫的是藉由學習不同程式語言後明白的事。
</p><p>
學特別的程式語言有助於掌握不同的概念。我最初慣用的語言是 C (高中三年),再來是 Java (大學四年)。藉由 Java 開始接觸 OOP 的思維,除了程式語法之外,學到更多的是寫程式的思考方式。其實回頭再去看 C,也可以使用一樣的思維,只是當語言直接綁入思維時,會被迫這麼做。就像用 Haskell 必須寫出 stateless 的程式,就會更明確地留意 state。不過學 Java 的時候還沒察覺到這點。
</p><p>
後來長時間地陸續用了 Ruby、Python、C++,還有加減寫 JavaScript。開始掌握到一些程式語言背後共同的特性:組合邏輯控制,以及管理資料的狀態。回歸到最原始的電路來看,程式的運作就是邏輯控制 (and/or) 和狀態 (on/off)。當要做的事變複雜時,必須結構性地管理它們。
</p><p>
舉例來說:</p><ul>
<li> Java 定了 class 和 interface 表示「資料+實作邏輯」和「操作介面」,class 內又有 public/(default)/protected/private 限制資料的存取範圍。還有 package 封裝同一群 class。</li>
<li> Python 表現的比較直接,容易連結回 C 該如何表現這些思維。每個 method 第一個參數一定是物件本身,慣例命名為 self。'_' 開頭的 function 和 method 表示只限於內部使用。用 module 封裝同一群 class。</li>
<li> C++ 藉由不同的方式使用 virtual,class 可當作 "class" 或當作 "interface",或帶有實作的 "interface"。class 有 public/protected/private 限制資料的存取範圍,但又有 friend 可讓指定的 class 存取無視這些限制。有 namespace 封裝同一群 class,還有 anonymous namespace 可以嚴格的封裝實作細節。</li>
</ul><p>
不同的語言有各自擅長的地方,學習別的語言的時候,會想要借鏡別的語言的特色,運用到原本常用的語言裡。但是接觸過 Scheme 和 Haskell 再回頭嘗試用在 Python 和 Java 的時候,遇到很大的問題:</p><ul>
<li> 無法直覺地使用天生不支援的功能。比方說 Java 的 function 不是 first-class function,得用物件包一層,再自成一套體系。例如用 <a href="https://code.google.com/p/functionaljava/">functionaljava</a>。</li>
<li> 和原本語言本身的風格不合,增加其他人的維護成本。</li>
</ul><p>
第二點是比較大的問題。自己用了一套「functional Java」的寫法,其他人也得學習如何解讀。以 Python 舉例,下面是同一件事用三種不同寫法:
</p><p>
<strong>for-loop</strong></p><pre class="prettyprint">numbers = []
for i in range(0, 10):
numbers.append(i * 2)
</pre><p>
<strong>list comprehension</strong></p><pre class="prettyprint">numbers = [i * 2 for i in range(0, 10)]
</pre><p>
<strong>map</strong></p><pre class="prettyprint">numbers = map(lambda i: i * 2, range(0, 10))
</pre><p>
對沒學過 Python 的人來說,for-loop 比較直覺,沒學過 Python 也可推敲出意思;初看 list comprehension 不明白這是什麼,不過花點時間看幾個例子就懂了。map 比較困難,得有 functional programming (FP) 的概念才能明白。
</p><p>
Python 本來就有這些語法,我們不會責怪這樣寫的人。但用在 Java 裡就有爭議了,為什麼要另外包一套框架,寫出要別人花額外工夫才能讀懂的程式?
</p><p>
後來我察覺到更深層的思維:OOP 也好、FP 也好,目的都是結構性地管理資料和邏輯,只是表達的方式不同。掌握到這點後,我將時間花在有助於真正目的事上,而不是寫得更 OOP 或 FP。原本遇到的困擾 (如何在 B 語言裡善用 A 語言的特性) 自然就不存在了。
</p><p>
舉例來說:</p><ul>
<li> 為什麼 FP 要強調 immutable,還有習慣用 map? 因為 immutable 的資料沒有 state,自然沒有被誤改的風險。有沒有用 FP 的 map 到不是重點,map 只是方便套用一套控制邏輯產生另一批 immutable 資料。是實作層級的小幫手,不是設計準則。</li>
<li> 為什麼 OOP 強調 <a href="http://pragprog.com/articles/tell-dont-ask">Tell, Don't Ask</a>? 也是為了減少外漏 state。即使用沒有 OOP 的語言,也可這麼做。像 iOS 的 <a href="https://developer.apple.com/library/ios/documentation/graphicsimaging/conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-TPXREF101">Quartz 2D</a>,API 需要用的 <a href="https://developer.apple.com/library/ios/documentation/graphicsimaging/reference/CGContext/Reference/reference.html">CGContextRef</a> 是 struct CGContext* 的 typedef,但是使用者不知道 struct CGContext 是什麼,只知道呼叫函式時要傳入 <a href="https://developer.apple.com/library/ios/documentation/graphicsimaging/reference/CGContext/Reference/reference.html">CGContextRef</a>。<a href="https://developer.apple.com/library/ios/documentation/graphicsimaging/conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-TPXREF101">Quartz 2D</a> 封裝需要的 state 在 struct CGContext 內,隨時可改變它的結構。</li>
<li> 為了方便模組之間獨立開發,不用擔心日後改寫模組內部的實作造成外部程式出錯,最重要的是定出乾淨的介面,讓別人不會誤用。再來才是善用語言的特性讓別人想誤用也不行。例如 C++ 的 anonymous namespace 是隱藏實作的好方法;Java 的 package + (default) class 也是好方法;Python ... 只能用底線命名然後祈禱不要有人亂搞。</li>
</ul><p>
從問題的本質 (管理資料和邏輯) 來看,自然會明白這些設計準則,還有區分出設計和實作 (準則和表現方式)。剩下的是衍生想法,像是管理 state 方法的優先順序: </p><ul>
<li> 沒有 member field (沒 state 最好!)</li>
<li> 有 immutable member field (非得有 state 不可的話,至少讓它不可修改)</li>
<li> private mutable member field (非改不可的話,盡可能減少能改它的程式碼)</li>
<li> ...</li>
</ul><p>
重點還是掌握問題的本質,避免迷失在解決的方案之中。
</p>
<h4>結語</h4><p>總結來說,一套程式語言有一套思維,試過多套不同特性的語言後,藉由它們之間的同異處,可以察覺這些語言想解決的問題是什麼。繼而明白真正的問題是什麼,再回饋到解決問題的思維中,避免落入特定解決模式裡 (如道地的 OOP 或 FP)。起初學不同語言只是覺得好玩,誤打誤撞得到這樣的體會,滿有意思的。
</p><h4>相關文章</h4><ul><li> <a href="http://fcamel-fc.blogspot.tw/2012/08/blog-post.html">從需求出發理解背後技術的思考脈胳</a></li>
<li> <a href="http://fcamel-fc.blogspot.tw/2012/02/blog-post.html">專注於滿足需求而非工具或方法</a></li>
<li> <a href="http://fcamel-fc.blogspot.tw/2011/04/problem-solving-2_6909.html">Problem Solving 的技巧 (2):別把解法當作問題定義</a></li></ul>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-23547769205117673372014-03-02T18:32:00.000+08:002014-03-02T20:35:22.425+08:00事情的另一面: 測試與除錯技巧<p>回顧過去數年的經驗,最大的體悟除<a href="http://fcamel-fc.blogspot.tw/2012/08/blog-post.html">《從需求出發理解背後技術的思考脈胳》</a>以及<a href="http://fcamel-fc.blogspot.tw/2012/02/blog-post.html">《專注於滿足需求而非工具或方法》</a>之外,大概就是沒有所謂的鐵則,許多事情都有另一面,端看你處於什麼情境、從什麼角度看它。
</p><p>
上份工作<a href="http://fcamel-fc.blogspot.tw/2010/03/machine-learning.html">以 machine learning 為主</a> (2008 <span class="escaped">~</span> 2011),針對一個特定的應用問題,實驗不同的演算法,看看能否實際運用在產品上。結果不如預期時,會陷入不知該懷疑演算法、演算法參數、還是程式寫錯的窘境。因此,對於程式正確的要求,遠高於其它產品。這樣才能專注地藉由實驗修正理論模型。在這個需求之下,<a href="http://fcamel-fc.blogspot.tw/2009/06/unit-test.html">Test Driven Development</a> 就成了相當有用的開發方式。經過數次用 TDD 開發的經驗後,總算能夠達到和不用 TDD 差不多的開發效率。這意味著用了 TDD 也不會拖慢完成第一版的時間,並且程式更易於理解和維護。儼然是 TDD 的完全勝利!
</p><p>
但這個經驗有個代價。代價不是學習 TDD 的時間,畢竟<a href="http://fcamel-fc.blogspot.tw/2010/04/blog-post_05.html">學什麼工具或方法 (如OOP) 都要時間</a>。學習 TDD 對立的結果是: 學不到除錯的技巧。
</p><p>
起初我以為除錯是果,治本的方式自然是避免有因。那麼,TDD 貌似完美的解法,學 TDD 即可。但是開發專案免不了團隊合作,也免不了使用第三方程式。換句話說,錯誤遍地都是,有時甚至是作業系統或編譯器的錯。拙劣的除錯技巧無法適應這個時代。我在和別人一起除錯時才發覺這事。對方能力很好,不過開發習慣不太好,常犯一些「我無法想像的錯誤」。但也因為他的習慣,讓他可以比較快看懂別人寫的亂糟糟的程式,可以比較快想到問題可能出錯的地方。我費了不少力氣才補足這塊,實在是始料未及的事。只能說,該走過的路還是得走,無法省去。
</p><p>
目前的工作以 C++ 為主,為了在大量的原始碼裡除錯,偶而會視需求加強一下 <a href="http://fcamel-life.blogspot.tw/search/label/gdb">gdb 的使用技巧</a>,還有練習<a href="http://fcamel-life.blogspot.tw/2013/08/python-gdb-backtrace-2.html">寫 python script 簡化 gdb 操作程序</a>。相對於兩年前,gdb 的操作技巧進步不少。另外也寫了 <a href="https://github.com/fcamel/gj">gj 幫助閱讀程式碼</a>。但是在觀察同事的開發方式後,發覺我有時過於依賴工具的便利性,反而減少全面性的思考。最後還是得有系統地一步步思考、推論,才能有效率地解決問題 (關於這點,之後有適當材料再另寫文章說明「系統化的解決問題」)。換句話說,熟練除錯工具反而無意識地減少我系統性思考的時間,也滅少我系統性思考的經驗。我的意思並非捨棄除錯工具,像 <a href="http://www.imdb.com/title/tt1475582/">Sherlock</a> 那樣全部都在自己的思維宮殿裡解決 (雖說那樣實在是相當地帥啊!)。凡人如我等,還是需要工具輔助搜集情報和記錄訊息。只是這兩件事是相斥的,愈是熟練除錯工具,愈少思考;愈長思考,自然也愈不依賴除錯工具。
</p><p>
再回頭看 TDD,目前的工作絕大多數情況不適用 TDD。一來不像開發 machine learning 工具那樣,要求近乎100%正確。二來大部份的程式和 GUI 相關,本來就不容易測試。若要達到像以前一樣的開發效率,我得先熟悉 C++,再熟悉 C++ 基本的 unit test 工具,再熟悉和 GUI 相關的測試工具和知識。對照達到後帶來的好處,相當地不划算。不過其中一個和 GUI 無關但和網路高度相關的子專案,我很自然地用 Python + TDD 的方式開發核心部份。日後上線時,也從中獲得明確的回報: (1) 極少的錯誤。以及 (2) 透過單元測試輕鬆地重制線上偶而才會發生的網路錯誤,只更新一次程式碼就修正了問題。可參考<a href="http://fcamel-fc.blogspot.tw/2010/04/blog-post.html">《寫出容易測試的程式》</a>了解類似的處境和用到的技巧。當然,我也因此「失去」一些線上除錯的經驗。
</p><p>
除了測試與除錯的心得外,軟體設計模式和軟體開發準則,也讓我經歷了幾次「打破鐵則」的心路歷程。之後再另寫文章補充。
</p><p>
回顧這些事,讓我明白個人經驗的侷限,而減少過度歸納和推衍的習慣。看別人的論述時,會多想想自己和對方的情境,從中得出目前我能用到的部份。不會過於尋找或遵從「聖杯」。白話來講,就是比較務實吧。
</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-40660087174234026332013-07-20T22:55:00.001+08:002014-01-05T13:53:08.684+08:00了解 C/C++ 程式行為的技巧<p>多數情況下我們會使用別人的程式,或是參與別人開發已久的程式,比較少是自己重頭撰寫所有程式。由此可知,了解程式行為的技巧相當重要,但是很少看到有書籍討論這部份的事,或許是因為很難理出一個主題吧。
</p><p>
下面以了解 C/C++ 程式為主,列出自己一些零散心得。
</p>
<h4>動態分析</h4>
<h5>在程式碼裡填入觀察碼</h5><p>加入程式 log function call 是直接有效的作法,而且做起來不如想像的麻煩,詳見 <a href="http://fcamel-life.blogspot.tw/2013/07/trace-cc-function-call.html">《trace C/C++ function call 的方法》</a>。
</p><p>
或是找看看前人是否有了下除錯的 flag。有經驗的程式設計師在開發時必定有一套除錯方式,只是在正式使用時關掉了這些除錯功能,詳見<a href="http://fcamel-life.blogspot.tw/2011/12/cc-debug.html">《C/C++ 檢查和打開 debug 功能的小技巧》</a>。
</p><p>
附帶一提,自己開發程式時,除了留下除錯程式之外,記得要考慮使用 gdb 的影響,讓除錯程式更易於使用,詳見 <a href="http://fcamel-life.blogspot.tw/2013/06/gdb_23.html">《除錯小技巧: 在程式中直接中斷及偵測是否被 gdb 監控中》</a>。
</p><h5>觀察使用的函式庫</h5><p>程式碼通常會用到第三方函式庫,有時我們可以從中得知一些線索,<a href="http://fcamel-life.blogspot.tw/2013/04/python-basehttpserver.html">《python BaseHTTPServer 速度緩慢的原因》</a>是一個簡短的例子,說明如何使用 <strong>ltrace</strong> 找出關鍵的第三方函式,繼而找出效率瓶頸。使用 ltrace 無須重編程式或加入 debug symbol。不過有可能讓程式變得不穩定,這時可使用 <tt>-l filename</tt> 只觀察部份函式庫,執行的時候會比較穩定一些。
</p><p>
其它兩則和 <strong>ltrace</strong> 相關的例子:</p><ul>
<li> <a href="http://fcamel-life.blogspot.tw/2010/02/strace-ltrace-system-call-library-call.html">《用 strace 和 ltrace 找出用到的 system call 和 library call》</a></li>
<li> <a href="http://fcamel-life.blogspot.tw/2012/11/sqlite.html">《以使用 libsqlite 為例說明如何找到程式的進入點》</a></li>
</ul><h5>觀察使用的 system call</h5><p><a href="https://en.wikipedia.org/wiki/System_call">system call</a> 是程式和 kernel 溝通的途徑,介面數量相對少,容易集中觀察。有時藉由觀察 system call 夾帶的參數,可以提供一些線索。比方說,不論上層函式如何包裝,開啟檔案最後會用到 open(),可以從 open() 的參數 找出程式目前使用的設定檔或 log 檔位置。或是像<a href="http://fcamel-life.blogspot.tw/2012/01/strace-ubuntu.html">《用 strace 找出 Ubuntu 如何提示未安裝的指令》</a>,使用 <strong>strace</strong> 觀察 system call 直接解答疑惑。
</p><p>
和 <strong>ltrace</strong> 一樣,無須重編程式也不用 debug symbol。更棒的是,使用 strace() 執行程式還滿穩定的。</p>
<p>其它 strace 相關的例子:</p><ul>
<li><a href="http://fcamel-life.blogspot.tw/2012/01/blog-post.html">《熟悉系統工具好處多多》</a></li>
<li> <a href="http://fcamel-life.blogspot.tw/2011/12/stracedebugger.html">《善用 strace、debugger 從執行期間找出問題根源》</a></li>
</ul><h5>使用 gdb</h5><p>debugger 的重要性無須多言,<a href="http://fcamel-life.blogspot.tw/2012/01/gdb.html">《gdb 初步心得》</a>條列我常用到的指令。另外值得一提的是,有 core dump 的時候,core dump 裡一定會記錄產生 segmentation fault 的 thread,不用擔心找錯 thread,原因見<a href="http://fcamel-life.blogspot.tw/2013/05/linux-thread-signal.html">《linux thread 與 signal》</a>。
</p><p>
編譯上線的程式時,多數會加上 -O2 最佳化效能,讓程式實際執行的狀況和程式碼有些出入。雖然如此,仍然可以加上 -g 觀察程式的 core dump,但要留意觀察的結果不見得是對的,詳見<a href="http://fcamel-life.blogspot.tw/2012/01/debug-info-optimization.html">《debug info 和 optimization》</a>。平時觀察程式行為還是用 -O0 -g 編譯較適當。
</p><p>
以下是其它和 gdb 相關的小技巧:</p><ul>
<li> <a href="http://fcamel-life.blogspot.tw/2013/07/gdb-symbol_18.html">《加速 gdb 載入 symbol 時間》</a></li>
<li> <a href="http://fcamel-life.blogspot.tw/2012/11/x86-64-system-call-conditional-break.html">《在 x86-64 上對 system call 使用 conditional break》</a></li>
<li> <a href="http://fcamel-life.blogspot.tw/2013/07/gdb-stl-container.html">《gdb 顯示 STL container 的方法》</a></li>
<li><a href="http://fcamel-life.blogspot.tw/2013/08/python-gdb-backtrace.html">《用 python gdb 客製化 backtrace 的結果》</a></li>
</ul>
<h5>從 kernel 切入</h5><p>有些情況下我們沒辦法從程式內部或 gdb 取得資訊,比方說程式莫明奇妙地收到 SIGKILL 而結束。由於程式無法攔截 SIGKILL,不方便查出凶手是誰。雖然可以在相關程式內直接對 system call kill() 設中斷點,但若凶手是外部程式,就沒輒了。
</p><p>
這種時候可用 <a href="http://sourceware.org/systemtap/">SystemTap</a> 直接從 kernel 內觀察是什麼程式請求使用 kill 送出 SIGKILL。我自己還沒有第一手經驗,這個例子是從同事那邊學來的,在這裡備忘觀察程式時,還可以用 <a href="http://sourceware.org/systemtap/">SystemTap</a> 觀察更底層的行為。
</p>
<h4>靜態分析</h4><h5>從執行檔找出關鍵字</h5><p><a href="http://fcamel-life.blogspot.tw/2012/02/cfilt.html">《配合 c++filt 讀程式》</a>說明如何從執行檔中找關鍵字,可藉此找出 UI 相關字串或是可能的函式名稱,有時比直接從程式碼下手容易。
</p>
<h5>找出 symbol 的定義或使用到 symbol 的程式碼</h5><p>C 的情況比較單純,相關工具比較正確,也有工具可以產生精美的 call graph。但 C++ 的情況複雜許多,最後我決定無視 C++ 語法,直接找出所有和目標 symbol 有關的程式。<a href="http://fcamel-life.blogspot.tw/2011/12/cc.html">《閱讀 C/C++ 原始碼的好幫手》</a>有整理相關工具,
<a href="http://fcamel-life.blogspot.tw/2012/10/cc-symbol.html">《查 C/C++ symbol 定義的方法》</a>有一點關於我使用 <a href="https://github.com/fcamel/gj/">gj</a> 的方法。
</p>
<h5>使用 doxygen 產生 class 階層關係圖</h5><p>大型專案會依功能切成數個模組,模組本身亦有一套自己使用 class 的方法。直接看程式碼容易陷入見樹不見林的困境。這時可用 class 階層關係圖協助了解整體架構。產生階層圖的方式見<a href="http://fcamel-life.blogspot.tw/2013/08/doxygen-class-hierarchy-diagram.html">《用 doxygen 產生 class hierarchy diagram》</a>。再輔以 gdb 產生 backtrace 觀察類別、模組之間的使用關係,比較容易明白整體架構。</p>
<h5>善用編輯器或 IDE</h5><p>目前是逐步學習如何強化用編輯器或 IDE 讀程式,加快在相關程式碼中探索的時間,還沒整理出一套完整的流程。
</p><p>
比方說用 <a href="https://github.com/fcamel/gj/">gj</a> 跳到某個使用函式 foo() 的檔案後,再來我會想知道目前在那個函式裡,<a href="http://fcamel-life.blogspot.tw/2013/02/vim.html">《vim 顯示目前函式的名稱》</a>就是針對此情境在 vim 內加了快速鍵做這這件事。或是像<a href="http://fcamel-life.blogspot.tw/2013/05/vim-script-cc.html">《使用 vim script 自動開啟 C/C++ 程式的標頭檔或程式碼》</a>說明如何在 .h 檔裡按 F4 快速開啟 .c 或 .cpp 檔,或是反過來在 .c 或 .cpp 裡開啟 .h。
</p><h5>預防勝於治療</h5><p>理解到了解程式是如此不容易後,行有餘力時,別忘了<a href="http://fcamel-fc.blogspot.tw/search/label/Testing">加強測試碼</a>,讓日後使用相關程式的人更快進入狀況。良好的 unit tests 也是不錯的使用範例,有助於了解模組的行為。</p>
<h4>核心分析</h4>
<p>2014-01-05 更新。</p>
<p>現在來看,這段的概念才是理解大型專案時最重要的技巧。經驗愈多,愈覺得如此。說來容易做來難,需要累積經驗後才能體會。</p>
<h5>想像自己會如何進行開發</h5>
<p>有經驗的軟體工程師,做事的方式十之八九也差不多。特別是需求愈嚴苛,最後的解法也不會差太多。先自己想像可能的脈絡,之後比較容易縮小觀察目標,專注驗證自己的假設,並從驗證結果獲得新的契機製定下一步。</p>
<h5>尋找原有專案的除錯工具</h5>
<p>經年累月的專案必定有自己一套除錯工具,不然很難長期維護。有經驗的工程師應該也會在開發過程中研發出負責專案所需的除錯工具。所以,最有效觀察程式的方式,就是用原專案專用的工具。</p>
<p>對目標專案有一定了解後,可以自己想像,若自己進行同樣類型的專案開發,可能會做什麼輔助工具呢?有機會從中猜中切入點。</p>
fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-43343996293945653002013-06-29T18:16:00.001+08:002014-01-05T13:55:40.694+08:00非同步程式心得<p>在這個網路盛行的時代,程式中常常需要連網路取用外部資料,並且不希望因此阻礙主程式的活動,這時會使用非同步操作。或是程式中途需要使用者輸入資料、決定繼續或取消等,若不希望阻礙主程式,也會用非同步操作。
</p><p>
以連上外部伺服器為例,介面可能像是這樣:</p><pre class="prettyprint">bool LoginToServer(const std::string& name,const std::string& password);
void OnLoginToServer(const std::string& name,int error);
</pre><p>
主程式呼叫 LoginToServer() 的當下只能由回傳值知道有沒有進行登入,但不知道是否登錄成功。過一段時間後,某個 thread 會呼叫 OnLoginToServer() 透過 error 的值告知主程式登入成功或失敗。
</p><h4>非同步溝通介面</h4><p>以下以 caller 和 async module 分別表示「使用非同步 API 的函式」以及「提供非同步 API 的模組」。async module 提供的 API 有兩種選擇:</p><ul>
<li> 使用 callback 的方式,async module 有結果時通知使用 callback 通知 caller。</li>
<li> caller 事後自行詢問先前的呼叫是否已有結果。</li>
</ul><p>
兩種介面各有優缺點,不過 callback 似乎比較盛行?
</p>
<h4>使用 callback 的方式</h4>
<p>
async module 會保證通知 caller,這有以下的好處:</p>
<ul>
<li>caller 省去自行詢問的麻煩。</li>
<li>可以即時通知 caller 繼續工作,適合要求反應速度的程式。</li>
<li>隱藏 thread 的概念,適合更廣的開發者使用。<a href="http://developer.apple.com/library/ios/#documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091">像 iOS 官方文件建議盡量不要使用 thread。</a></li>
</ul>
<p>
代價是 caller 需要記下呼叫當下的 context,callback 時才知道怎麼完成剩下的工作。並且不方便閱讀以及用 debugger 追踪程式。
</p><p>
以續傳上傳為例,假設網站提供的介面如下:</p><ol>
<li> /create/?filename=X&size=Y: 得知上傳檔名和大小,回傳檔案代碼</li>
<li> /upload/?filehandle=X&data=Y&offset=Z: 持續接收檔案內容</li>
<li> /done/?filehandle=X: 結束上傳</li>
</ol><p>
client 使用 http 函式庫時,需要注意以下事項:</p><ul>
<li> 需要類似 state machine 的結構,callback 時才明白目前進行到那一個步驟。</li>
<li> 需要記錄輔助資訊,像是目前在處理那個檔案,上傳到那一段資料。</li>
<li> 需要考慮中途的錯誤處理。</li>
<li> 結束後和主程式接軌的方法</li>
<li> 同時上傳多個檔案時,儲存狀態的資料結構需要更靈活一些。</li>
</ul>
<h4>使用詢問的方式</h4>
<p>
async module 可以提供 non-blocking 或 blocking 的詢問函式。使用 <a href="http://linux.die.net/man/3/pthread_cond_wait">pthread_cond_wait()</a> 之類的方式避免 <a href="http://en.wikipedia.org/wiki/Busy_waiting">busy waiting</a>。於是 caller 可使用 non-blocking 的方式確認情況,或是使用另外特定的 thread 用 blocking 的方式確認情況。使用 blocking 的詢問方式時,caller 不需另外存下呼叫時的狀態,易寫易讀。</p>
<p>但是有多個非同步操作時,詢問的方式不容易即時延續先前非同步的操作。比方發出十個 http request 時,若是使用 callback 介面,那個 http response 好了,就先呼叫它的 callback 傳回 http response;使用詢問方式時,caller 得一個個用 non-blocking 詢問方式,或十個 thread 各用 blocking 詢問的方式,才能達到一樣的反應速度。
</p>
<h4>callback 介面的選擇</h4><p>async module 提供 caller 傳入 callback 時,有幾種選擇:</p><ul>
<li> async module 定義 class APIDelegate,caller 使用 async module 前放入 async module 規定的 APIDelegate,async module 之後會呼叫 APIDelegate 不同函式告知後續情況。比方說 iOS 的 <a href="http://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSURLConnection_Class/Reference/Reference.html">NSURLConnection</a> 定義了 <a href="http://developer.apple.com/library/ios/#documentation/Foundation/Reference/NSURLConnectionDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intf/NSURLConnectionDelegate">NSURLConnectionDelegate</a> 和 <a href="http://developer.apple.com/library/ios/#documentation/Foundation/Reference/NSURLConnectionDownloadDelegate_Protocol/NSURLConnectionDownloadDelegate/NSURLConnectionDownloadDelegate.html#//apple_ref/occ/intf/NSURLConnectionDownloadDelegate">NSURLConnectionDownloadDelegate</a>。 </li>
<li> caller 呼叫 async module 的函式時,直接傳入 callback object,像 JavaScript 的 <a href="http://api.jquery.com/jQuery.ajax/">jQuery.ajax()</a> 裡面的 success 以及 error 參數。</li>
</ul><p>
第一種介面明確,易讀難寫;第二種則是易寫難讀。若是長期發展的程式,我偏好第一種設計,有明確的名稱比較好追蹤程式碼。
</p><p>
不論是那種介面,caller 都需要自行記住呼叫當下的狀態,callback 回來時才知道如何完成後續工作。像 <a href="http://fcamel-life.blogspot.tw/2013/01/nsmutabledictionary-category.html">NSURLConnection 的 callback 只有告知 connection,connection 又不能作為 dictionary 的 key,使用上稍有不便</a>。理想的情況下,callback 應該提供 context 帶有必要的資訊或是提供 unique ID 方便 caller 自行管理 context。比方說 Unix 的檔案操作提供一個 file handle,由系統記住檔案相關的設定和讀寫的狀態,caller 透過 file handle 可以取得所有相關資料。
</p><h4>async module 實作的選擇</h4><p>實作 async module 有幾種選擇:</p><ul>
<li> 新增一個 thread 讀寫外部資料,結束後呼叫 callback。</li>
<li> 使用 non-blocking I/O 讀寫外部資料,再由一個特定的 thread 持續追蹤結果,有結果後再呼叫 callback。由於可以使用系統函式 (如 select 或 epoll) 追蹤,負擔相對的低。</li>
</ul><p>
第一個作法易讀易寫,但是有以下的缺點:</p><ul>
<li> 新增和消滅 thread 有時間成本。</li>
<li> thread 會占用額外記憶體。若一個 thread 占用 2MB,512 個會占用 1GB。</li>
<li> 大量 thread 同時讀寫資料時,增加額外 context switch 的成本。</li>
</ul><p>
若這個 module 只會偶而用到的話 (比方說登入伺服器),到是沒什麼問題。
</p><p>
第二個作法難讀難寫,也因此有些善心人士包好函式庫負責做這類事,像是 <a href="http://libevent.org/">libevent</a>、<a href="http://software.schmorp.de/pkg/libev.html">libev</a>。
</p><p>
雖然使用函式庫降低第二種作法的實作難度,程式碼還是不易閱讀。於是有人結合 <a href="http://en.wikipedia.org/wiki/Coroutine">coroutine</a> 和 non-blocking I/O,做出更好用的框架。我目前只有用過 Python 的 <a href="http://www.gevent.org/">gevent</a>,寫起來如同寫 multi-thread,但是底層沒有用到大量 thread,免除了 thread 帶來的缺點。相信在其它語言也有類似的框架。
</p><h4>使用 coroutine 的時機</h4><p>雖然 <a href="http://en.wikipedia.org/wiki/Coroutine">coroutine</a> 兼具易寫易讀負擔又低等優點,它有個關鍵的難處:所有程式都要在 <a href="http://en.wikipedia.org/wiki/Coroutine">coroutine</a> 的架構下,只要有一個 "thread" (正確說法是 subroutine) 卡住了 (比方不小心用到 blocking I/O),全部 "thread" 都會被卡住。現今的程式使用許多外部函式庫,很難保證不會發生這類事。
</p><p>
此外,依實作方式而定,<a href="http://en.wikipedia.org/wiki/Coroutine">coroutine</a> 可能不方便使用多個 CPU,只要有一個工作需要大量 CPU 計算時間,也會拖累其它 "thread"。
</p><p>
所以一般的 GUI 程式或 client 端連線程式不見得適用 <a href="http://en.wikipedia.org/wiki/Coroutine">coroutine</a>。但像 proxy server 這類主要工作是 I/O 且相當重視連線數量 scalability 的程式,就很適合用 <a href="http://en.wikipedia.org/wiki/Coroutine">coroutine</a>。
</p><h4>gevent 使用心得</h4><p>以我自己的情況來說,主程式使用 <a href="http://www.gevent.org/">gevent</a> 處理 client 的需求,需要外部 http 連線時使用 <a href="https://github.com/gwik/geventhttpclient">geventhttpclient</a>,滿容易達到 <a href="http://en.wikipedia.org/wiki/C10k_problem">C10K</a>。不過 unit test 的時候有些小問題,還有 gevent signal 處理有些 bug,踩到時要特別處理。
</p><p>
另外,雖然使用 <a href="http://www.gevent.org/gevent.monkey.html">gevent 的 monkey patch</a> 可「無痛替換所有網路操作」,實測的結果效能不太好,而且我個人偏好「Explicit is better than implicit」,所以沒用 monkey patach。
</p><h4>結語</h4><p>以上是這一年半來寫網路程式和 GUI 的心得。實作非同步程式時,要留意自己是處於 caller 或 async module 的角度。兩者需要考慮的事不同。特別是自己一手包辦整個程式時,切清楚兩者的角色才會有清楚的架構,方便日後維護。
</p><h4>備註</h4><ul><li> <a href="http://blog.ez2learn.com/2010/07/17/talk-about-coroutine-and-gevent/">淺談coroutine與gevent</a>:有關於兩者更多的說明。</li>
<li> <a href="http://www.kegel.com/c10k.html">The C10K problem</a>:介紹實作承載 concurrent 10,000 連線的 server 程式時的相關技術。如今函式庫遍地開花,使用 <a href="http://www.gevent.org/">gevent</a> 可以輕鬆達到這要求,不過需要另外調一下 OS 參數放鬆一些 process 的限制 (如可使用的 fd 數量)。</li>
<li> <a href="http://fcamel-life.blogspot.tw/2013/02/async-non-blocking-io.html">Linux 上的 non-blocking flag 不適用於檔案</a>。可考慮用 thread pool 處理檔案讀寫。</li>
</ul>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com2tag:blogger.com,1999:blog-451051488891335923.post-79123021037610456112012-08-05T20:27:00.001+08:002014-01-05T13:55:40.705+08:00從需求出發理解背後技術的思考脈胳<p>最近在技術上多了不少體悟,關鍵是要<a href="http://fcamel-fc.blogspot.tw/2012/02/blog-post.html">掌握住問題的需求</a>,接著不斷思考和嘗試,避免卡在特定的規則或工具裡,將重心放在隱藏在這些東西背後的核心思想。
</p><p>
直接看現有的工具或別人的作法很難有所體會,但自己從頭邊做邊想,會得出自己一套理論, 接著會出乎意料地更快理解這些工具和作法。畢竟在足夠嚴苛的需求下,方法可能有些差異,背後的精神卻是類似的。
</p><p>
比方說,若程式反應速度要求在 1ms 以內,全部資料來源都得在記憶體或網路,不能使用硬碟。那麼,針對這種應用,單機程式能做的事只有將東西<a href="http://fcamel-life.blogspot.tw/2011/02/mysql-memory.html">塞在記憶體裡取用</a>。
</p><p>
然而記憶體有限,資料總有放不進去的時候。在這種速度要求且資料過大的前提下,可能的作法大概是:</p><ul>
<li> 像 cache 那般有套機制留住最常用的資料。</li>
<li> <a href="http://fcamel-life.blogspot.tw/2010/11/myisam-data-cache-covering-index.html">在記憶體裡存 index,index 內存必要的資料</a>,若需要更多資料再從硬碟讀。</li>
<li> 先做前處理「壓縮」資料成摘要,摘要必須小到可以放入記憶體內。資料的品質(正確性)也許會有一些損失,但是是必要的取捨。之前在讀 YouTube 關聯影片和 IBM 的 Watson 論文時,有看到類似的思路,工作上也做過類似的事。和前述方法不同的地方是,資料的筆數變少,轉為另一種高質量的資料。</li>
</ul><p>
再以 <a href="http://en.wikipedia.org/wiki/Test-driven_development">Test Driven Development</a> 為例,最重要的不是 <a href="http://fcamel-fc.blogspot.tw/2009/03/tdd_09.html">TDD 的三步規則</a>,也不是<a href="http://fcamel-fc.blogspot.tw/2009/09/python.html">相關工具</a>要怎麼用,而是導入<a href="http://fcamel-fc.blogspot.tw/2010/04/blog-post.html">「在設計的開頭, 就將測試視為主要考慮項目」</a>,其它東西都是這個想法的衍生。
</p><p>
從這個角度出發,實作久了自然會理解<a href="http://fcamel-fc.blogspot.tw/2010/04/blog-post.html">為什麼介面要開洞放入物件</a>,為什麼需要 factory 隔離生成和操作邏輯。至於是否真的有先寫測試,現在我覺得不是鐵則,在有為測試而考慮的設計下,在必要時補測試可能更划算,將時間花在刀口上。<a href="http://fcamel-fc.blogspot.tw/2009/06/unit-test.html">但若一開始沒為測試考慮,事後要補就很辛苦,加測試的價值又更低了</a>。
</p><p>
再往上拉一個層次來看,<a href="http://fcamel-fc.blogspot.tw/2009/09/blog-post_05.html">解決問題的前提,本來就是如何確認問題有被解決</a>,若無法確認問題有無被解決,用什麼方法也是白搭,不知成效如何。從這個角度來看,在設計之初就考慮測試,並不是什麼新穎或強人所難的事。但要走到這步,<a href="http://fcamel-fc.blogspot.tw/2010/04/blog-post_05.html">如同學其它東西一般,需要累積不少經驗</a>。
</p>
<p>
總結來說,要學習一個技術最好的方法,就是在有適當的需求時再學。平時看到和自己需求無關的知識,加減有個印象在腦裡即可。待要用到時可利用腦裡的索引找出相關資訊,先有個廣泛認知。接著根據自己的需求可以明白那些是比較相關的知識,邊看邊動手做些雛型試試。有了這些操作經驗和背景知識後,再從頭確認一次自己真正的需求為何、有那些限制和對應解法,就能理解要如何應用這些技術和工具到自己的情況裡。
</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-58827965365410222362012-05-06T21:16:00.001+08:002014-01-05T13:55:40.697+08:00The Linux Programming Interface 讀書心得<p>書還沒出以前就聽 <a href="http://scottt.tw/">Scott</a> 推薦過 <a href="http://man7.org/tlpi/">TLPI</a>,直到因為工作需要,才買回來看。斷斷續續大概看了三個月半,來寫一下心得。先說結論,<b>若需要寫 Linux programming,這本是極佳的選擇,它是我買過最貴最厚的書,同時也是我買過最划算的書。</b>
</p>
<p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQ2SxZjKc-1_pj6ppEhLgmbpsEJfFLnHTs0RpW-TUTXnenJiuM8pxdPW6nR1WUITn4uVkxfdug5H5vSkD-RXq8VUmxZ7eBxDFdgsiWqbVgRnvJPRmOoq72SS1r1ttN0IKAXK3n7wigDDJV/s1600/IMG_0124.JPG" imageanchor="1" style="margin-left:1em; margin-right:1em"><img border="0" height="240" width="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQ2SxZjKc-1_pj6ppEhLgmbpsEJfFLnHTs0RpW-TUTXnenJiuM8pxdPW6nR1WUITn4uVkxfdug5H5vSkD-RXq8VUmxZ7eBxDFdgsiWqbVgRnvJPRmOoq72SS1r1ttN0IKAXK3n7wigDDJV/s320/IMG_0124.JPG" /></a></div>
</p>
<p>
全書共 64 個章節近 1500 頁,在 2010 年底出版,算是相當新且完整的書。我看的方式是:每天沒事翻個四頁,有事時針對需要的重點看,不見得每個章節都要看完,有解決當下需求即可。持續這樣運作個一陣子,效果相當好。從上面的照片可看到我插的一堆書籤,發票、鐵尺、名片、計算紙等,不知不覺就放了 14 個,看了小有成就感。
</p><p>
本書最大的好處是,作者對每個主題循序漸進地提供鉅細靡遺的說明,不只是 POSIX 或 Linux 能做什麼,同時也會提供必要的基礎知識,像在 network programming 的章節,會補充說明 TCP/IP 的運作方式,而且相當容易吸收。ch 41、42 則是說明 static 和 shared libraries,也是相當實用的知識。之前曾在網路上看過一些片段知識和一些動手操作的經驗,但一直沒有拼出全貌。直到看了這兩章後,才有種打通的感覺,更有把握知道底層是怎麼運作的,從而解決工作上的疑問。
</p><p>
所以,只要有耐心啃完書中相關內容,就有個八成把握知道怎麼做才對。相較於在網路上搜尋,更能系統地了解 Linux 能與不能做什麼事,有利於判斷可行的方案。偶而我也會看 man page 的說明,交叉對照效果更好。作者說的是他整理消化後各系統通用的知識,而 man page 可進一步反映出自己使用 OS 版本的細節。
</p><p>
本書另一大好處是,每個章節都有附完整的程式碼,輔以執行結果,來說明系統的一些特性。像是「20.12 Signals Are Not Queued」,為了說明 signal 只有保證送出後至少會收到一次,但不是送幾次就收到幾次,作者寫了個小程式 A 送個上一百萬次 signal 給 B,B 則是在 signal handler 裡計數,結果在作者的這次實驗裡, B 只收到 52 次。我很喜歡這種實證的方式,現在的系統太複雜,在不同平台會有不同狀況。了解知識的概念後,手邊還是要有程式實測,感覺才會踏實。
</p><p>
除了內容完善以外,對書中有疑問時,寄信給作者也會得到很友善的回應。舉了這麼多優點,實在是讓人想不出不買它的理由,也難怪 <a href="http://www.amazon.com/The-Linux-Programming-Interface-Handbook/dp/1593272200">Amazon 上 35 個評價全都是五顆星了</a>。</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com2tag:blogger.com,1999:blog-451051488891335923.post-77080319880537551102012-02-19T03:11:00.001+08:002014-01-05T13:55:40.703+08:00專注於滿足需求而非工具或方法<p>看到 <a href="https://plus.google.com/102249536116095884743/posts">command</a> 提到<a href="http://prog21.dadgum.com/128.html">《Don't Fall in Love With Your Technology》</a>,而有一些感觸。
</p><p>
從高中開始,我一直想弄明白 Perl、Python 到底那一個比較好用,這樣我學其中一個就可以了。後來又多了 Ruby 這個選項,讓這問題變得更複雜。大概到大學後期或研究所的時候,我才肯定這是一種<a href="http://fcamel-fc.blogspot.com/2009/07/blog-post_22.html">信仰上的爭辯</a>,而將這個問題拋之於腦後。
</p><p>
同一時期,我也花了滿長一段時間才明白許多問題<a href="http://fcamel-fc.blogspot.com/2010/04/3.html">沒有標準答案</a>,<a href="http://fcamel-fc.blogspot.com/2011/09/problem-solving-3.html">得視情況而定</a>。每當對此有所體會時,就會想起大學電子學老師整年重覆強調的一句話:<b>「沒有前提,就沒有答案」</b>。雖然兩學期的電子學都是低空飛過,這句話深深印在心裡,只是那時我對這句話的理解仍不深,還需時時重新琢磨它的含意。
</p><p>
我花了更長的時間才將前面兩個心得連結在一起,從而明白<strong>任何工具或方法的爭辯很可能都是偽命題,重點在於需求是什麼?要如何滿足需求?</strong>如今回想起來,<a href="http://www.ruanyifeng.com/blog/2011/10/dont_call_yourself_a_programmer.html">《不要自称为程序员》</a>將這個觀念解析得相當清楚,相當值得一看。
</p><p>
舉例來說,「vim vs. emacs」是個偽命題,這取決於自己當下的環境為何。若團隊內多數人使用 emacs 且自己兩者都不熟,那麼 emacs 是較為合理的選擇。反之,若自己相當熟 vim 而團隊內多數人兩者都不熟,那繼續使用 vim 較為合理。重點在於<strong>「如何有效率地在自己的環境下解決問題</strong>」,<strong>而非「一般而言,那一個編輯器比較強?」</strong>
</p><p>
再以軟體開發的方法來看,「<a href="http://en.wikipedia.org/wiki/Agile_software_development">agile</a> vs. 某個軟體開發方法」也是偽命題,不論 <a href="http://en.wikipedia.org/wiki/Agile_software_development">agile</a> 公認的定義為何,重點在於滿足需求,而滿足需求不見得需要一套完備的軟體開發方法;有完備的軟體開發方法不見得能滿足需求。要滿足需求有太多事要做,研讀相關技術、軟體開發、市場行銷等,軟體開發可能是滿足需求的其中一項基石,但不是全部。若滿足需求的前提需要改善軟體需求,自然需要改善它;反之則否。<a href="http://www.csie.ntu.edu.tw/~p92005/Joel/fog0000000018.html">《Joel on Software - 別讓架構太空人嚇到你》</a>對「開發軟體的方法 vs. 滿足需求」提了生動的描述。
</p><p>
舉另一個具體例子,「是否需要重構?」往往帶來許多爭議性的討論,各方人馬(PM、RD、QA、...) 對此有不同看法。若這段程式一直都不需要加新功能,那的確不需要重構。重構只會花費時間讓程式碼變漂亮,對於滿足需求沒有任何影響。反之,之後需要繼續大幅加功能,逐步重構部份功能,則對完成產品(滿足需求)大有幫助。
</p><p>
最近幾年有一個很紅的議題,開發網站是用 <a href="http://rubyonrails.org/">Ruby on Rails</a> 好,還是用 ... 好。最近一年可能還會多一些人問是否要改用基於 <a href="http://nodejs.org/">node.js</a> 的新 framework。要回答這問題得先看需求為何,若只是做幾頁的小網站,用什麼方法差異都不大;若是做長期維護的大網站,要看目前團隊成員熟悉的工具和程式語言為何,再來評估使用 Rails 的相對成本。若再涉及和後端整合,又和既有的 code base 有大幅關聯。而要回答這一切一切衍生的議題,還是得先看:究竟需求為何,基於什麼原因而採用 X 會更好?以 <a href="http://justin.tv">Justin.tv</a> 為例,<a href="http://www.quora.com/Django/Why-is-Justin-tv-porting-their-codebase-to-Django-from-RoR">《Django: Why is Justin.tv porting their codebase to Django from RoR?》</a>說明 Justin.tv 轉換的主因是全部程式都是用 Python 寫的,此外,他們也想藉機重新設計一遍架構,去除 legacy codes,以符合現今的使用需求。
</p><p>
舉這些例子的用意不是無限上綱地說工具和方法都不重要,而是強調將焦點放在如何滿足需求,若有需要選用好工具,才有必要討論它。<strong>問錯問題的話,永遠不會得到有用的答案。</strong></p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-51795177932127749902012-01-19T00:22:00.013+08:002018-08-19T09:58:38.072+08:00在 Linux 下開發 C/C++ 的新手指南<h4>2018-08 更新</h4>
<p>依近幾年的經驗更新了另一份介紹,見<a href="https://medium.com/@fcamel/%E5%9C%A8-linux-%E4%B8%8B%E9%96%8B%E7%99%BC-c-c-%E7%9A%84%E6%96%B0%E6%89%8B%E6%8C%87%E5%8D%97-735fcd960b0">這裡</a>。</p>
<hr /><hr />
<p>新加入一個專案,最先面對的課題是如何正確地編譯和執行專案,可從 <a href="http://nebulousarion.wordpress.com/2008/02/23/top-20-replies-by-programmers-when-their-programs-do-not-work/">"It works on my machine" </a> 如此地風行,印證這件事的困難性;再來則是閱讀負責工作相關的程式碼。至於發揮程式語言的特性,運用高階設計模式等,都是另開新專案或熟悉狀況後才有機會發揮。 </p><p>過去數年沉浸在愉快的 scripting language 和開發新專案中,一直沒踏入這殘酷的世界。這篇記錄在這樣的情境下,可能需要的技能,結算一下這一個多月的心得,全都是血淚談啊 ...。</p><h4>系統工具</h4><p>熟悉作業系統的安裝套件是首要之務,這樣才知道如何補足需要的 header、library,或是安裝含 debug symbol 版的函式庫以執行 gdb 觀察程式或除錯。參見<a href="http://fcamel-life.blogspot.com/2012/01/debug-symbol-package.html">《自行編譯含 debug symbol 的套件 (package)》</a>了解 Ubuntu/Debian 下的套件命名規則。 </p><p>在未安裝套件的情況下,可用</p><ul><li> aptitude search SUBSTRING # 找套件</li>
<li> aptitude show PACKAGE # 顯示套件用途</li>
<li> apt-file search X # 找出 X 包在那個套件裡,找 header 時很有用。</li>
</ul><p>注意在用 apt-file 前要先跑 sudo apt-file update,不然搜不出東西來。 </p><p>對於已安裝套件,可用</p><ul><li> dpkg <span class="escaped">-</span>-search SUBSTRING # 找出安裝在那個套件,已知 header 時,適合用來找 library</li>
<li> dpkg -L PACKAGE # 列出套件內容,可用來找 header、library</li>
<li> locate SUBSTRING # 我比較常用它找 header 的位置,再觀看 header 內容</li>
</ul><p>執行 locate 前記得先執行 sudo updatedb,原因同 apt-file。 </p>
<p><a href="http://fcamel-life.blogspot.tw/2017/03/ubuntu.html">《除錯技巧:在 Ubuntu 上找出第三方函式庫的程式碼》</a>用一個小例子說明如何使用這些工具找出原始碼協助除錯。</p>
<p>
其它參考資料: <a href="http://www.thegeekstuff.com/2009/10/debian-ubuntu-install-upgrade-remove-packages-using-apt-get-apt-cache-apt-file-dpkg/">How To Manage Packages Using apt-get, apt-cache, apt-file and dpkg Commands ( With 13 Practical Examples )</a>
</p>
<h4>編譯</h4><ul><li> 參考<a href="http://fcamel-life.blogspot.com/2011/12/undefined-symbol-reference.html">《解決 undefined symbol / reference》</a>了解整個編譯的流程,先有觀念才清楚問題的環節,才能選對工具檢查問題。</li>
<li> 另在<a href="http://fcamel-life.blogspot.com/2012/01/c-c.html">《從 C 呼叫 C++ 函式的過程理解程式編譯、連結的原理》</a>以一個小個案,從另一個角度描述編譯的流程來除錯。</li>
<li> <a href="http://fcamel-life.blogspot.com/2012/01/debug-info-optimization.html">《debug info 和 optimization》</a>提到 -O 和 -g 可同時用,以及注意事項。</li>
<li> <a href="http://fcamel-life.blogspot.com/2012/01/man-page.html">《讀懂函式庫的 man page》</a> 說明使用系統函式庫時,如何從 man page 得知該定義的 feature test macro 和連結用的參數。</li>
</ul>
<h4>連結</h4><p>這一塊讓我卡了一陣子。一些粗淺心得:</p><ul><li> <a href="http://fcamel-life.blogspot.com/2011/12/list-shared-library.html">《列出用到的 shared library》</a>。</li>
<li> <a href="http://fcamel-life.blogspot.com/2011/12/linking-time.html">《加速 linking time》</a></li>
<li> <a href="http://fcamel-life.blogspot.com/2012/01/ld-ldso-ldconfig.html">《ld, ld.so 和 ldconfig 的行為》</a></li>
<li> <a href="https://fcamel-life.blogspot.tw/2017/04/blog-post.html">《(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫 》</a></la></li>
</ul>
<h4>執行</h4><p>光只是讀程式碼就像大海撈針一樣,不太有效率。可從動態執行過程找出主要執行的路徑,再專注相關的程式碼。 </p><p>1. strace 和 ltrace </p><p>srace 是分析執行行為的強大工具,google 一下會看到很多別人的個案心得,看看再自己試一試,很快能上手,不知能發揮它多少功能。這裡列自己用的兩個小案例:</p><ul><li> <a href="http://fcamel-life.blogspot.com/2011/12/stracedebugger.html">《善用 strace、debugger 從執行期間找出問題根源》</a></li>
<li> <a href="http://fcamel-life.blogspot.com/2012/01/blog-post.html">《熟悉系統工具好處多多》</a></li>
<li> <a href="http://fcamel-life.blogspot.com/2012/01/strace-ubuntu.html">《用 strace 找出 Ubuntu 如何提示未安裝的指令》</a></li>
</ul><p>反而是 ltrace 一直都想不到使用它的時機,也沒找到好的個案心得文。 </p><p>2. gdb </p><p>gdb 的重要性不需多說明,之前的幾則心得:</p><ul><li> <a href="http://fcamel-life.blogspot.com/2012/01/gdb.html">《gdb 初步心得》</a></li>
<li> <a href="http://fcamel-life.blogspot.com/2011/12/core-dump-cgdb.html">《打開 core dump 和使用 cgdb 檢查程式掛點原因》</a></li>
<li> <a href="http://fcamel-life.blogspot.com/2012/01/glibc.html">《追踪 glibc 裡的程式》</a></li>
</ul><p>強烈建議使用 <a href="http://cgdb.sourceforge.net/">cgdb</a>,簡易安裝 + 無痛上手,瞬間省下大量操作和讀碼的時間。 </p><p>3. 打開除錯功能 </p><p>依照開發者的習性,一定會留後門讓自己方便除錯,從這角度下手也可省下不少時間:</p><ul><li> <a href="http://fcamel-life.blogspot.com/2011/12/cc-debug.html">《C/C++ 檢查和打開 debug 功能的小技巧》</a></li>
</ul><p>4. 載入函式庫</p><ul><li> 若在編譯、連結時無法解決相依問題,可考慮偷吃步<a href="http://fcamel-life.blogspot.com/2011/09/ldpreload.html">在載入程式時用 LD_PRELOAD 換掉部份函式</a>。</li>
<li> 另外備忘用 LD_LIBRARY_PATH 補充載入 shared library 的位置,目前仍沒用過它。</li>
</ul><p>除以上所言外,我另外有找過畫出程式流程的靜態和動態分析工具,像是畫 call graph 或是 C 的 cflow。不過 C++ 的靜態分析效果很糟,就沒花太多時間研究。目前用 strace 和 gdb 覺得已夠用了,不知用工具產生 call graph、class 相依圖或其它東西,是否會更有幫助。待有需求看整體的程式時再來試試。<br />
</p><h4>閱讀程式碼</h4><p>聽了大家的建議後,做了一些實際操作,而有些心得:</p><ul><li> <a href="http://fcamel-life.blogspot.com/2011/12/cc.html">《閱讀 C/C++ 原始碼的好幫手》</a></li>
<li> <a href="http://fcamel-life.blogspot.com/2011/12/eclipse-cdt-cc.html">《用 Eclipse CDT 讀 C/C++ 原始碼》</a></li>
</ul><p>Eclipse CDT 雖然方便,後來我還是用 <a href="https://github.com/fcamel/gj/">gj</a> 居多。原因有幾點:</p><ul><li> 我已很習慣用 vim + screen 做事,<a href="https://github.com/fcamel/gj/">gj</a> 最合這個情境</li>
<li> <a href="http://www.gnu.org/s/idutils/">id-utils</a> 真的是超級快</li>
<li> 我針對自己的需求更新 <a href="https://github.com/fcamel/gj/">gj</a> 多次,愈用愈順手</li>
</ul><p>另外 <a href="http://betterthangrep.com/">ack</a> 也滿方便的,懶得建 index 或是想比對子字串時,可直接使用。當然 <a href="http://www.gnu.org/s/idutils/">id-utils</a> 也支援子字串比對,只是暫時懶得為此修改 <a href="https://github.com/fcamel/gj/">gj</a> 的程式,目前大部份需求是找完整的 symbol。 </p>
<h4>熟悉 Linux 系統程式</h4><p>在基本工具都上手後,打算每天抽一點時間加減讀一點相關知識。一兩年下來應該會有不錯的成果。目前打算讀<a href="http://man7.org/tlpi/">《The Linux Programming Interface》</a>,年假時試看看效果如何。 </p><p>這一個月的心得以了解 /proc 為主,對觀察 CPU 用量、RAM 用量、載入那些函式庫、multi-thread、程式執行狀態等都很有幫助:</p><ul><li> <a href="http://fcamel-life.blogspot.com/2012/01/procpidtask-multi-thread-status.html">《透過 /proc/PID/task/ 觀察 multi-thread 狀態》</a></li>
<li> <a href="http://fcamel-life.blogspot.com/2011/12/list-shared-library.html">《列出用到的 shared library》</a></li>
</ul><h4>結論</h4><p>即使大概知道有那些東西,還是需要實際動手的經驗,才會真的學進去。一個月下來進步了不少,不過對於要面對的戰役,還有一大段路要趕上,還有很多很多要學的。</p>
<h4>2012-01-29 更新</h4>
<p>
補上一些後來新寫的連結。此外,<a href="http://man7.org/tlpi/">《The Linux Programming Interface》</a> 相當實用,讀 ch1 ~ 3 讓我補足不少基礎知識。ch41、42 講解 shared library 也相當值得一看。相關心得見<a href="http://fcamel-fc.blogspot.tw/2012/05/linux-programming-interface.html">《The Linux Programming Interface 讀書心得》</a>。
</p>
<h4>2013-07-13 更新</h4>
<p>備忘效能分析相關的工具:</p><p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1IUkL0Lqsg5iMEHjcAVgfqwhxK-RKnXM8nQf938zzzcUdFkkeCNhwkPSamsLLi083rFqD8arq-lnsq5d1nJht1M8YlGmE1Tv59clQAbCMFFzj37vI1MSyGoT0VNK35c-NXzn7KPxefBXX/s1600/Linux+Performance+Analysis+and+Tools.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1IUkL0Lqsg5iMEHjcAVgfqwhxK-RKnXM8nQf938zzzcUdFkkeCNhwkPSamsLLi083rFqD8arq-lnsq5d1nJht1M8YlGmE1Tv59clQAbCMFFzj37vI1MSyGoT0VNK35c-NXzn7KPxefBXX/s320/Linux+Performance+Analysis+and+Tools.png" width="480" height="351" /></a></div></p>
<p>出處:<a href="http://www.slideshare.net/brendangregg/linux-performance-analysis-and-tools">Linux Performance Analysis and Tools</a>
</p>
<h4>2013-07-20 更新</h4>
<p>將後半部份內容抽出來,另寫了一篇比較完整的文章:<a href="http://fcamel-fc.blogspot.tw/2013/07/cc.html">《了解 C/C++ 程式行為的技巧》。</a>
</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com5tag:blogger.com,1999:blog-451051488891335923.post-32652704234951204882011-11-23T14:49:00.001+08:002011-11-23T14:59:02.108+08:00使用 Django 的雜感<p>換工作後大概很少會用到 <a href="https://www.djangoproject.com/">Django</a>, 在這對它做個了結吧。
</p><h4>先說結論</h4><p>對於不熟 web 又想用 python 開發的人來說, <a href="https://www.djangoproject.com/">Django</a> 仍是首選, 主要的優勢有:</p><ol>
<li> 官方文件超級豐富, 網路上文件也豐富, 也有出一些書, 不過書上內容應該沒官網新, 看官網就夠了。不夠的話再看原始碼也比較方便。</li>
<li> 社群龐大, 有許多 middleware 和 plugin 可用, 像多人合寫 web 一定會需要 database integration, <a href="http://south.aeracode.org/">South</a> 相當好用。其它像用 Facebook / Google / Yahoo 等帳號登入, 也都有整合好的套件可用。</li>
<li> 框架本身實作了許多 web 相關功能 (像 session、cookie、cache、傳輸 zip 後的內容), 可避免犯錯 (像是有擋 <a href="http://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF</a>), 很多東西是新手從來沒想過的, 但框架都有考慮到並且實作好了。在使用的過程中可學到這些知識。</li>
<li> 極佳的向下相容。官方的 roadmap 會明確提到那些功能已 deprecated, 並會在未來的那一版移掉, 有滿長的過渡期。使用者升級的負擔相當小。</li>
<li> 提供一組規範, 切開 model、view、control, 還有 class / table 命名規則等, 減少團隊合作的問題。</li>
</ol><p>
當初選 <a href="https://www.djangoproject.com/">Django</a> 的主因是第一、二點, 第三點稍微有想到, 而第四、五點是使用後得到的驚喜。
</p><p>
說完結論後, 要開始碎碎念我的不滿, 請各位看倌記得, <strong>即使如此, 我還是推薦不熟 web 又想用 python 開發的人使用 <a href="https://www.djangoproject.com/">Django</a></strong>, 理由如前所述。
</p><h4>關於 template</h4><p><a href="https://www.djangoproject.com/">Django</a> 內建的 template 不好用, 效率也差, 甚至在 FAQ 裡有一項提到<a href="https://docs.djangoproject.com/en/dev/faq/usage/#i-can-t-stand-your-template-language-do-i-have-to-use-it">「I can't stand your template language. Do I have to use it?」</a>。有些人可能覺得 template 的效率不是重點, 瓶頸會在 database。當我費盡心力減少 SQL、改 schema、改index、改寫 SQL 將 database 花的時間壓到極致後, 卻發現 template 怎麼縮都要 0.1s, 讓我很無力。更別提在關掉 i18n / l10n 前, template 要 0.2s。看著簡單的 template 內容, 很難理解為什麼這樣的東西要花到 0.1s。
</p><p>
此外, template 的語法很受限, 不過到 1.3 版後 <a href="https://docs.djangoproject.com/en/dev/ref/templates/builtins/?from=olddocs#include">include 多了 with</a> 的語法方便代入子版面後, 變得好用許多, 不然有類似這種簡單需求時要另寫 templatetag, 實在是多餘又不易懂。相關心得見<a href="http://fcamel-life.blogspot.com/2010/05/django-template.html">之前寫的文章</a>。實在是換掉也為難, 不換也為難。
</p><h4>關於 ORM</h4><p>先來個免責聲明, <strong>若是需要頻繁地寫多個不同的小型網站, 用 ORM 是利多於弊, 可減少重覆的程式碼。</strong>以下的論點基於我的個人經驗, 需求是長期維護一個資料量大且有嚴苛速度需求的網站。
</p><p>
之前已寫過幾篇 ORM 心得, 在<a href="http://fcamel-life.blogspot.com/2010/06/django-python-database.html">《Django 和 Python 操作 database 時的額外負擔》</a>提到實測大量數據的情況有多慢, 注意我的使用情境有超過百萬筆資料, 若資料量沒那麼大, 這個負擔較無所謂。
</p><p>
在<a href="http://fcamel-fc.blogspot.com/2011/01/blog-post.html">《撰寫資料庫相關程式的心得》</a>有提到使用 ORM 的成本。這裡要補充的是, <strong>無論如何, 我們都需要分開「資料庫的操作」和「邏輯操作」。而在程式裡直接使用 ORM 並沒有隔離好兩者。</strong>
</p><p>
舉例來說, 在頁面裡使用 <tt>get_user("fcamel")</tt> 會比 <tt>User.objects.get(name="fcamel")</tt> 來得好。想想要如何針對這個頁面寫 unit test, 會發覺後者仍和 database 有很大的相依性, 不容易 mock。或著換個說法, <tt>get_user()</tt> 的抽象程度比直接使用 ORM 高。<tt>get_user()</tt> 裡是使用 ORM 還是下 raw SQL, 都和使用者無關。當需要連續幾個 ORM 操作以達成一個目的時, 另外包函式的優勢會更明顯。
</p><p>
若能接受上面的論點的話, 會發覺 ORM 的優勢又少了一點。所以, 我個人的看法是: <strong>重點是必須另外包一層 API 存取資料, 供邏輯操作使用。所以, 對前端開發者來說, 是否使用 ORM, 影響不大。但對後端開發者來說, ORM 的缺點遠大於優點。</strong>ORM 最吸引人的地方是提供不錯介面隔離 database, 可以有彈性地存取資料並且不用太了解如何寫 SQL。但隨著使用經驗漸增, 會發覺這些優點並非事實。但若一直不去了解 database, 不會發覺付出的隱性成本。
</p><p>
題外話, 聽說 <a href="http://www.sqlalchemy.org/">SQLAlchemy</a> 相當強大, 不知實際用起來效果如何。我後來淡化使用 ORM 的場合, 加上需要使用 <a href="http://south.aeracode.org/">South</a> (基於 Django ORM 的 plugin), 不方便換掉, 就沒有研究 <a href="http://www.sqlalchemy.org/">SQLAlchemy</a> 了。
</p><h4>關於 test</h4><p>內建的 <a href="https://docs.djangoproject.com/en/dev/topics/testing/">django.test</a> 相關模組不太好用, 而且執行 test 要先重建全部 table, 然後在每個 test case 前 truncate 全部 table, 效率不好。若能指定只 truncate 需要的 table, 可省下許多時間。通常 test case 是愈寫愈多, 實務上執行 test case 的效率相當重要。
</p><p>
使用 <a href="http://south.aeracode.org/">South</a> 後更會有 production schema 和 test schema 不同的問題, 因為 production 用 <a href="http://south.aeracode.org/">South</a> 建 schema, 中間可能用到客制化的 SQL 改 schema (如建 multiple column indexes), 但 <a href="https://docs.djangoproject.com/en/dev/topics/testing/">django.test</a> 不會呼叫 <a href="http://south.aeracode.org/">South</a>, 而是用 <a href="https://www.djangoproject.com/">Django</a> 原本讀 models.py 建 schema 的方式。我後來改用自己寫的模組來建 test database, 沒研究後續發展, 不知後來是否有修正。
</p><h4>關於 coding style</h4><p>Django 違反許多 <a href="http://www.python.org/dev/peps/pep-0008/">PEP 8</a> 的規則或 Python 精神, 像是:</p><ul>
<li> 在程式碼裡面 import 別的 module (而非開頭), 並且有 circular import。</li>
<li> 有多種方法做一件事。我很討厭這點, 像自訂 login 的重導頁面卻沒成功, 除錯時很麻煩, 要搞清楚多種規則的執行順序, 才知道問題出在那。</li>
<li> 有許多 lazy initialization。我不確定這是否違反 Python 社群的 "explicit is better than implicit", 我個人不喜歡一堆 lazy initialization, 很難掌握程式的行為。</li>
</ul><p>
以上這些事對使用者有什麼影響? 當行為不合預期, 文件也看不出所以然時, 讀原始碼並加 log message 是滿有效率的除錯方法。上述都是我在研究功能 (像是如何使用 cache) 或除錯時, 讀原始碼遇到的困擾。
</p><h4>結語</h4><p>除前面一再強調的「結論」外, 再多強調一下, 一個東西愈多人罵, 表示愈多人用, 出事也愈好處理。本篇不是建議別用 <a href="https://www.djangoproject.com/">Django</a>, 也不是反串推廣, 只是之前一年多的使用心得。</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-40785791606515637752011-09-18T11:39:00.001+08:002011-09-25T21:06:41.677+08:00Problem Solving 的技巧 (3):因事制宜<p>不知不覺,這類文章可以寫到第三篇,感到頗意外的。前兩篇是:</p><ul>
<li> <a href="http://fcamel-fc.blogspot.com/2010/11/problem-solving.html">《Problem Solving 的技巧 (1):系統設計是一連串的取捨》</a></li>
<li> <a href="http://fcamel-fc.blogspot.com/2011/04/problem-solving-2_6909.html">《Problem Solving 的技巧 (2):別把解法當作問題定義》</a></li>
</ul><p>
昨天看到 TonyQ 在 Soft_Job 寫的文章:<a href="http://www.ptt.cc/bbs/Soft_Job/M.1316191715.A.323.html">《Re: [閒聊] 你在開發程式時,是重視績效還是品質</a>。覺得深有同感,摘錄幾段如下:</p><blockquote>對我們來講很多其實可以是 nice to have 的東西,
都會被我們當成 must have。<br />
<br />
這個判斷是經驗跟 domain 累積下來的,沒有公式,沒有法則,
做久了你就是會知道什麼架構之後會一直噴 exception ,<br />
<br />
而且你還會知道等他出事時你一定沒辦法好好處理,
所以你要在這個當下把它處理掉。<br />
<br />
...<br />
<br />
觀察他們影響到哪些地方,你有沒有能力測試他們,
還有你的環境允不允許這件事情帶來的不穩定性。<br />
<br />
一般來講,如果是我個人自己的專案,我非常不介意大改,
只要我後續還有時間可以處理這些出來的問題。<br />
<br />
對公司或者客戶的專案,我會採取相對保守的態度。<br />
<br />
<b>這是對風險管理的策略問題。</b><br />
</blockquote><p>
在 Plurk 上貼了<a href="http://www.plurk.com/p/e0nbzi">這篇文章</a>後,<a href="http://www.plurk.com/Thinker">Thinker</a> 和 <a href="http://www.plurk.com/qrtt1">qrtt1</a> 提出另一面看法,強調<strong>「早期發現,早期治療」</strong>的好處。於是決定借題寫一下自己的看法。
</p><p>
在討論這個議題前,我想先強調,為了方便聚焦討論,我們往往會先偋除一些條件,或是先依自己的假設開始論述。<strong>而有爭論的地方,往往卻是這些隱藏的前提。</strong>好比說<a href="http://fcamel-fc.blogspot.com/2009/06/unit-test.html">「寫測試碼重不重要?」</a>單單這樣一個命題,只能討論出很模糊的概念,不論支持與否,看起來都有些道理。但加入一些條件後,像是「這是長期開發並有多人參與的專案」或是「後天要交的雛型」,相信大家對此會有不同的答案,也會少一些分歧。相關的想法,可以參考<a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010399930">《黑天鵝效應》</a>。 <a href="http://hoamon.blogspot.com/2009/07/1000-1001-999-1001-2009-1-1-5-2008-12.html">這篇</a>有摘要讀書心得,其中敘事謬誤和戲局謬誤啟發我這個觀點。
</p><p>
我的想法是,在達到必要需求的前提下,依個人以及團隊的能力,看看能提昇多少品質。問題在於,何謂 must have、何謂 nice to have,每個人的見解不同。<strong>爭議點在於,每個人的能力不同,導致評估的實作成本不同。</strong>一樣是 nice to have,有些被認同可以做,有些則否。
</p><p>( ps. 這篇文章不討論規格不明確的問題,這是另一個大議題。 )
</p><p>
舉例來說,A 認為現在重構只能提升一點品質,卻要花兩天;B 認同 A 評估的品質,但 B 覺得只要花半天。所以 A 覺得不划算,而 B 持相反意見。從這樣的評估結果很難說 A 或 B 誰對誰錯,也許 A 沒重構經驗不信任重構,也可能 A 有豐富的經驗,估得比 B 準。
</p><p>
<strong>在體認到個人能力不同、<a href="http://fcamel-fc.blogspot.com/2010/05/blog-post.html">習慣</a>不同的前提下,對自己的要求是盡量提高自己的能力,多付出時間提高品質,</strong>減少日後維護成本並能練功,形成<a href="http://chingyichan.wordpress.com/2010/04/26/cycle/">正向循環</a>;<strong>對其他人來說,看對方是那種人,是「過」或「不及」,再從另一個角度和對方討論。</strong>
</p><p>
舉例來說,若團隊成員不熟或不認同<a href="http://fcamel-fc.blogspot.com/search/label/Testing"> unit test</a>。當下就要出貨了,這時硬要大家學習並實作測試,並不適合 (不如當「負面教材」,待下次專案的開頭提出討論)。但若成員有過相關經驗,同樣時程下,就能針對核心程式補些測試,花最小成本減少最多風險。當成員嘗到測試甜頭,不小心寫過多測試碼時,和對方討論優先順序,減少寫 C/P 值低的測試碼。
</p><p>
回頭看品質的問題,能力愈強、經驗愈豐富,增加 nice to have 的成本愈低,自然能前期處理,減少<a href="http://en.wikipedia.org/wiki/Technical_debt">技術債</a>。反之,在時程的考量下 (ship or die),能力和經驗不夠時,得選擇先放過一些潛在問題,日後仍有需求時,再花更多成本補救。先活下去,才有更多的本錢來還債。
</p><p>
<strong>因事制宜說來簡單,只有四個字而已。實務上卻需經年累月的經驗,</strong>不止要考量時程、未來需求變化等項目,也要留意每個人的能力和習性,才能在當下找到較為適當的平衡點。而增加經驗的方法,如同杜書伍在<a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010449478">《打造將才基因》</a>裡所言,除了比別人投入更多時間外,沒有其它的捷徑。
</p>
<p>
<b>2011-09-25 更新</b>
</p>
<p>
看到 Thinker 提了他的相關看法:<a href="http://www.codemud.net/~thinker/GinGin_CGI.py/show_id_doc/457">《程式碼要清的多乾淨?》</a>,裡面提了不錯的建議:<blockquote> 除了能力不同之外,個人的膽量、積極態度和價值觀也同樣影嚮著評估的差異。 我強調的是,積極態度和個人的能力呈正相關。<br />
...<br />
<b>要評估自己的能力,並適度的承受犯錯的風險。</b></blockquote>
勇於嘗試、有控制性地犯錯,對於學習很有幫助。我覺得在我學習的前一大段生涯裡,過於謹慎而不敢犯錯,以致於學習速度較為緩慢。而我認識一些晚起步、卻成長相當快的朋友,都具有大膽嘗試、不斷犯錯的特質。他們快速地累積經驗,並培養出更多的膽識和更大的企圖心。
</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com3tag:blogger.com,1999:blog-451051488891335923.post-74682173284472991442011-08-07T12:41:00.005+08:002015-08-17T21:42:56.326+08:00Python 的特別之處 (1)<p>從新手的眼中來看 Python,比較能看出 Python 和其它語言不同之處。最近有機會幫別人快速上手 Python,就順便整理一下我從中發覺 Python 較為突出的優點。 </p><h4>list、dictionary and string</h4><p>平時 coding 最常用到的 container 就是 list 和 dictionary,另外也會常用到字串操作,Python 提供方便的方法來操作它們。string 可看成一個有實作 list 介面的類別,一些常用操作像是 slice:"abcd"[1:3] 回傳 "bc";負數的索引: "abcd"[-1] 回傳 "d";直接和 for-loop 整合在一起:</p><pre class="prettyprint">In [1]: for ch in "abcd":
....: print ch
....:
a
b
c
d
</pre><p>讓存取這些常用資料型態輕鬆許多。 </p><h4>iterator</h4><p>使用 iterator 比傳統的 <tt>for (i=0; i<n; i++)</tt> 來得清楚,Python 針對 iterator 下了不少工夫,提供好用的輔助函式,像是 enumerate 補足需要用到 index 的情況:</p><pre class="prettyprint">In [2]: for i, n in enumerate([1, 3, 5]):
....: print i, n
....:
0 1
1 3
2 5
</pre><p>使用 zip 整合多個 list:</p><pre class="prettyprint">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
</pre><h4>map, filter and reduce</h4><p>任何使用過 map 的人,都會喜歡 map 輕巧的用法,來看幾個例子:</p><pre class="prettyprint">In [1]: map(int, ["12", "37", "999"])
Out[1]: [12, 37, 999]
In [2]: map(str, [12, 37, 999])
Out[2]: ['12', '37', '999']
</pre><p>int 是一個函式,將傳入的物件轉成整數;str 則是轉成字串。使用 map 可以將一個 iterator 轉為另一種 list。 </p><p>另一個常見的情境是,從一個 list 裡篩選出需要的物件,比方說只留下偶數:</p><pre class="prettyprint">In [1]: numbers = [1, 2, 3, 4, 5]
In [2]: filter(lambda x: x % 2 == 0, numbers)
Out[2]: [2, 4]
</pre><p>或像 <tt>filter(lambda s: s.endswith('.py'), file_names)</tt> 只留下結尾為 ".py" 的字串。 </p><p>除 map 和 filter 的重心放在轉換 list 之外,reduce 則是將 list 匯整成一個物件。有了這些函式,就能任意的操作 list,用以匯整或擴散資料容器。 </p><p>比方說將一串數字加起來:</p><pre class="prettyprint">In [1]: numbers = [1, 2, 3, 4, 5]
In [2]: reduce(lambda x, y: x + y, numbers, 0)
Out[2]: 15
</pre><p>上面這個例子可以用內建的 sum 取代,來看另一個複雜點的例子,將一串 0、1 值合成一個整數:</p><pre class="prettyprint">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
</pre><h4>list comprehension</h4><p>map 和 filter 雖然方便,要用到 lambda 或是混合使用時就沒那麼好讀了。Python 提供一個重量級的武器 <a href="http://en.wikipedia.org/wiki/List_comprehension">list comprehension</a> 來解決這問題。比方說留下偶數並乘以三再加一:</p><pre class="prettyprint">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]
</pre><p>綜合以上的語法,可以輕鬆地寫出易懂的 <a href="http://en.wikipedia.org/wiki/Quicksort">quick sort</a>:</p><pre class="prettyprint">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)
</pre><p>對於習慣 C、C++、Java 世界的人來說,應該不曾看過這麼直覺易懂的 <a href="http://en.wikipedia.org/wiki/Quicksort">quick sort</a> 吧。 </p><h4>tuple</h4><p>tuple 是一個很妙的資料結構,它和 list 的主要差別是它是唯讀的,Python 裡鮮少有這種唯讀物件。不過它較易發覺的好處是被用在 Python 的 <a href="http://en.wikipedia.org/wiki/Assignment_(computer_science)#Parallel_assignment">parallel assignment</a> 和函式傳回值。 </p><p>於是在 Python 裡可以這麼寫:</p><pre class="prettyprint">a, b = b, a # swap
</pre><p>Python 在看到 <tt>b, a</tt> 時會產生一個 tuple 表示 (b, a),再透過 tuple 達到 <a href="http://en.wikipedia.org/wiki/Assignment_(computer_science)#Parallel_assignment">parallel assignment</a>。 </p><p>函式也可以一次「傳回多個結果」:</p><pre class="prettyprint">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
</pre><p>原理一樣是先轉成 tuple 再傳回,再視等號左側放什麼,決定要存成 tuple 或做 <a href="http://en.wikipedia.org/wiki/Assignment_(computer_science)#Parallel_assignment">parallel assignment</a>。 </p>
<h4>2012-01-25 更新</h4>
<p>
應該沒什麼力氣更新續篇,在這裡簡短描述一下,有興趣的人可以找看看相關介紹。
</p>
<h4>with</h4>
<p>
在 Python 2.6 後,支援用 <a href="http://effbot.org/zone/python-with-statement.htm">with</a> 管理資源。像讀檔案可以用 with 的方式寫:
</p>
<pre class="prettyprint">
# 印出所有使用者的 id
with open('/etc/passwd') as fr:
for line in fr:
print line.split(':')[0]
</pre>
<p>
在進入 with 的 block 前,會呼叫 file object 的 <tt>__enter__</tt> 以獲得 file descriptor;在離開 block 前會呼叫 <tt>__exit__</tt> 關掉 file descriptor。即使中間呼叫了 <tt>sys.exit()</tt> 或丟出 exception,仍會執行到 __exit__,不用擔心會漏關。方便用在許多情境 (比方說 lock / unlock、自動 flush output buffer),易讀易用。
</p>
<h4>內建常用函式庫</h4>
<p>
除上述的基本資料結構和 string 外,還有 <a href="http://docs.python.org/library/sqlite3.html">sqlite</a>、<a href="http://docs.python.org/library/json.html">json</a>等。
</p>
<h4>簡單不易出錯的語法</h4>
<p>
舉幾個寫 C 可能發生的問題,但在 Python 的語法下則不會發生:
</p>
<pre class="prettyprint">if (condition);
{
// BUG!! 這裡的程式一定會被執行
}
</pre><pre class="prettyprint">if (x < 60)
number_of_fail++;
total_fail_score += x; // BUG!! 這行每次都會執行
</pre><p>另外,由於 Python 的 condition 只能是 expression,不能是 assignment。不會有 <tt>if (x -= 3 < 0)</tt> 這種 bug。
</p>
<h4>ipython</h4>
<p><a href="http://fcamel-life.blogspot.tw/2013/09/ipython-python.html">有 ipython 方便快速試語法、試函式庫還有開發程式。</a>ipython 比簡單的 interactive interpreter 強大許多。</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com3tag:blogger.com,1999:blog-451051488891335923.post-42495780416532827012011-07-09T17:07:00.002+08:002014-01-05T13:55:40.691+08:00使用 Mockito 更輕鬆地寫 unit test<p>聽 <a href="http://blog.vgod.tw/">vgod</a> 提到可以用 <a href="http://code.google.com/p/powermock/">PowerMock</a> 來換掉 static、private、final 的方法,還有 <a href="http://mockito.org/">Mockito</a> 用起來很順手。於是大概看了一下相關文件:</p><ul><li> <a href="http://code.google.com/p/powermock/">PowerMock</a> 的作法是換掉 classloader 和修改 bytecode,所以它可以跨越 Java 的限制 (像是無法換掉 final)。聽起來很威,感覺很危險,不確定是否夠穩定。應該不會想試它。</li>
<li> <a href="http://mockito.org/">Mockito</a> 承接 <a href="http://easymock.org/">EasyMock</a> 的思維,但是用起來更容易。雖說<a href="http://code.google.com/p/mockito/wiki/FAQ#What_are_the_limitations_of_Mockito">它也無法換掉 static、private、final 等方法</a>。 </li>
</ul><p>我花了一些時間看了 <a href="http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html">Mockito 範例</a>和作者設計的思路,覺得很有意思。 <a href="http://hamletdarcy.blogspot.com/2007/10/mocks-and-stubs-arent-spies.html">《behind the times: Mocks and Stubs aren't Spies》</a> 提到在寫測試碼時,替換掉實際互動的元件,有四種不同層級的輔助元件: dummy、stub、<strong>spy</strong>、mock:</p><ul><li> dummy: 什麼事也不做。</li>
<li> stub: 依據輸入傳回物件,藉此控制後續的邏輯。(相對來說) 不在意被呼叫的方式,像是何時被呼叫、呼叫了幾次等。</li>
<li> spy: 用來確認該物件如何被使用。比方說呼叫 cursor 物件的 commit() 前有沒有先呼叫 execute()。</li>
<li> mock: 同stub + spy,既需要傳回物件供待測方法使用,也在意它如何被呼叫。</li>
</ul><p>寫測試碼常遇到的困擾是:mock 太囉唆了。<a href="http://hamletdarcy.blogspot.com/2008/03/mockito-new-mock-framework-on-block.html">這篇</a>提供一個小例子對照 <a href="http://mockito.org/">Mockito</a> 和 <a href="http://easymock.org/">EasyMock</a> 的差異。<a href="http://monkeyisland.pl/2008/07/12/should-i-worry-about-the-unexpected/">《should I worry about the unexpected?》</a> 解釋 mock 之所以囉唆,是因為它管太多了,導致加新功能時,常常行為沒錯,卻無法通過舊的測試。使用 spy (<a href="http://mockito.org/">Mockito</a> 的主要功能) 就不會有這種困擾。大多情況我們並不在意是否每一個物件都要如預期般執行,只要關鍵的幾個步驟沒錯即可。 </p><p>之前會排斥使用 mock 的原因為:</p><ol><li> 沒有和真正的物件互動,不夠踏實。</li>
<li> 寫起來很囉唆。</li>
<li> 囉唆就算了,稍微改改程式還很容易出錯。</li>
</ol><p>第一點是 trade-off,有時候是不得不做的必要之惡。在看到 spy 的概念後,發覺它少了後兩項缺點。之後需要用到「mock」時,再來用 <a href="http://mockito.org/">Mockito</a> 看看。<a href="http://mockito.org/">Mockito</a> 另一個好處是,它有<a href="http://code.google.com/p/mockito/wiki/MockitoForOtherLanguages">提供各種語言的版本</a>,可以學一套語法走天下。</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-75910975718269024842011-06-02T22:18:00.012+08:002014-01-05T13:55:40.700+08:00ego-post!! 即時同步編寫 wiki code 和顯示 html 畫面<img alt="Ego" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8WqqITSyOuJzxePpWRDMCt70a66Jry4WuXyi8K1rCERkT1pTtJe2PC7s9GqDQmFVw9ldZFMiij8bTp7YxB73EuGDJ6iyRvH0nk9P2HrsS56LMQAe95bv9mt64UxUvYxWJNYqpJpUPaxPk/s400/Ego03.png" title="Ego" /><br />
<br />
對一個時常寫 blog 的人來說, 好的編輯介面相當重要。以前寫過 <a href="http://fcamel-fc.blogspot.com/2009/04/blog-td-post.html">td-post</a> 以節省重覆輸入同樣連結的時間。但 td-post 有個問題, 每次改完文章又要重執行一次。我常常修來修去, 用起來有點彆手。後來偷懶, 就改用 blogger 的編輯介面。<br />
<br />
但 blogger 的所見即所得並不太準, 有時會包含 div 有時不會; 有時會多空行; 複製網路上的文字時, 常會不小心連帶複製到文字的格式, 之後不方便修改。於是得切到 blogger 的 raw html 編輯介面修正這類問題, 再切回來繼續寫, 還挺麻煩的。<br />
<br />
去年 <a href="http://traditionalchineseblog.scottt.tw/">Scott</a> 和強者學弟 Will 幫高中學弟妹寫了個 <a href="http://code.google.com/p/itrs-test3/">on-line judge</a>。server 端用 Google App, client 端網頁裡嵌了 telnet client, 連到綁好的 local server (透過 QEMU 執行), 用來即時開發程式、編輯和提交程式。並有一個所見即所得的 wiki 編輯器, 用來編題庫。整個工程相當驚人 (或著該說是.......吃飽太閒)!<br />
<br />
整個專案有不少地方值得深入玩玩, 而我最有興趣的, 是所見即所得的 wiki 編輯器。畢竟, <a href="http://fcamel-life.blogspot.com/2011/05/virtual-box-host-os-guest-os.html">用 putty 連到 VirtulBox 裡的 Ubuntu</a> 開發還是方便許多。但 wiki 編輯器卻沒其它替代品。<br />
<br />
昨天晚上錯過早睡的時機, 自暴自棄地開始拼湊程式, 今天晚上再改一改就有個不錯的雛型。寫好的東西放在<a href="http://code.google.com/p/ego-post/">這裡</a>, 有興趣的人可以看看。如同大家所猜, 這篇文章是用 ego-post 打出來的。剛好前陣子<a href="http://fcamel-daily.blogspot.com/2011/05/ratatouille.html">在看《料理鼠王》練英文</a>, 就順便用 Ego 命名了。<br />
<br />
待做事項<br />
<ul>
<li> 提供新的語法用來貼程式碼。</li>
</ul><br />
2011-06-02 更新: 有圖有真相, 附上 screenshot:<br />
<a href="https://picasaweb.google.com/lh/photo/u6c6BJurbwhUhF5Y0NPGzEZZhvS8A7xyyi0-E9QamLM?feat=embedwebsite"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjytr2SgUyoqrGWbb8v7F8kmQUF_Cmpof8BU2NZqNwUPV5cjNSz8su5_oSx9noDo_d_eLWmruaDe6M9D7YOJ0nmYvyoRqtaV3noJ4OCF9lGhBYSlBr2YpqutC801lc5tjjF0RnMYLjJr7qO/s400/ego_post_screenshot.png" height="284" width="400" /></a><br />
<br />
2011-07-09 更新: 補上自動記錄 link 和發文到 blogger 的功能, 並在 <a href="http://code.google.com/p/ego-post/">project 首頁</a>補上如何執行。
<br />
2012-08-07 更新: 隔了一年多, 終於受不了<a href="http://code.google.com/p/ego-post/source/detail?r=9b58ed4ca89c6f17d2a413bf0b5f01d9acd59793#">補上 auto save 的功能。</a>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com3tag:blogger.com,1999:blog-451051488891335923.post-17308196466083108102011-04-02T11:49:00.006+08:002011-05-15T00:56:15.030+08:00Problem Solving 的技巧 (2):別把解法當作問題定義<p>在<a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010470961">《真正的問題是什麼?你想通了嗎?》</a>裡提到一點重要的觀念,別把別人的解法當作問題。最近累積了不少實例,比較能清楚地表達這個想法。</p>
<p><span></span></p>
<h4>「把解法當作問題」的意思以及它帶來的負面影響</h4>
<p>在分配工作時,為了減少溝通成本,常會隱藏一些訊息,只告訴合作同伴他「應該」知道的事,而省略了原始的動機和考量。問題在於,實際執行的人通常比對方清楚細節,很多問題在執行後才會浮現。但分配到工作的人不清楚前因後果,即使查覺不對勁,也無法 (或不願) 做出進一步修正。</p>
<p>好一點的情況,在分工前會先進行討論。然而,討論常會陷入各執一詞的局面,討論雙方作法優缺點後,會發覺找不到共識。實際上這只是眾多解法的兩個提案而已,若深入討論雙方作法背後的動機,各自預先假定的前提,會發覺尋找共識並不困難,畢竟,雙方是在解同一個問題。</p>
<p>在需求一樣,假設一致的情況下,兩個頭腦清楚願意討論的人,沒道理找不到共識,只是我們習慣省略前提,也沒先釐清需求,直接討論解法,才會覺得有個看不見的牆擋在中間,對話難以有交集,我不認同你認同的點,你也不認同我認同的點。</p>
<p>當我開始不預設立場後,看清楚很多事,我第一件事不是評斷大家作法的好壞,而是先問這樣做的考量為何?從考量的點會問出需求,從需求和考量的點會發覺矛盾,從而發現隱藏的假設。接著就能討論假設是否成立,或是不成立又會如何?很可能所有的點會重新洗牌,然後聚焦出更清楚的需求。</p>
<p>按部就班從需求討論各種作法,尋找大家認同的假設,接著在假設下收斂可能的作法,或是列出有影響但還不確定的點,然後評估何時由誰來釐清不確定的點,再來做更細的決定。這樣做下來,通常會有大家都滿意的成果,互動的過程中也會相處愉快。</p>
<h4>以開發網站為例</h4>
<p>舉例來說,A、B 要合寫一個網站,A 想用 PHP 直接寫,因為他覺得大家都會寫 PHP,寫起來也快;B 想用 Python + Django,因為他覺得日後比較好維護。</p>
<p>A 和 B 的想法都沒錯,但是一討論就陷入死結,這是很吊詭的事,兩個人想法都對,也在做同一件事,為什麼不會有共識?</p>
<p>這表示上述的論點一定有其中一點是錯的,仔細思考會發覺,其實 A 和 B 「沒有在討論同一個問題」,也沒有在同樣的立足點 (假設) 上討論。</p>
<ul>
<li>A 可能假設時程很趕,沒有時間慢慢從頭學 Django,若他不會 Python,還得多學 Python,而且他可能不想學新東西,覺得 PHP 用起來沒什麼問題,為什麼要自找麻煩用別的作法?</li>
<li>B 可能假設學習 Python + Django 很簡單,即使時程很趕也還好,或是時程可以再談,沒有人說一定要什麼時候出來,在 B 認為可以來得及做完的前提下,B 認為要認真考量維護的事。</li>
</ul>
<p>從 A 和 B 隱藏的假設可看出一些衝突:學習新東西花的時間成本、對於時程的認知、是否需要重視維護。而這些假設大概和 A、B 各自過去的經驗有關,比方說 B 收拾過用 PHP 開發的爛攤子,A 沒有和多人共同開發過稍具規模的專案。若 A、B 能和對方說明自己的前提,就有機會順著前提再討論為何有這樣的前提,彼此較能接受對方的看法。</p>
<p>然後再對照目前的需求,會發覺有些前提不再成立,或是更為重要。像是 A 當初收拾的爛攤子是別的因素造成的,可能是時程太趕或開發者習慣不好,主因不是 PHP。</p>
<p>將這些事都攤出來討論後,會發覺幾個立基不牢靠的推論:</p>
<ul>
<li>為了方便維護,要用 Python + Django。</li>
<li>為了快速開發,要用 PHP。</li>
</ul>
<p>這是將需求和解法綁在一起討論,但是實際的情況是,方便維護的作法不只有 Python + Django,快速開發的作法也不只有 PHP。視開發人員的經驗,將兩者反過來陳述也可能成立。</p>
<p>所以,真正考量的點應該是:</p>
<ul>
<li>先確定時程和功能需求,才能判斷開發速度要多快。</li>
<li>確定日後是否需要擴充,擴充的程度和時程,從而判斷維護的成本。</li>
</ul>
<p>然後列出相關選項,需注意的是,要依現有人員的能力列,而不是「一般」的認知。若時程不是太緊,可以先排時間評估各種方案,像是 Python 不只有 Django 一家 framework,也許可以考慮 Flasky 或 Pyramid;PHP 也不是沒 framework,若願意犠牲多一點執行速度,可以研究 PHP 的 framework;或是另找別的路,用 Ruby on Rails 或用 Java 體系的解法。</p>
<p>要注意的是,過猶不及,當考量的點過於發散時,不確定的點太多,沒有心力一一確認,討論會流於空泛。這時要列好需求,依現有人員的能力做些假設,先達成局部共識再繼續討論,比較容易聚焦。像是因為內部人員最熟 PHP 和 Python,用這兩者風險較低,所以只考慮這兩個體系的解法。</p>
<h4>結語</h4>
<p>類似的例子很常發生在日常生活的討論裡,不限於技術方面。隱藏的前提不見得都是技術議題,人性是很複雜的。關鍵在於別把別人提的解法當問題,自己也別預設立場著重在說明解法,而沒說明問題需求和自己的假設 (前提)。</p>
<p>在討論過於發散時,要先取得共識排除一些考量。即使共識的原因只是「不為什麼,我們都覺得不重要」,也是不錯的作法。可以先聚焦往後討論,待討論到後面有更明確的想法,或是再和第三個人討論時,可能會查覺原本的前提有問題,這時再回頭修正它們,重新沙盤推演一遍,得到更確實的解法。有了紮實的共識,清楚各項決策的前因後果後,之後遇到各種變動,都能迅速明確地處理。</p>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/QmD_PFdb794" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-74905168032937080042011-02-19T15:08:00.001+08:002011-05-15T00:56:16.104+08:00DRY 的缺點以及測試碼的衝突<p>這篇是看到 Pylons (Pyramid) 的 <a href="http://docs.pylonsproject.org/community/testing.html">《Unit Testing Guidelines》</a> 後寫的心得。</p>
<p>以前我覺得每件事都有標準答案,或是所謂的「Best practice」。後來才發覺這是很嚴重的錯誤認知。因為希望能簡單地處理事情,而一廂情願地認定有「標準答案」,結果忽略了許多反面的訊息。<a href="http://fcamel.twbbs.org/archives/2010/04/24/1053/">《百人百觀》系列</a>裡道出我的心態轉變。</p>
<p><span></span></p>
<p>以 DRY (Don’t Repeat Yourself) 來說,這是資訊人奉為終旨的鐵則,可應用到各種情境。這裡我們先縮小範圍,討論 DRY 對於寫程式的影響。它的優點顯而易見,只需要改一處程式,不會因漏改程式而產生 bug。重覆的程式碼容易造成 bug,複製貼上是常見的主因,甚至有 paper (<a href="http://www.computer.org/portal/web/csdl/doi/10.1109/TSE.2006.28">CP-Miner</a>) 提出方法自動偵測這種 bug。</p>
<p>但是 DRY 的缺點呢?造成 client codes 之間的相依性,迫使所有 client codes 共用同一介面,這帶來不少問題:</p>
<ul>
<li>寫錯共享程式時,影響不止一份程式。</li>
<li>最簡單的情境得配合最複雜的情境使用,增加簡單情境的維護成本。即使介面設計的很完善,不需更改呼叫方式,執行時勢必多了一些檢查手續,或在空間上做了些妥協,提高時間和空間的成本,各種 framework 是最好的例子。</li>
<li>承上,像 Django 的 session 為了能存各種 object,選擇以 dict 表示 session,直接序列化 session 物件存到資料庫或檔案裡。為了簡化實作並提供無限的空間存 session 資料,用 MySQL 時選擇用 LONGTEXT 以儲存無限制大小的資料,造成每次取資料都要從 disk 讀。在大量使用者連入的時候,這會是個問題。</li>
<li>變更一處 client codes 的需求,可能會影響共享程式的介面。選擇相下向容的話,介面會變複雜,可能會多一些選擇性參數。邏輯變複雜,共享程式容易寫錯,client code 使用方式也變複雜。</li>
<li>承上,選擇改變介面的話,需找出影響到的 client codes。對 dynamic typing 的語言來說,這是件苦差事,甚至無法 100% 保證沒有遺漏。</li>
</ul>
<p>如同<a href="http://fcamel.twbbs.org/archives/2010/11/21/1081/">《Problem Solving 的技巧》</a>裡說的,每個方法都有帶來的好處,也有帶來的壞處,也有針對壞處所做的後續修補。關鍵在於弄清楚現在的需求,明白各項設計的優缺點,配套作出一連串的設計,以獲得整體的最大效益。比方說用 VCS 切 branch 可以減少介面相容問題,不過會多出維護 branch 的成本,那是另一個議題了。</p>
<p>自從意識到 DRY 帶來的成本後,我覺得有些困惑,因為它不再是 100% 正確、用了一定好的原則。在寫測試碼時,我感到更困惑,若測試碼也變複雜,之間有相依性,那誰來保證測試碼是正確的?更何況一個具有完備測試碼的專案,測試碼和產品碼的比例將近 1:1,在量如此大的情況下,測試碼的邏輯太複雜的話,測試碼容易出錯,會造成不少問題。我體驗過測試碼寫太複雜而造成測試碼有錯,因測試碼出錯而誤以為產品碼有錯,結果費了更多力氣才找出錯誤 (程式碼變兩倍)。也體驗過在 setUp 或其它初始化部份出錯,造成訊息混亂,無法掌握錯誤的源頭。後來就不知不覺地將測試碼寫得很簡單,也漸漸減少犯這些錯的機會。</p>
<p>昨天看到 Pylons (Pyramid) 的 <a href="http://docs.pylonsproject.org/community/testing.html">《Unit Testing Guidelines》</a>後,才串起過去的經驗,發覺問題的源頭在於 DRY 並不適合用在測試碼,但是 DRY 已成為根深蒂固的習慣,壓根兒就不會想到將重覆程式碼抽出整理成跨 method / class / module 的行為,反而是妨礙測試碼品質的元兇。該篇文章有精闢的說明和例子,推薦大家參考。其中有些規則,現在還不能掌握使用後的優缺點,之後再抽時間讀讀 Pyramid 的原始碼,應該能學到一些東西。</p>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/J2EiC1lpuTE" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-20628297537346237932011-01-08T23:08:00.000+08:002011-05-15T00:56:17.076+08:00撰寫資料庫相關程式的心得<p>我是用 MySQL + Django,處理的資料量有小有大。資料量大的情況下,通常有上萬筆,甚至會到上億筆。相關心得大概分成四類,依實務經驗記錄一下心得。</p>
<p><span></span></p>
<h4>
是否應該使用 ORM?</h4>
<p>我是使用 Django ORM,以下指的 ORM 問題可能不適用全部 ORM framework,但我猜大部份應該是半斤八兩。</p>
<p>剛開始不熟 SQL 時,很喜歡用 ORM,ORM 有些學習門檻,不過習慣後用起來相當順,也容易閱讀程式。但是在好寫好讀的背後,卻犠牲掉極大的效率。原因有幾點:</p>
<ul>
<li>需要大概了解 ORM 產生的 SQL,才知道如何寫出有效率的操作。比方說用到 foreign key 時,可以在取物件時順便 join。若沒特別處理,預設行為是參考到關聯物件才取資料,於是取一萬個物件並讀取它們的關聯欄位,就會多下一萬次 SQL。</li>
<li>即使了解 ORM 各項操作避免一些地雷寫法,ORM 不見得能產生最快的操作方式。明顯的缺點是讀寫 N 個物件時,很可能會轉成 N 次 SQL,而不是一次。</li>
<li>ORM 為了提供一致的抽象介面,沒有支援各家 DBMS 完整的語法,減少一些最佳化的機會。如缺少批次操作,以及使用 force index、決定 join order、技巧性地用 IN 不用 range query 等。</li>
<li>即使 SQL 沒有問題,產生 object 的時間成本比自己執行 SQL 取資料來得高 (見<a href="http://fcamel-life.blogspot.com/2010/06/django-python-database.html">這篇</a>),資料量大的時候會變成瓶頸。</li>
</ul>
<p>我一開始寫的專案全用 ORM。第二個寫的大部份用 ORM 但遇到一堆難解的效率問題。第三個寫的開始刻意減少 ORM 操作。最後則是全面禁用 ORM。原因很簡單,弄懂 ORM 操作並做最佳化的時間,比直接寫函式封裝 SQL 操作多,而且最後達成的效率又較差。除此之外,複雜的 ORM 操作可能有 bug 或是令人誤會,導致取出不對的資料,看 ORM 產生的 SQL 才明白問題出在那。</p>
<p>愈懂 MySQL 後,愈覺得 ORM 不順手,最後就改成寫模組封裝 SQL 操作。結論是,若有意願硬啃 DBMS 相關知識的話,將時間投資在所用的 DBMS 上,會比學習 ORM 操作和理解背後運作方式划算。</p>
<p>也許有人會質疑不用 ORM 會增加換 DBMS 的成本,我沒這樣的經驗不清楚用了 ORM 能省下多少成本,相較於前述的問題,整體來說是否划算。至少我會選擇先專精一個 DBMS,還有自己寫模組隔離應用層邏輯和資料庫操作,減低轉換 DBMS 的成本。</p>
<h4>
Database migration tool</h4>
<p>雖然我上面將 Django ORM 說得很慘,但是用 Django ORM 搭配 <a href="http://south.aeracode.org/">South </a>到是滿不錯的。South 是 Django 的 migration tool,提供一個框架維護資料庫的變動,並且可以偵測 Django model 的變化,產生對應改變 schema 的操作。在說明 South 的優點前,要先談談為何需要用 database migration tool。</p>
<p>使用 database migration tool 有兩個好處:</p>
<ul>
<li>記錄目前這版程式用的 database schema。既然程式碼需要版本記錄,database schema 當然也要一併記錄,才能確保每版都能正常運作。</li>
<li>方便其他組員更新資料庫。更新程式碼後執行 database migration,就能擁有和其他人同步的資料庫。</li>
</ul>
<p>當然,有很多方式可以達成以上目的,像是每次更新 schema 就 dump schema,並存成一個 SQL 檔存在 VCS 裡。我沒這樣做過,不知會有什麼大問題。目前只想到幾個小問題:不方便多人同時修改 schema,之後要 merge schema 可能會比較麻煩,特別是改到同一 table 時。不方便追踪 schema 各步的轉換,像是加入 table A、B、C 以支援功能 X。但是回頭翻 VCS log 似乎也能滿足這個需求。唯一無解的大概是有些情境拆開 schema 執行會比較有效率。像是先建好 table、填完實體資料後,再建 covering index。</p>
<p>若改成維護多個 SQL 檔,第一個起始 SQL 產生基本 table,後面的 SQL 都是「schema diff」,則方便多人同時開發。但要人工產生 schema diff 有點辛苦,沒記好每個操作,手動改完 table 後,要回頭比對差別才能寫出 schema diff,容易出錯並增加確認的成本。</p>
<p>除了滿足基本需求外,South 另外提供下列功能:</p>
<ul>
<li>提供單線前進的 migration 方式。能在各版本之間前進、後退。</li>
<li>migration 分成 schema migration 和 data migration。並且提供偵測 Django model 變化自動產生 schema migration 的程式碼。data migration 只是空殼,由工程師自己填程式碼。</li>
<li>提供修改 schema 的 API,像是加減欄位、加減 index。</li>
</ul>
<p>使用 South 的額外好處是,可以避開 Django model 的限制,像是不支援多欄 index、不支援使用不同的 MySQL Engine。用 South 的話,只要自己在 schema migration 裡用 alter table 修改即可。</p>
<p>不清楚別家 database migration tool 怎麼運做的,感覺這方面的工具有很大的發揮空間,值得了解一下各家工具提供的功能。目前遇到的最大困擾是,無法明確看出那些 migration 有相依關係,更新資料或程式時,不方便只執行有影響到的範圍,若更新在很前期的 migration,就得回溯到前面再重跑。</p>
<h4>
包工具箱</h4>
<p>將資料庫操作和應用層邏輯分離的好處應該不用多說,使用統一的介面有其它好處,目前覺得最實用的是可以寫 try catch 自動記下所有出錯的 SQL,再依參數決定要吃掉 exception 或丟回應用層。由於出錯的 SQL 都有被 log,程式出錯時可以馬上找到有問題的 SQL,縮短除錯時間。</p>
<h4>
單元測試</h4>
<div>
我原本是用 Django 內建的方式重建測試資料庫,但是最近開始用 multiple database 後,遇到一些問題。由於我在 South 裡做了一些不合 Django 規定的操作,不想花時間理解 Django model 和 test 詳細的運作方式,最後決定自己寫簡單的模組來建置測試環境,速度也會比較快。還在小規模的試用中,看看之後能不能投多點時間打穩這塊,再來寫心得。</div>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/_fFQHff3pdQ" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-24515281978413678022010-11-21T22:07:00.001+08:002011-09-18T10:38:43.318+08:00Problem Solving 的技巧 (1):系統設計是一連串的取捨<p>費曼和杜書伍可說是影響我思考方式最深的人,即使重覆閱讀、重新思考他們話,仍能獲得不少新體悟。今天偶然發現《真正的問題是什麼?你想通了嗎?》的推薦序是杜書伍寫的,看了以後收獲良多,看了這麼多書,這還是頭一回看了推薦序而覺得有用。</p><p><span></span></p><p>文中提到:</p><blockquote><p>釐清問題的真實性後,面對真正的問題時,須有一個認知:<b>甚少問題能以單一方案解決,而須由不同面向,分頭淡化問題。...這些不同面向的解法,單獨用都只能解決局部問題,但配套提出後,卻能大幅降低問題的嚴重性,到一可接受的範圍內。 </b></p></blockquote><blockquote><p>企業經營無時不刻面臨問題的發生與解決,在問題決的過程中會發現,現實生活中並沒有可「百分之百」被解決的問題。誠如書中所言:「每一個解決方案都是下一個問題的根源」,一個有利於某面向的方案,代價往往是犠牲另一面向的利益。因此,如何透過溝通、妥協的過程,尋求最適的解法而非完美的解法,將問題的衝擊降到多數人可接受的範圍內,即為好的解決方式,否則反而可能適得其反,滋生新的問題。 </p></blockquote><p>看了這段話,更明白杜書伍花了多少歲月與心力實踐他在<a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010449478">《打造將才基因》</a>裡說的方法,完全是實實在在的真工夫。用精簡的文字描述出極為深刻的道理,實在是太厲害啦!</p><p>舉例來說,使用資料庫時,需同時擔心寫入速度和確保不會損失資料。若每次寫入時都寫入硬碟,可以確保資料正確,但卻會很慢;若先寫入記憶體,集合成一批再一起寫入硬碟,可以大幅提昇速度,但若資料庫伺服器當掉,卻會喪失大批資料。</p><p>一個配套解法是每次都寫入硬碟確保資料正確,接著使用 RAID 提昇寫入速度。但是用上 RAID 仍是操作硬碟,提昇有限,於是再加上帶有 write cache 的 RAID,這樣就能大幅提昇速度。但這又造成新的問題:雖然資料庫伺服器掛了不會影響資料,但若機房停電,仍會喪失 RAID cache 內的資料。於是在 RAID 裡加上電池,就能克服這問題 (附帶一提,我很喜歡這句話:<b>「系統設計是一連串的取捨」</b>)。</p><p>整個串起來就是「資料庫伺服器每次都寫入硬碟,並使用帶有 write cache + 電池的 RAID 機器」,這樣能同時兼顧寫入速度並確保不會損失資料,不過就變成大幅提昇開銷了。若將支出也列入考量點的話,配套解法可能又不同,也許是降低寫入速度的要求,也許是容許損失多少時間內的資料。</p><p>以這麼技術的問題做為例子,其實有損杜書伍提的解題思維、以及《真正的問題是什麼?你想通了嗎?》的價值。<b>真正在解決問題時,需要思考多個面向,而不能只局限在定死的框架裡以技術本位思考。</b>以資工的術語類比的話,用 BFS 的方式思考會比 DFS 容易找出好方案。比方說可以考慮修改規格或換方式滿足需求,或是重新定義問題,找出真正的需求和剛好到位的解法。問題往往來自於期望和感受之間出現落差,有時換個角度看問題,就能剛好繞開原本的技術難題,並且沒有犠牲任何事情。</p><p>以我最近在做的事為例,我在找網站壓力測試的工具,雖然能找到一些好用的工具,但它們卻沒提供良好的登入機制,讓我無法開始測試。若硬往技術方向思考,解決問題的選項不外乎再找下一個工具,或是改工具的程式碼,加入一個前置登入的機制。但其實退一步思考,我真正的需求是產生大量連線,以「如同使用者登入」的方式進行壓力測試,我並不需要用正規的方式登入自家網站,大可另寫一個網址,開後門避開繁瑣的登入流程,直接登入測試帳號。於是,我只需要評估壓力測試工具,不用在意它是否有完備的登入機制。</p><p><b>但思考面向變廣後,比起定好其它因素、單純考量技術來說,問題也變得更複雜。要有效地在不同面向間穿梭,需要更多的經驗和練習,就像以往專精學一件事一樣,只是變得更困難。</b>杜書伍在<a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010449478">《打造將才基因》</a>裡有不少這方面的說明,強烈推薦閱讀。概念是先專精一個技能,接著專精第二個技能,並找出和先前學習經驗的異同點,融會兩者成為自己的思考系統。接著專精第三個、第四個技能,找異同點,漸漸拼出多元且深入的思考方式。</p><p>最近這一年有點開竅,開始會朝多元化思考,先記錄目前的體悟。待有更多經驗後,再來寫心得吧。</p><img src="http://feeds.feedburner.com/~r/fcamel/~4/rndR8hfzf18" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-77098261834321943252010-11-04T22:57:00.001+08:002011-08-09T08:36:26.480+08:00讓 if、else 帶有更明確的語意<p>最近在維護程式時對於 if、else 有更深的體會,一但邏輯分支變多,很難釐清各種控制流程,一些簡單的習慣可以大幅簡化除錯和改程式的負擔。<br />
<span></span></p><h4>變數的初始值</h4><p>一個常見的情境是有個變數會依條件而有不同的值,典型的寫法如下:</p><div><table><tr><td><pre>1
2
3
4
5
</pre></td><td><pre style="font-family:monospace"><span style="color:#808080;font-style:italic"># 假設後面有一長串算式會乘上 weight,這裡先決定它的值</span>
<span style="color:#ff7700;font-weight:bold">if</span> double:
weight = <span style="color:#ff4500">2</span>
<span style="color:#ff7700;font-weight:bold">else</span>:
weight = <span style="color:#ff4500">1</span></pre></td></tr>
</table></div><p>( 備註,在 Python 裡 if / else 裡設的變數和它的外層是同一個 scope。 )</p><p>或是善用程式語言提供的三元運算子設值 (即 ? : ),在 Python 裡則是這麼寫:</p><div><table><tr><td><pre>1
</pre></td><td><pre style="font-family:monospace">weight = <span style="color:#ff4500">2</span> <span style="color:#ff7700;font-weight:bold">if</span> double <span style="color:#ff7700;font-weight:bold">else</span> <span style="color:#ff4500">1</span></pre></td></tr>
</table></div><p>若有多種情況,在其它語言裡可能會用 switch,我個人不喜歡 switch,覺得用起來不直覺,Python 裡也沒有 switch,但可以用 dict 代替:</p><div><table><tr><td><pre>1
2
3
4
</pre></td><td><pre style="font-family:monospace">weight = <span style="color:black">{</span>
<span style="color:#483d8b">'double'</span>: <span style="color:#ff4500">2</span>,
<span style="color:#483d8b">'triple'</span>: <span style="color:#ff4500">3</span>,
<span style="color:black">}</span>.<span style="color:black">get</span><span style="color:black">(</span>condition, <span style="color:#ff4500">1</span><span style="color:black">)</span></pre></td></tr>
</table></div><p>操作複雜時可在 dict 的 value 裡改用輔助的小函式,明確的用簡短的程式表明「這區塊在決定 weight 的值」。</p><p>別小看這一點小改變,當程式碼很多時,看到 “value = a if condition else b” 可以立即明白這裡的判斷式是用來設值,可以省下為 if、else 這區塊煩心的時間,也可以減少消耗精神和腦內暫存記憶。</p><h4>提前處理簡單的分支</h4><p>以用遞迴的方式實作費氏數列為例:</p><div><table><tr><td><pre>1
2
3
4
5
6
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">def</span> fib<span style="color:black">(</span>n<span style="color:black">)</span>:
<span style="color:#ff7700;font-weight:bold">if</span> n <span style="color:#66cc66"><</span> <span style="color:#ff4500">0</span>: <span style="color:#808080;font-style:italic"># Error input.</span>
<span style="color:#ff7700;font-weight:bold">raise</span> <span style="color:#008000">ValueError</span><span style="color:black">(</span><span style="color:#483d8b">'n must be positive.'</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">if</span> n == <span style="color:#ff4500">0</span> <span style="color:#ff7700;font-weight:bold">or</span> n == <span style="color:#ff4500">1</span>:
<span style="color:#ff7700;font-weight:bold">return</span> <span style="color:#ff4500">1</span>
<span style="color:#ff7700;font-weight:bold">return</span> fib<span style="color:black">(</span>n - <span style="color:#ff4500">1</span><span style="color:black">)</span> + fib<span style="color:black">(</span>n - <span style="color:#ff4500">2</span><span style="color:black">)</span></pre></td></tr>
</table></div><p>上面的寫法先處理例外,接著就能放心處理正常的情況,再來處理特例 (初始值),最後就能專心和主邏輯奮戰,而覺得主邏輯變得單純許多,很好處理。</p><p>較大的程式,就是先寫幾個簡單輔助小函式 (例如 is_invalid()),先呼叫小函式避開特殊情況,一樣可以化繁為簡。</p><h4>避免巢狀區塊和 continue、break</h4><p>常見到在多層迴圈裡呼叫 if、else,並和 continue、break 混用,我個人覺得這種寫法很亂,而傾向用小函式 + return 避開使用 continue 或 break,比方像下面的程式要從一個兩層 list 裡找出每個 list 第一個負數,並算出負數的總和:</p><div><table><tr><td><pre>1
2
3
4
5
6
7
</pre></td><td><pre style="font-family:monospace"><span style="color:#008000">sum</span> = <span style="color:#ff4500">0</span>
<span style="color:#ff7700;font-weight:bold">for</span> numbers <span style="color:#ff7700;font-weight:bold">in</span> a_list_of_numbers:
<span style="color:#ff7700;font-weight:bold">for</span> n <span style="color:#ff7700;font-weight:bold">in</span> numbers:
<span style="color:#ff7700;font-weight:bold">if</span> n <span style="color:#66cc66"><</span> <span style="color:#ff4500">0</span>:
<span style="color:#ff7700;font-weight:bold">break</span>
<span style="color:#ff7700;font-weight:bold">if</span> n <span style="color:#66cc66"><</span> <span style="color:#ff4500">0</span>:
<span style="color:#008000">sum</span> += n</pre></td></tr>
</table></div><p>可以改用小函式配合 return 避免使用 break 並「隱藏」分支:</p><div><table><tr><td><pre>1
2
3
4
5
6
7
8
9
10
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">def</span> find_first_negative<span style="color:black">(</span>numbers<span style="color:black">)</span>:
<span style="color:#483d8b">''</span><span style="color:#483d8b">'Return 0 if there is no negative number.'</span><span style="color:#483d8b">''</span>
<span style="color:#ff7700;font-weight:bold">for</span> n <span style="color:#ff7700;font-weight:bold">in</span> numbers:
<span style="color:#ff7700;font-weight:bold">if</span> n <span style="color:#66cc66"><</span> <span style="color:#ff4500">0</span>:
<span style="color:#ff7700;font-weight:bold">return</span> n
<span style="color:#ff7700;font-weight:bold">return</span> <span style="color:#ff4500">0</span>
<span style="color:#008000">sum</span> = <span style="color:#ff4500">0</span>
<span style="color:#ff7700;font-weight:bold">for</span> numbers <span style="color:#ff7700;font-weight:bold">in</span> a_list_of_numbers:
<span style="color:#008000">sum</span> += find_first_negative<span style="color:black">(</span>numbers<span style="color:black">)</span></pre></td></tr>
</table></div><p>改寫後,兩個 if 都不見了,主邏輯很清楚地表現出「找出各 list 第一個負數並加總」。</p><p>同樣的,程式愈複雜時,這些寫法省下的思考時間愈可觀。</p><h4>明確的指明 else 的處理方式</h4><p>這和前面提的東西有一點相衝突,視情況而定。在任何有 if、elif 的情況,即使 else 的情況不需做任何處理,仍要明確的寫出 else 並加上註解。如下所示:</p><div><table><tr><td><pre>1
2
3
4
5
6
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">if</span> some_condition:
...
<span style="color:#ff7700;font-weight:bold">elif</span> another_condition:
....
<span style="color:#ff7700;font-weight:bold">else</span>: <span style="color:#808080;font-style:italic"># Do nothing.</span>
<span style="color:#ff7700;font-weight:bold">pass</span></pre></td></tr>
</table></div><p>這個作法的目的是,讓其他讀這份程式的人,明白原作者沒有漏考慮 else 的情況,不處理是符合預期的作法。函式愈長時,這樣寫的好處愈明顯。別小看這個小動作,程式碼一多,回頭讀程式碼時,這點小動作可以省下不少分心的機會。</p><h4>結語</h4><p>上面提的例子背後的目的都一樣,就是避免讀程式碼的人分心在分支裡,而能專注在主邏輯上。類似的例子還有「使用 iterator 少用 for + index」,平時留意一些小細節,不但能愈寫愈快 (省去煩心細節的時間),也能降低維護成本,讓其他人易於理解。舉手之勞做環保,大家一起來維護程式碼的品質吧!</p><img src="http://feeds.feedburner.com/~r/fcamel/~4/T_4-OzR1Z0g" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com1tag:blogger.com,1999:blog-451051488891335923.post-71431187671692135312010-09-11T18:32:00.000+08:002011-05-15T00:56:20.614+08:00每件事都有它的價值<p>過去我一直在尋找捷徑,希望能找到經驗以外的價值,不然只是比年資,感覺很無趣。老手比新手強,只是因為他做得久、比較熟悉公司內的程式。待新手變強後,也只是因為他經驗變多。似乎人與人之間沒什麼差異性。改善自己學習的方式是個好主意,但有智慧的人都會不斷改善自己學習的方式,以能夠更有效率地學習。我不斷地思考自己的定位,以及如何學得更有效率。期望在天份、經驗外找到其它的聖杯。</p>
<p><span></span></p>
<p>Steve Jobs 在對 <a href="http://www.youtube.com/watch?v=UF8uR6Z6KLc">Stanford 大學的畢業演講</a>裡提到:</p>
<blockquote><p>Again, you can’t connect the dots looking forward; you can only connect them looking backwards.</p></blockquote>
<p>抱著半信半疑的心態,我只能先相信這個看法,繼續邊做邊想。</p>
<p>後來在杜書伍的《打造將才基因》裡看到:</p>
<blockquote><p>我並末想出什麼令人拍案叫絕的答案來,但當時得到的結論卻一直到現在我都認為是正確的,...,經驗要累積得快,除了比別人更努力工作外,似乎找不到其他的方法。</p></blockquote>
<p>看第一次時仍是半信半疑。然而,最近重讀這本書,對照近來的體悟,忽然間想通了這點。</p>
<p>在經歷 TDD 的洗禮後,我覺得我好像找到一個不錯的答案,至少它是我在軟體開發領域裡找到最好的答案。似乎只要練 TDD,就能在同樣時間內爆增功力。相較於過去看的 Design Pattern、程式語言實作技巧等,TDD 似乎更宏觀實用。</p>
<p>就在實踐 TDD 一年後,偶然看到別人的討論,而有不同想法。練習 TDD 後確實能寫出較好維護的軟體,不過除錯的經驗也變少了。寫了快一年半的程式下來,我一直沒有用 debugger 的需求,只開過兩三次 python debugger,碰一下就放棄它,改成補寫 unit test 偵錯。這才發覺,每段時間都有它的意義,只是練得目標不同。我明白照目前的方式走下去,再過個三年五年,我用 TDD 的功力會愈來愈深,不過用 debugger 功力仍然會是零。這沒好壞之分,端看目標為何,以及自己的定位。</p>
<p>在其它學習的經驗裡,發覺看過的知識,還是要實際操作過數次才能內化到自己的思考體系裡。操作次數愈頻繁,體悟愈深。像許多系統設計或軟體開發流程的最佳實踐 (best practice),還是要走過不同的路,回頭才會明白最佳實踐背後的考量,才知道如何融會這些點到自己的情境裡。很多東西光用看的,或光用做的,無法體會背後的精神,遇到狀況時仍然使不出來。所以,減少嘗試多讀最佳實踐,效果有限;老是硬幹不讀別人的體悟,也是效果有限。</p>
<p>剛才在聊<a href="http://www.plurk.com/p/7i4d3s">看 Python 源始碼的事</a>,<a href="http://scottt.tw/">Scott </a>和 <a href="http://www.plurk.com/Thinker">Thinker </a>不約而同建議直接看原始碼,而不用看書 (即使 <a href="http://www.google.com/buzz/111353793049965752735/ASTDLu9A3EZ/fcamel-%E8%AA%AA-keitheis-%E9%82%A3%E7%9C%8B%E5%88%B0%E7%9A%84-http-tinyurl">Scott 認為書本的大綱看來不錯</a>)。但我覺得先看書有個概念再來看原始碼應該比較有「效率」。經 Thinker 說明,才發覺這是練得目標不同。直接硬啃 code 看來較費工,但在費工之中才會有更多練習讀碼的機會,會更熟練相關工具和技巧。而我的目的是增加系統設計經驗為主,讀原始碼為輔,自然會覺得先讀書較好。</p>
<p>每件事都有它的價值,每個技術都需要時間才能挖深。然而,要將走過的路合起來發揮最大功效,要將所學收斂在相關領域裡。這讓我不經花更多時間思考,往後自己的定位為何,我想發展什麼樣的知識網。總之,相信 Steve Jobs 和杜書伍的話,「Stay hungry. Stay foolish.」繼續邊做邊想吧。</p>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/TDDHel-1gts" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-5601082106204498722010-07-18T13:12:00.000+08:002011-05-15T00:56:21.601+08:00學習的自我要求<p>學生時代我一直在想一個問題,究竟是問題導向的學習方式較佳,還是技術導向較佳?舉例來說,我想寫個留言版,我發現要完成最基本的需求,我得學 HTML、PHP 和 MySQL,於是我針對我要做的功能學相關的語法,最後完成留言版。可能資料庫設計沒用到正規化,網站外觀沒用到 CSS,一些動態功能用 PHP 實現而非 JavaScript。相較於從頭學寫網站所需的各項技術,這個作法比較務實,我用較少的時間完成目標。極端地看,用問題導向的解法時,我可能找到相關的程式碼,用它們拼貼出結果,但不確定各塊在做什麼。技術導向的解法,可能要先花一段時間弄清楚寫網站需要的技術,再各別學個基本能力,接著開始實作。當然,世界上不是只有 0 和 1,我們常混著用問題導向和技術導向的方式解問題。這裡我說用問題導向的意思,是指偏向用問題導向。<br>
<span></span></p>
<p>不只做產品,做研究時也是類似的情況。問題導向就是從問題開始往下挖,看前人做那些方法,將自己不熟的部份補起來,有了必要的知識後,就能回頭解決問題。而技術導向的做法,我可能會加強基礎學科知識,視類別而定可能是機率和線代,或是計算機組織和編譯器,接著才開始思考如何解問題(或重新定義問題)。</p>
<p>以前我沒什麼時間觀念,通常是技術導向的作法,覺得有趣就看我能從多底層開始往上學。比方說使用 Java Collections Framework 時,我會弄清楚我要用的資料結構怎麼運作的的,時間複雜度是多少。實際上大部份的使用情境不需要這些知識,稍微讀一下文件的注意事項即可。要用 machine learning 的工具,就找適合的 machine learning 課程講義來讀,弄清楚要用到的 model 怎麼運作。直到現在,我無法確認這些對於使用工具究竟幫了多少忙,當初花的時間對應到目前的幫助,真的划算嗎?</p>
<p>最近我試著用問題導向的方式解問題,初期進展確實比較快,不過後來遇到一些狀況,才發覺技術導向的好處。有些情況,我們根本不明白問題出在那裡,有太多可能。舉例來說,前幾天我和同事發覺不同使用者登入同一頁面,操作速度卻差了一大截。有許多可能原因:網路連線 、網頁框架、資料庫等。做了一些初步測試,我懷疑是 MySQL 根據歷史記錄做錯 query optimization。於是用 <a href="http://dev.mysql.com/doc/refman/5.0/en/using-explain.html">EXPLAIN </a> 看相同的 SQL 配合不同使用者 ID,結果發現 MySQL 執行 query 的方式有細微差異,造成取出某些使用者的資料時,用較慢方式執行 SQL。於是讓 MySQL 重分析表內的資料後,問題就解決了。若不是之前稍微看過 MySQL 執行 query 的相關知識,不會這麼快就直指問題核心。也許就會用別的方式繞開這問題,一輩子都不知道怎麼解它。待發生類似問題時,又用別的方式繞開它,長遠來看浪費開發時間又增加維護成本。</p>
<p>另一個反面的例子是,我一直沒用 lock 的習慣,教科書告訴我們 deadlock 很可怕,所以我會想辦法避開用 lock。結果最近有個小專案因為沒用 lock,真的發生 race condition 造成有一點點資料不正確。實作前我明白會有這樣的狀況,但這個問題對我們的目的沒什麼影響,衡量開發時間後我決定寫下註解強調會有 race condition,而選擇不處理它。對照最近的體悟,我明白這樣下去我不可能學會用 lock,這不是個好現象,所以又找時間回頭看 MySQL 怎麼 lock table,結果比想像中來得簡單,之前多慮了。</p>
<p>有很多類似這種的逃避例子,像多人一起寫程式容易有問題,於是大家傾向將功能切乾淨,每人寫沒有交集的功能,最後再來整合。但是,對照近年來的軟體開發的趨勢,愈早整合愈容易解決問題。一個人開發容易有盲點,互相協助可以降低初期錯誤,以利後期整合。問題是,要能順利地多人共同開發,得做對不少事才行。像是版本管理系統、coding style、天天合併程式等。每一項都需要時間練習。若一個人開發時有好好練習,和別人合作時會減少許多問題,比較容易推動密集的團隊合作。</p>
<p>在面試別人時,我發覺一個問題:有些人學到的技能剛好只能應付他負責的專案。問題在於,若平時我們都處理簡單的專案,要怎麼轉去負責困難的專案?兩者之間有個斷層。像這類的例子不勝杖舉。比方說要從資料庫表 A 取出部份資料塞入表 B,最「簡單」的作法是寫個程式用 SQL 取出資料,用程式做些處理再用 SQL 一筆筆寫入表 B。另一個作法是直接用一個較複雜的 SQL 直接搞定。當資料量大時,後者執行速度會快上不少。並且,學會後者的寫法後,之後只要花一點時間就能處理類似的情況,不用再寫一個小程式。其它像寫程式的習慣、用程式工具的習慣,都是一樣的。多數情況我們可以用最「不費力」的作法滿足需求,但長遠來看卻是毫無長進。實際上有更有效率的作法,這裡的效率包含開發時間和軟體品質。</p>
<p>對照大學的程式來看,這一年來我以為自己程式已寫得頗有品質了,雖然知道一些小問題,但覺得並不迫切,也不知怎麼查相關的解法,就放著它們。最近翻了一下 <a href="http://java.sun.com/docs/books/effective/">Effective Java 第二版</a>和 <a href="http://www.growing-object-oriented-software.com/">Growing Object-Oriented Software Guided by Tests</a>,才發覺還有太多的東西要學,自己和資深的軟體工程師差了一大截,照我目前的學習方式,和他們的差距只會拉大不會縮短。若只專注完成眼前的工作,我永遠無法補足和更難工作之間的差距,這才驚覺問題導向的盲點。</p>
<p>走過天平的兩端後,我現在的體悟是,得雙向夾擊來解決問題。一方面用問題導向解問題以符合時程,確保時間有花在刀口上。另一方面再抽時間用技術導向的方式強化自己的實力。如此一來,在完成當下的專案的同時,也有一點一滴地補足技術斷層,取得挑戰更難專案的機會。題外話,英文也是很重要的「技術」,這一年來我半強迫地讓自己盡量搜英文文件,思考關鍵字比以前敏銳不少,閱讀速度也變快,獲得答案的速度比以前快、品質也較佳。</p>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/eRhU02I0sxQ" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-30617731208891842342010-05-29T16:54:00.003+08:002011-06-22T22:24:08.661+08:00養成寫程式的好習慣我很喜歡 Kent Beck 說的這段話<br />
<blockquote>“I’m not a great programmer, I’m a pretty good programmer with great habits.”</blockquote>從學生時代開始,我就習慣照著書上的建議寫程式,一直沒覺得什麼特別的。後來和一些人合作,或是看到別人抱怨那裡又出錯了,才驚覺那些好習慣有這麼大的影響力。試著在一些場合向別人說明如何改進寫程式的方式,才發覺很難改變寫程式的習慣 — 大概就和我一直無法早睡早起一樣難...。<br />
<br />
大前研一說許多人在解決問題時誤把結果當原因,沒有深入追究問題的根本,只是解決問題根源造成的表面問題。寫程式自然會有 bug,但是有許多 bug 並不是 bug,而是壞習慣造成的。像是在 PHP 或 JavaScript 裡不用 === 和 !==,而被 type casting 誤導造成 bug。或是在 if / while 裡用 assignment 又不小心漏了括號,寫出 if (a=foo() > 0) 這類 bug。<br />
好習慣得隨時間慢慢累積,做得愈多愈久,功力自然會變深。就如同龜仙人要悟空和克林送牛奶那般練基礎功,帶著好習慣寫程式,經年累月下來,學到的東西會更多。以下針對一些特定的習慣提我自己的感受。<br />
<h4>Coding style</h4>網路上有不少 coding style 建議,有些如 Google coding style 甚至會解釋為何要這麼寫,這樣寫的好處和壞處為何。可以學到不少寫程式的小技巧。<br />
最近遇到的實例是遵守 Python coding style 所說,在 module 開頭寫 import,並照順序 import 內建、第三方模組和自己的模組。在程式寫到近兩萬行後要做些修改時,我發覺這個簡單的習慣,讓我很容易明白那些模組有關聯,很快就能找出要修改的程式。反過來說,閱讀一些 Django 程式碼時,發覺常在函式裡 import module,不易掌握模組之間的關係。<br />
還有控制函式的行數在螢幕的高度內、區域變數的使用範圍(要用到時再「宣告」)、避免用全域變數。遵守這些習慣使我容易掌握變數的影響範圍,除錯時可以省下不少心思思考變數是否被別的地方改到。<br />
最近初學 JavaScript,就在 coding style 的建議裡找到減少 global object 和管理變數 scope 的技巧:使用一個 global object 存放所有變數和函式,藉此在 JavaScript 中做出模組的效果。<br />
<h4>Version control (以 Mercurial 的指令為例)</h4>即使一個人寫程式,version control 仍有很大的用處。保持每個 commit 精簡,每個 commit 只完成一個小功能,就能輕易追踪過去的改變。<br />
以下是幾個我常用 VCS 協助的情況:<br />
<ul><li>寫程式較不怕被中斷,只要 hg diff 就知道剛才改了什麼。commit 前也能清楚明白這次做了那些修改,去掉忘了除掉的 debug code。</li>
<li>可以放心地修改,改到昏頭就 hg up -C 清掉剛才不知所云的修改,不用花費力氣將程式弄回正常的版本。</li>
<li>寫到一半發覺要先完成另一個功能,hg shelve 暫存目前的修改,接著將另一個功能做完並 commit,再 hg unshelve 回頭做原本的事,可以輕鬆地切換目標,隨時專注在目前的目標上。</li>
<li>若發覺某個功能忽然不能運作,hg up 切回舊的版本,做個 binary search (或用 hg bisect) 立即找到改出問題的 commit。由於每個 commit 都很精簡,看一下就會找到改爛的原因。</li>
</ul>我最近用 jQuery 寫的程式,過了一陣子後發覺某個功能不能運作。用 hg up 和 binary search 的方式,很快地找到在一百多版前改爛的,而且改爛的原因很奇妙,我將目標 tag 的 id 設為 “submit” 後就爛了,但若換個名字或不設 id 就沒事。若沒有 version control,我想我在原本的程式裡找半天也不會找到,根本不會懷疑問題出在這裡。最後大概會重寫該段,然後莫明奇妙地避開這個問題。<br />
和人合作時 version control 就更有用了,方便和其他人共用程式、做 code review、自動跑測試確保各版運作正常,好處不勝杖舉。相較於每個人各自寫程式,多個人同寫一份程式不但方便討論,容易互相支援,開發時士氣也會較好。每次看到別人 push code,就會覺得待做事項漸漸變少,而寫得更有勁。<br />
<h4>Refactoring</h4>在寫新功能或修 bug 前,若發現有重覆程式碼或一些有潛在風險的程式,先重構程式再回頭做原本該做的事。重構前記得要確保重構後行為不變。若情況太糟很難補 unit test 或是沒時間補太細,準備好幾組常用的輸入資料,記好它們對應的輸出結果,寫個自動測試的 recorded test 會比較安全,也可加快後續的重構。配合 VCS 做起來更容易,改改發現無法通過 recorded test,就 hg up -C 重頭改一次。然後別太貪心,一次改一點比較不容易犯錯。視情況寫 recorded test 的方式有所不同。通常我會用 shell script + diff 這類指令很快的拼一個可以用的小工具,重構完就丟了,節省準備 recorded test 的時間還有免除日後維護它的負擔。但若打算長久維護的話,自然是照規矩一步步做會比較穩當。<br />
<br />
<h4>Unit test / Acceptance test / TDD</h4>這部份在<a href="http://fcamel.twbbs.org/archives/2009/06/13/849/">先前的文章</a>已提了不少,隨著時間演進,<a href="http://fcamel.twbbs.org/archives/2009/10/29/928/">實例</a>愈來愈多。最近將 Django 1.1 昇到 1.2,跑 unit test OK, 但跑 Selenium test 時看到 <a href="http://docs.djangoproject.com/en/1.2/ref/contrib/csrf/">CSRF </a>的錯誤訊息。稍微修一下,測試程式全過。讓我立即確信所有用到的 plugin 都順利地在 1.2 版下運作。<br />
開始用 TDD 可說是我寫程式生涯中的重大里程碑,踏入完全不同的格局。讓我明白如何寫出易於長期發展的程式,不用像在玩踩地雷般辛苦。<br />
<h4>Pair programming</h4>這不算是個人習慣,順便記在這裡。<br />
這部份我沒太多經驗,有時運作的不錯,有時不太順。執行 pair programming 前要先確保兩人的背景知識差不多,才不會有一人跟不上進度,讓另一人空轉。運作順利時,可以很快地完成較複雜的設計,並確保至少有兩個人可以繼續維護這份程式。而且程式也會較易懂:兩個人覺得好懂的程式,遠比一個人覺得好懂的程式易懂多了。<br />
Pair programming 比寫下規範更容易讓大家有一致的開發習慣,像是 coding style 或是 commit 的規範。藉由一人帶一人的方式連結開發習慣。也方便分享實作技巧,像操作工具的技巧、使用函式庫的經驗或是寫程式的技巧。<br />
<h4>其它</h4>除養成好習慣外,偶而抽點時間學習工具的操作,像是 Linux 架站裝軟體之類的,開發軟體時很難避免這些事。像我習慣用 Linux terminal 開發程式,多熟悉 screen、bash、Vim 的設定和操作,開發速度可以快上不少。<br />
最近的例子是使用 Firefox 的 plugin Firebug。以前改 CSS 都笨笨地存檔、重讀網頁,用 Firebug 讓我用十倍以上的速度完工,令人不勝噓唏。<br />
<h4>結語</h4>剩下的就是熟悉函式庫、框架,還有學會資訊工程一些基本知識,了解程式背後運作的原理。一但每個人都能保持寫程式的好習慣,團隊合作將會簡單許多,大家方便共用程式,方便互相支援 (寫相依的元件或除錯),既能加快開發速度,也會比較有趣。<br />
<img height="1" src="http://feeds.feedburner.com/~r/fcamel/~4/GaQZD8slMpM" width="1" />fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-54334227854836748442010-04-24T14:33:00.000+08:002011-05-15T00:56:23.444+08:00百人百觀 (3)<p>原本我以為大部份問題有「標準答案」,而不斷地尋找每個問題的「標準解」。兩個人見解不同,必然有人有誤,也可能都不對。在<a href="http://fcamel.twbbs.org/archives/2006/11/27/182/">《百人百觀》</a>裡,我才明白不是這麼一回事:</p>
<blockquote><p>
最後我發覺沒什麼最正確、最有道理的事,百人百觀,於是我不會硬把自己的想法套到別人身上,當別人和我抱怨誰的想法不合理,大家都如何,他偏偏不合群,即使我贊同大部份人的看法,卻不會像以前一樣,認為那個特立獨行的人有問題,想說服他。</p></blockquote>
<p><span></span></p>
<p>在<a href="http://fcamel.twbbs.org/archives/2007/02/21/299/">《百人百觀 (2)》</a>裡,我才明白不該強加自己的觀念在別人身上,即使我認為自己沒錯,也要尊重別人的見解:</p>
<blockquote><p>
於是我不在意我是否能影響聽者的觀點,有的話,當然很高興;沒有的話,對方可能需要不同的契機來改變,那個契機不是我。也可能這個想法適用於我,不適用於對方。另一方面,我仍然熱於與人討論,百人百觀不意味交流沒有意義,只是不用過於執著自己的想法。</p></blockquote>
<p>但是直到最近,我才明白自己的想法常常有誤,要能接受不同的看法,從中學習。這個轉變花了我不少時間。先是看了費曼的言論而開始懷疑一切、懷疑自己。我很喜歡他在《這個不科學的年代! 》第一篇裡說的話:</p>
<blockquote><p>
有些人說:「你怎麼能夠活著而無知?」我不知道他們是什麼意思。我從來都活著,也從來都很無知。那容易得很。我想知道的是你如何能什麼都知道。</p></blockquote>
<p>後來又看了 TED Talk <a href="http://www.ted.com/talks/derek_sivers_weird_or_just_different.html">《Weird, or just different?》</a>,裡面有段話很棒:</p>
<blockquote><p>
Whatever brilliant ideas you have or hear, the opposite may also be true.</p></blockquote>
<p>我原本對此半信半疑,經過幾次實例驗證,發覺我堅信「絕對沒錯」的作法,仍有一些情境不適用。若能從自己習慣的作法中找出反面的價值,或許會大有幫助。一但認定「肯定如此」後,就失去改進的機會了。</p>
<p>至於前兩篇文章對於溝通的焦慮,現在的心得是,要能開放心胸,站在對方的角度思考。嘗試幾次後,會發覺以前沒想到的事。再來就是去除情緒,就事論事。說來容易做來難,還在持續練習中。附帶一提,站在對方的角度思考並不容易,不只是情緒上的問題,有時沒類似的經驗,無法明白對方看重的點。</p>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/qyuxclxL_RM" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-24752847745488300422010-04-11T21:11:00.000+08:002014-03-02T19:28:28.259+08:00寫出容易測試的程式<p>昨天和何米特、卡曼德 (不要懷疑,他們都是道地的台灣人)聊到測試,想到一個不錯的例子。這回就用實例來說明「容易測試」和「不容易測試」到底是怎麼一回事。完整的程式可以從<a href="http://code.google.com/p/fc-toolkit/source/browse/#hg/test_samples/wwc">這裡</a>取得。<br>
<span></span></p>
<h4>題目說明</h4>
<p>從參數列讀入一個網址,計算網頁內的單字數量並輸出到螢幕。單字之間用空白區隔。當網址無效時,輸出 -1。</p>
<p>為了方便說明起見,我選了個簡單但完整的例子。若要將問題變得更有說服力,不妨想像要連線到網頁伺服器,需要來回幾次的通訊和分析網頁內容以達成目的。比方說寫個程式下載漫畫或美女圖之類的。總之,程式愈複雜,下面提到的問題愈嚴重。</p>
<h4>直覺的寫法</h4>
<p>相信大家看到這題目都會覺得簡單嘛,這樣的程式有什麼難的,寫一寫、跑一跑、改一改,來回幾次就搞定了。寫出來的程式大概長這個樣子:</p>
<div><table><tr><td><pre>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">import</span> <span style="color:#dc143c">sys</span>
<span style="color:#ff7700;font-weight:bold">import</span> <span style="color:#dc143c">optparse</span>
<span style="color:#ff7700;font-weight:bold">import</span> <span style="color:#dc143c">urllib2</span>
<span style="color:#ff7700;font-weight:bold">def</span> main<span style="color:black">(</span>url<span style="color:black">)</span>:
<span style="color:#483d8b">''</span><span style="color:#483d8b">'<span style="color:#000099;font-weight:bold">\</span>
%prog [options] <url>
Print the number of words in <url>.
Print -1 if <url> is invalid.
'</span><span style="color:#483d8b">''</span>
<span style="color:#ff7700;font-weight:bold">try</span>:
content = <span style="color:#dc143c">urllib2</span>.<span style="color:black">urlopen</span><span style="color:black">(</span>url<span style="color:black">)</span>.<span style="color:black">read</span><span style="color:black">(</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">print</span> <span style="color:#008000">sum</span><span style="color:black">(</span><span style="color:#008000">len</span><span style="color:black">(</span>line.<span style="color:black">split</span><span style="color:black">(</span><span style="color:black">)</span><span style="color:black">)</span> <span style="color:#ff7700;font-weight:bold">for</span> line <span style="color:#ff7700;font-weight:bold">in</span> content.<span style="color:black">split</span><span style="color:black">(</span><span style="color:#483d8b">'<span style="color:#000099;font-weight:bold">\n</span>'</span><span style="color:black">)</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">except</span> <span style="color:#dc143c">urllib2</span>.<span style="color:black">URLError</span>, e:
<span style="color:#ff7700;font-weight:bold">print</span> -<span style="color:#ff4500">1</span>
<span style="color:#ff7700;font-weight:bold">return</span> <span style="color:#ff4500">0</span>
<span style="color:#ff7700;font-weight:bold">if</span> __name__ == <span style="color:#483d8b">'__main__'</span>:
<span style="color:#dc143c">parser</span> = <span style="color:#dc143c">optparse</span>.<span style="color:black">OptionParser</span><span style="color:black">(</span>usage=main.__doc__<span style="color:black">)</span>
options, args = <span style="color:#dc143c">parser</span>.<span style="color:black">parse_args</span><span style="color:black">(</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">if</span> <span style="color:#008000">len</span><span style="color:black">(</span>args<span style="color:black">)</span> <span style="color:#66cc66">!</span>= <span style="color:#ff4500">1</span>:
<span style="color:#dc143c">parser</span>.<span style="color:black">print_help</span><span style="color:black">(</span><span style="color:black">)</span>
<span style="color:#dc143c">sys</span>.<span style="color:black">exit</span><span style="color:black">(</span><span style="color:#ff4500">1</span><span style="color:black">)</span>
<span style="color:#dc143c">sys</span>.<span style="color:black">exit</span><span style="color:black">(</span>main<span style="color:black">(</span><span style="color:#66cc66">*</span>args<span style="color:black">)</span><span style="color:black">)</span></pre></td></tr></table></div>
<p>看起來沒啥問題,但是我們要怎麼確定這份程式是對的?嗯,大概找幾個有效的網址、無效的網址,試一試,看看跑出來的數字對不對。或著專業一點,自己弄幾個簡單的網頁方便計算答案,連入自己的網頁,對看看有沒有算對字數。</p>
<p>可以想見,這個測試過程冗長乏味。之後若做些修改,像是規格改成「輸出的數字要四捨五入到百位」、「輸出全部的字和它的次數」,沒人想從頭重測一次所有情況。</p>
<p>這裡的問題是什麼?問題出在沒有做到自動測試。那就寫個 shell script 讀入一串網址依序執行並存下結果,人工確認結果無誤後,再將答案存起來。之後就執行 shell script 讀網址比對輸出。比方說像這樣:</p>
<div><table><tr><td><pre>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td><pre style="font-family:monospace"><span style="color:#666666;font-style:italic">#!/bin/bash</span>
<span style="color:#666666;font-style:italic"># usage: ./check.sh <prog></span>
<span style="color:#000000;font-weight:bold">function</span> check<span style="color:#7a0874;font-weight:bold">(</span><span style="color:#7a0874;font-weight:bold">)</span> <span style="color:#7a0874;font-weight:bold">{</span>
python $<span style="color:#000000">1</span> $<span style="color:#000000">2</span> <span style="color:#000000;font-weight:bold">></span> t.txt
<span style="color:#c20cb9;font-weight:bold">read</span> n <span style="color:#000000;font-weight:bold"><</span> t.txt
<span style="color:#c20cb9;font-weight:bold">rm</span> <span style="color:#660033">-f</span> t.txt
<span style="color:#000000;font-weight:bold">if</span> <span style="color:#7a0874;font-weight:bold">[</span> <span style="color:#007800">$n</span> <span style="color:#660033">-eq</span> $<span style="color:#000000">3</span> <span style="color:#7a0874;font-weight:bold">]</span>; <span style="color:#000000;font-weight:bold">then</span>
<span style="color:#7a0874;font-weight:bold">echo</span> <span style="color:#ff0000">"Pass."</span>
<span style="color:#000000;font-weight:bold">else</span>
<span style="color:#7a0874;font-weight:bold">echo</span> <span style="color:#ff0000">"Fail."</span>
<span style="color:#7a0874;font-weight:bold">exit</span> <span style="color:#000000">1</span>
<span style="color:#000000;font-weight:bold">fi</span>
<span style="color:#7a0874;font-weight:bold">}</span>
check <span style="color:#000000">MY_PROG</span> http:<span style="color:#000000;font-weight:bold">//</span>www.googel.com<span style="color:#000000;font-weight:bold">/</span> <span style="color:#000000">257</span>
check <span style="color:#000000">MY_PROG</span> http:<span style="color:#000000;font-weight:bold">//</span>www.googel<span style="color:#000000;font-weight:bold">/</span> <span style="color:#660033">-1</span></pre></td></tr></table></div>
<p>雖然少了冗長的手動測試,上述的測試流程仍有幾個問題: </p>
<ul>
<li>無法保證測試結果一致。有可能連到的網站改變內容,或是剛好連不上,使得每次測試可能得到不同的結果 (即使 Google 不會斷線,它總會改變網頁內容吧)。那麼,測試失敗時,我們怎麼知道是測試過程有問題,還是被測試的程式有問題?</li>
<li>測試費時。受到網路連線的限制,測試相當費時。使得我們不會改一小段程式就執行所有測試。若我們能寫一行程式就跑一次測試,馬上能明白是那裡改出問題。</li>
<li>測試失敗無法直指錯誤的源頭。我們只知道連 A 網址沒得到預期結果,接著得進程式一步步看,輸出內部資訊才能慢慢找到寫錯的地方。</li>
<li>不易準備測試資料。若想測空網頁、有一個字的網頁和有一堆字的網頁,就得準備三個檔案。</li>
</ul>
<p>目前看來,上述的問題似乎不大。但若有上萬行程式和上百個測試案例,一個案例要跑一秒,加起來就變一百秒。其中又有一兩個偶而會測試失敗,造成每次跑完測試無法相信測試結果。即使測試結果沒有疑慮,當測試失敗時,要怎麼從上萬行程式中找出錯在那裡?</p>
<h4>
第二版:將計算字元數的部份獨立成函式</h4>
<p>在第一版的程式裡,為了測試是否有算對字數,得準備多份網頁和網頁伺服器再透過 HTTP 讀入內文做測試。光看這麼長的描述就會發覺那裡有些不對勁。若將計算字數的部份獨立成一個函式,就能單獨測「無內文」、「只有一個字」、「有很多字」等情況:</p>
<div><table><tr><td><pre>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">def</span> count_lines<span style="color:black">(</span>lines<span style="color:black">)</span>:
<span style="color:#ff7700;font-weight:bold">return</span> <span style="color:#008000">sum</span><span style="color:black">(</span><span style="color:#008000">len</span><span style="color:black">(</span>line.<span style="color:black">split</span><span style="color:black">(</span><span style="color:black">)</span><span style="color:black">)</span> <span style="color:#ff7700;font-weight:bold">for</span> line <span style="color:#ff7700;font-weight:bold">in</span> lines<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> main<span style="color:black">(</span>url<span style="color:black">)</span>:
<span style="color:#483d8b">''</span><span style="color:#483d8b">'<span style="color:#000099;font-weight:bold">\</span>
%prog [options] <url>
Print the number of words in <url>.
Print -1 if <url> is invalid.
'</span><span style="color:#483d8b">''</span>
<span style="color:#ff7700;font-weight:bold">try</span>:
content = <span style="color:#dc143c">urllib2</span>.<span style="color:black">urlopen</span><span style="color:black">(</span>url<span style="color:black">)</span>.<span style="color:black">read</span><span style="color:black">(</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">print</span> count_lines<span style="color:black">(</span>content.<span style="color:black">split</span><span style="color:black">(</span><span style="color:#483d8b">'<span style="color:#000099;font-weight:bold">\n</span>'</span><span style="color:black">)</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">except</span> <span style="color:#dc143c">urllib2</span>.<span style="color:black">URLError</span>, e:
<span style="color:#ff7700;font-weight:bold">print</span> -<span style="color:#ff4500">1</span>
<span style="color:#ff7700;font-weight:bold">return</span> <span style="color:#ff4500">0</span></pre></td></tr></table></div>
<p>獨立出函式後,就能直接測算字數的部份:</p>
<div><table><tr><td><pre>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">class</span> CountLinesTest<span style="color:black">(</span><span style="color:#dc143c">unittest</span>.<span style="color:black">TestCase</span><span style="color:black">)</span>:
<span style="color:#ff7700;font-weight:bold">def</span> testEmptyContent<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
actual = wc.<span style="color:black">count_lines</span><span style="color:black">(</span><span style="color:black">[</span><span style="color:#483d8b">""</span><span style="color:black">]</span><span style="color:black">)</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span><span style="color:#ff4500">0</span>, actual<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> testAWord<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
actual = wc.<span style="color:black">count_lines</span><span style="color:black">(</span><span style="color:black">[</span><span style="color:#483d8b">"camel"</span><span style="color:black">]</span><span style="color:black">)</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span><span style="color:#ff4500">1</span>, actual<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> testWords<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
actual = wc.<span style="color:black">count_lines</span><span style="color:black">(</span><span style="color:black">[</span><span style="color:#483d8b">"a camel can fly"</span><span style="color:black">]</span><span style="color:black">)</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span><span style="color:#ff4500">4</span>, actual<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> testWordsWithSpaces<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
actual = wc.<span style="color:black">count_lines</span><span style="color:black">(</span><span style="color:black">[</span><span style="color:#483d8b">" a camel can fly "</span><span style="color:black">]</span><span style="color:black">)</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span><span style="color:#ff4500">4</span>, actual<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> testWordsWithAdjacentSpaces<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
actual = wc.<span style="color:black">count_lines</span><span style="color:black">(</span><span style="color:black">[</span><span style="color:#483d8b">" a camel can <span style="color:#000099;font-weight:bold">\t</span>fly "</span><span style="color:black">]</span><span style="color:black">)</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span><span style="color:#ff4500">4</span>, actual<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> testMultiLines<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
actual = wc.<span style="color:black">count_lines</span><span style="color:black">(</span><span style="color:black">[</span><span style="color:#483d8b">"a"</span>, <span style="color:#483d8b">"b c"</span>, <span style="color:#483d8b">"d e f"</span><span style="color:black">]</span><span style="color:black">)</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span><span style="color:#ff4500">6</span>, actual<span style="color:black">)</span></pre></td></tr></table></div>
<p>看來不壞,上面的單元測試可以確保算字數的部份是對的。若上面的測試失敗,也能明白錯在那段程式,又有精簡的輸出入範例協助除錯。並且,獨立出來的函式 (count_lines) 可供其它程式使用。</p>
<p>但是對整個程式來說,我們還是擺脫不了下列問題: </p>
<ul>
<li>無法保證測試結果一致。</li>
<li>測試費時。</li>
</ul>
<h4>
第三版:將網路連線的部份封裝成物件,並用「傳入」的方式使用它</h4>
<p>問題出在 main() 直接用 urllib2.urlopen() 連上網路,若能在測試時替換成我們準備好的函式,就能確保測試結果一致,且減少網路連線的時間。也許有人會說,可以在測試前直接換掉 urllib2.urlopen。這是一個解法,但不建議這麼做,原因日後再說明。比較適當的作法如下:</p>
<div><table><tr><td><pre>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">class</span> Client<span style="color:black">(</span><span style="color:#008000">object</span><span style="color:black">)</span>:
<span style="color:#ff7700;font-weight:bold">def</span> get<span style="color:black">(</span><span style="color:#008000">self</span>, url<span style="color:black">)</span>:
<span style="color:#ff7700;font-weight:bold">return</span> <span style="color:#dc143c">urllib2</span>.<span style="color:black">urlopen</span><span style="color:black">(</span>url<span style="color:black">)</span>.<span style="color:black">read</span><span style="color:black">(</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> count_web_page<span style="color:black">(</span>client, url<span style="color:black">)</span>:
<span style="color:#ff7700;font-weight:bold">try</span>:
content = client.<span style="color:black">get</span><span style="color:black">(</span>url<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">except</span> <span style="color:#dc143c">urllib2</span>.<span style="color:black">URLError</span>, e:
<span style="color:#ff7700;font-weight:bold">return</span> -<span style="color:#ff4500">1</span>
<span style="color:#ff7700;font-weight:bold">return</span> count_lines<span style="color:black">(</span>content.<span style="color:black">split</span><span style="color:black">(</span><span style="color:#483d8b">'<span style="color:#000099;font-weight:bold">\n</span>'</span><span style="color:black">)</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> main<span style="color:black">(</span>url<span style="color:black">)</span>:
<span style="color:#483d8b">''</span><span style="color:#483d8b">'<span style="color:#000099;font-weight:bold">\</span>
%prog [options] <url>
Print the number of words in <url>.
Print -1 if <url> is invalid.
'</span><span style="color:#483d8b">''</span>
client = Client<span style="color:black">(</span><span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">print</span> count_web_page<span style="color:black">(</span>client, url<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">return</span> <span style="color:#ff4500">0</span></pre></td></tr></table></div>
<p>於是我們可以用 <a href="http://martinfowler.com/articles/mocksArentStubs.html">mock</a> (我用 <a href="http://code.google.com/p/pymox/wiki/MoxDocumentation">pymox</a>) 準備假的 Client 反應出我們期望的網頁連線結果,輕易地測試正常連線和網址無效的情況:</p>
<div><table><tr><td><pre>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
</pre></td><td><pre style="font-family:monospace"><span style="color:#ff7700;font-weight:bold">class</span> CountWebPageTest<span style="color:black">(</span><span style="color:#dc143c">unittest</span>.<span style="color:black">TestCase</span><span style="color:black">)</span>:
<span style="color:#ff7700;font-weight:bold">def</span> testCountWebPage<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
<span style="color:#808080;font-style:italic"># Prepare the fixture</span>
url = <span style="color:#483d8b">'http://www.google.com/'</span>
client = mox.<span style="color:black">MockObject</span><span style="color:black">(</span>wc.<span style="color:black">Client</span><span style="color:black">)</span>
client.<span style="color:black">get</span><span style="color:black">(</span>url<span style="color:black">)</span>.<span style="color:black">AndReturn</span><span style="color:black">(</span><span style="color:#483d8b">'<html> ... </html>'</span><span style="color:black">)</span>
mox.<span style="color:black">Replay</span><span style="color:black">(</span>client<span style="color:black">)</span>
<span style="color:#808080;font-style:italic"># Run</span>
actual = wc.<span style="color:black">count_web_page</span><span style="color:black">(</span>client, url<span style="color:black">)</span>
<span style="color:#808080;font-style:italic"># Verify</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span><span style="color:#ff4500">3</span>, actual<span style="color:black">)</span>
mox.<span style="color:black">Verify</span><span style="color:black">(</span>client<span style="color:black">)</span>
<span style="color:#ff7700;font-weight:bold">def</span> testPageNotFound<span style="color:black">(</span><span style="color:#008000">self</span><span style="color:black">)</span>:
<span style="color:#808080;font-style:italic"># Prepare the fixture</span>
url = <span style="color:#483d8b">'http://www.google/'</span>
client = mox.<span style="color:black">MockObject</span><span style="color:black">(</span>wc.<span style="color:black">Client</span><span style="color:black">)</span>
client.<span style="color:black">get</span><span style="color:black">(</span>url<span style="color:black">)</span>.<span style="color:black">AndRaise</span><span style="color:black">(</span><span style="color:#dc143c">urllib2</span>.<span style="color:black">URLError</span><span style="color:black">(</span><span style="color:#483d8b">"..."</span><span style="color:black">)</span><span style="color:black">)</span>
mox.<span style="color:black">Replay</span><span style="color:black">(</span>client<span style="color:black">)</span>
<span style="color:#808080;font-style:italic"># Run</span>
actual = wc.<span style="color:black">count_web_page</span><span style="color:black">(</span>client, url<span style="color:black">)</span>
<span style="color:#808080;font-style:italic"># Verify</span>
<span style="color:#008000">self</span>.<span style="color:black">assertEqual</span><span style="color:black">(</span>-<span style="color:#ff4500">1</span>, actual<span style="color:black">)</span>
mox.<span style="color:black">Verify</span><span style="color:black">(</span>client<span style="color:black">)</span></pre></td></tr></table></div>
<p>回頭看原本的程式有那些改變:</p>
<ul>
<li>獨立出 Client 物件,用來封裝網路連線的操作。</li>
<li>傳 client 給 count_web_page() ,而不是在 count_web_page() 內部 new 出 Client。</li>
</ul>
<p>想想看,若沒有上述兩個改變,要怎麼滿足測試需求呢?該怎麼測試各種網路連線問題呢?這個問題觸及容易測試和不易測試程式的關鍵差異。</p>
<h4>
結語</h4>
<p>就結論來說,一但在函式內直接用全域變數、全域函式或使用自己 new 的物件 X,就不容易測試之後的程式了,因為之後的程式邏輯受到 X 的影響,但測試程式又無法直接控制 X 的行為。當然我們無法完全避免這個情況,總會有全域函式、需要 new 物件。重點在於,我們能將程式隔離到什麼程度?有沒有留「後門」讓測試程式方便控制內部邏輯。</p>
<p>也許有人會懷疑為了測試而改變原本的寫法,是否有些本末倒置?不妨自己練習做個幾次,對照一下原本的寫法和修正後的寫法,用自己的經驗判斷:<b>為測試而改變的結果,是否恰好讓程式變得更彈性、更易重用?</b></p>
<p>附帶一提,上面的範例碼是逆向寫出來的。我是先用 TDD 寫出最後的版本,再將拆開的東西塞回去 main,弄出「直覺的寫法」。習慣 TDD 後,會改變寫程式的思維。先寫測試有助於寫出易測的程式。</p>fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-25444112006569518432010-04-05T17:23:00.000+08:002011-05-15T00:56:26.158+08:00[譯文] 為什麼我們不好意思承認,我們不知道如何寫測試?<p>原文見:<a href="http://misko.hevery.com/2009/07/07/why-are-we-embarrassed-to-admit-that-we-dont-know-how-to-write-tests/">《Why are we embarrassed to admit that we don’t know how to write tests?》</a>。</p>
<h4>前言</h4>
<p>這篇帶給我很大的影響。對我來說,明白「可測性是最重要的」是一大里程碑。隨著經驗累積,了解得愈深,愈明白 <a href="http://misko.hevery.com/">Miško Hevery</a> 寫得多有道理。就當我打算寫篇心得時,才發覺很容易變成用我的話重說 Miško Hevery 說過的東西,而且還很容易漏講。轉念一想,乾脆翻譯他的文章好了。二月底時徵得他的同意,沒想到一拖就拖了一個月半,真不好意思。</p>
<p>大家覺得那裡譯得不好或呈現方式不好,就直接反應出來 。透過 Buzz、Murmur、Plurk、Facebook 或在此留言都可。謝啦。</p>
<p><span></span></p>
<h4>本文</h4>
<p>Take your average developer and ask “do you know language/technology X?” None of us will feel any shame in admitting that we do not know X. After all there are so many languages, frameworks and technologies, how could you know them all? But what if X is writing testable code? Somehow we have trouble answering the question “do you know how to write tests?” Everyone says yes, whether or not we actually know it. It is as if there is some shame in admitting that you don’t know how to write tests.</p>
<p>找出你公司內一般水準的開發者並問他們「你會語言/技術 X嗎?」沒有人會為承認自己不懂 X 而感到不好意思。畢竟有太多程式語言、框架和技術,你怎麼可能全部都會?但若 X 是寫出能被測試的程式呢?不知為何,我們很難回答這個問題:「你會寫測試嗎?」不論我們是否真的懂,每個人都回答會。就如同承認自己不懂寫測試是件很不好意思的事。</p>
<p>Now I am not suggesting that people knowingly lie here, it is just that they think there is nothing to it. We think: I know how to write code, I think my code is pretty good, therefore my code is testable!</p>
<p>我不是暗示人們故意說謊,而是他們認為測試沒什麼大不了的。我們認為「我知道如何寫程式,我覺得我的程式相當不錯,因此程式是可以測試的!」</p>
<p>I personally think that we would do a lot better if we would recognize testability as a skill in its own right. And as such skills are not innate and take years of practice to develop. We could than treat it as any other skill and freely admit that we don’t know it. We could than do something about it. We could offer classes, or other materials to grow our developers, but instead we treat it like breathing. We think that any developer can write testable code.</p>
<p>我個人認為如果我們能意識到可測試性是一個獨立的技術,我們可以做得好多了。這種技術並不是天生就會的,需要經年累月的練習來培養。我們可以將它視為另一項技術並坦率地承認我們不會這項技術。於是我們就能對它做點事。我們能提供課程或其它教材來讓開發者成長,而不是將寫測試的技術視為如同呼吸的能力,好像任何開發者都會寫可以測試的程式。</p>
<p>It took me two years of writing tests first, where I had as much tests as production code, before I started to understand what is the difference between testable and hard to test code. Ask yourself, how long have you been writing tests? What percentage of the code you write is tests?</p>
<p>在我開始了解可測試的程式和難以測試的程式的差別前,我花了兩年的時間先寫測試。在這些程式裡,測試碼的量和產品碼一樣多。問問你自己,你持續寫測試多久了?在你寫的程式裡,測試碼占了百分之多少?</p>
<p>Here is a question which you can ask to prove my point: “How do you write hard to test code?” I like to ask this question in interviews and most of the time I get silence. Sometimes I get people to say, make things private. Well if visibility is your only problem, I have a RegExp for you which will solve all of your problems. The truth is a lot more complicated, the code is hard to test doe to its structure, not doe to its naming conventions or visibility. <a href="http://misko.hevery.com/2008/07/30/top-10-things-which-make-your-code-hard-to-test/">Do you know the answer?</a></p>
<p>你可以問這個問題來證明我的觀點:「你如何寫出難以測試的程式?」我喜歡在面試時問這個問題,多數的時候我得到沉默的回應。有時有人回答「隱藏物件」。嗯,如果物件的可見範圍是唯一的問題,我可以給你一個正規表示式讓你解決這問題(譯者注:我猜是在測試程式前先用字串比對把程式內所有 private 換成 public,那就可以測了)。真正的答案複雜許多,是因為程式的結構造成它難以測試,而不是命名習慣或物件的可見範圍。<a href="http://misko.hevery.com/2008/07/30/top-10-things-which-make-your-code-hard-to-test/">你知道答案嗎?</a></p>
<p>We all start at the same place. When I first heard about testing I immediately thought about writing a framework which will pretend to be a user so that I can put the app through its paces. It is only natural to thing this way. This kind of tests are called end-to-end-tests (or scenario or large tests), and they should be the last kind of tests which you write not the first thing you think of. End-to-end-tests are great for locating <a href="http://misko.hevery.com/2008/11/17/unified-theory-of-bugs/">wiring bugs</a> but are pretty bad at locating <a href="http://misko.hevery.com/2008/11/17/unified-theory-of-bugs/">logical bugs</a>. And most of your mistakes are in logical bugs, those are the hard ones to find. I find it a bit amusing that to fight buggy code we write even more complex framework which will pretends to be the user, so now we have even more code to test.</p>
<p>一開始我們都是一樣的。當我第一次聽到測試時,我立即想到寫一個框架來假裝使用者,使得我能用它來執行被測的應用程式。很自然會這麼想。這類型的測試被稱為使用者端測試(end-to-end-tests)(或是情境測試、大型測試),它們應該是你最後寫的測試,而不是一開始想到的。使用者端測試很適合找出 <a href="http://misko.hevery.com/2008/11/17/unified-theory-of-bugs/">wiring bugs</a>(譯者注:不知該怎麼翻,請參見 wiring bug 連結的說明),但不適合找出<a href="http://misko.hevery.com/2008/11/17/unified-theory-of-bugs/">邏輯錯誤</a>。並且,你的多數錯誤會是邏輯錯誤,它們才是難以找到的錯誤。我發覺這有些有趣,為了對抗有錯誤的程式我們卻寫了更複雜的框架來假裝使用者,於是我們有更多程式待測。</p>
<p>Everyone is in search of some magic test framework, technology, the know-how, which will solve the testing woes. Well I have news for you: there is no such thing. <strong>The secret in tests is in writing testable code,</strong> not in knowing some magic on testing side. And it certainly is not in some company which will sell you some test automation framework. Let me make this super clear: <strong>The secret in testing is in writing testable-code! </strong>You need to go after your developers not your test-organization.</p>
<p>每個人都在尋找解決測試麻煩的神奇測試框架、技術、知識。然而我有個消息要告訴你:沒有這種東西。<strong>測試的祕訣就是寫出能被測試的程式碼,</strong>而不是明白測試領域中某種魔法。並且大概不會有某家公司賣你某種自動測試框架。讓我說得更清楚一些:<strong>測試的祕訣就是寫出能被測試的程式碼!</strong>你需要關注你的開發者而不是你的測試組織。</p>
<p>Now lets think about this. Most organizations have developers which write code and than a test organization to test it. So let me make sure I understand. There is a group of people which write untestable code and a group which desperately tries to put tests around the untestable code. (Oh and test-group is not allowed to change the production code.) The developers are where the mistakes are made, and testers are the ones who feel the pain. Do you think that the developers have any incentive to change their behavior if they don’t feel the pain of their mistakes? Can the test-organization be effective if they can’t change the production code?</p>
<p>現在讓我們想想這點。大部份的組織讓開發者寫程式,接著讓一個測試組織來測試。讓我確保我明白這是怎麼回事,有組人馬寫出無法測試的程式,和另一組人馬分頭試著測試這些無法測試的程式(喔,而且測試小組不被允許改變產品碼)。開發者是錯誤的來源,測試者感受這些痛苦。你認為有任何誘因讓開發者改變他們的行為 — 如果他們沒有為他們製造的錯誤而感到痛苦?在不能改變產品碼的情況下,測試組織能夠有效地作事嗎?</p>
<p>It is so easy to hide behind a “framework” which needs to be built/bought and things will be better. But the root cause is the untestable code, and until we learn to admit that we don’t know how to write testable code, nothing is going to change…</p>
<p>躲在能建造/買來的框架後面是很容易的,事情也會改善。但是問題的根源是無法測試的程式,直到我們學會承認我們不懂如何寫出能被測試的程式,情況不會改變的...。</p>
<h4>相關閱讀</h4>
<ul>
<li><a href="http://www.youtube.com/watch?v=wEhu57pih5w">Google Tech Talks: The Clean Code Talks — Unit Testing</a>:內容主旨和這篇文章差不多。Miško Hevery 的口語表達很生動,值得一看。</li>
<li><a href="http://misko.hevery.com/2008/07/30/top-10-things-which-make-your-code-hard-to-test/">Top 10 things which make your code hard to test</a>:提及文中問題的答案,值得一讀。</li>
</ul>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/1Q8H8x4Tq_k" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0tag:blogger.com,1999:blog-451051488891335923.post-74499960506378792222010-04-01T22:12:00.000+08:002011-05-15T00:56:27.585+08:00我們不能改變手裡的牌,但是可以決定如何出牌。<p>在愚人節發文章好像怪怪的,搞不好會被人誤以為在說反話。</p>
<p>我很喜歡 <a href="http://www.thelastlecture.com/">Randy </a>說的這段話:</p>
<blockquote><p>我們不能改變手裡的牌,但是可以決定如何出牌。</p></blockquote>
<p>了解自己能改變什麼、不能改變什麼,專注在自己能影響的範圍裡,就沒什麼需要煩惱的事了。</p>
<img src="http://feeds.feedburner.com/~r/fcamel/~4/skDc8DzLpQM" height="1" width="1">fcamelhttp://www.blogger.com/profile/04792244455260595133noreply@blogger.com0