2014年5月10日 星期六

從不同程式語言的異同思考問題的本質

《學程式語言的樂趣》描述我早期學程式語言的事,後來幾年多了一些心得 ( Python, Haskell, C++, Objective-C ),之後有機會再補上。這裡想寫的是藉由學習不同程式語言後明白的事。

學特別的程式語言有助於掌握不同的概念。我最初慣用的語言是 C (高中三年),再來是 Java (大學四年)。藉由 Java 開始接觸 OOP 的思維,除了程式語法之外,學到更多的是寫程式的思考方式。其實回頭再去看 C,也可以使用一樣的思維,只是當語言直接綁入思維時,會被迫這麼做。就像用 Haskell 必須寫出 stateless 的程式,就會更明確地留意 state。不過學 Java 的時候還沒察覺到這點。

後來長時間地陸續用了 Ruby、Python、C++,還有加減寫 JavaScript。開始掌握到一些程式語言背後共同的特性:組合邏輯控制,以及管理資料的狀態。回歸到最原始的電路來看,程式的運作就是邏輯控制 (and/or) 和狀態 (on/off)。當要做的事變複雜時,必須結構性地管理它們。

舉例來說:

  • Java 定了 class 和 interface 表示「資料+實作邏輯」和「操作介面」,class 內又有 public/(default)/protected/private 限制資料的存取範圍。還有 package 封裝同一群 class。
  • Python 表現的比較直接,容易連結回 C 該如何表現這些思維。每個 method 第一個參數一定是物件本身,慣例命名為 self。'_' 開頭的 function 和 method 表示只限於內部使用。用 module 封裝同一群 class。
  • C++ 藉由不同的方式使用 virtual,class 可當作 "class" 或當作 "interface",或帶有實作的 "interface"。class 有 public/protected/private 限制資料的存取範圍,但又有 friend 可讓指定的 class 存取無視這些限制。有 namespace 封裝同一群 class,還有 anonymous namespace 可以嚴格的封裝實作細節。

不同的語言有各自擅長的地方,學習別的語言的時候,會想要借鏡別的語言的特色,運用到原本常用的語言裡。但是接觸過 Scheme 和 Haskell 再回頭嘗試用在 Python 和 Java 的時候,遇到很大的問題:

  • 無法直覺地使用天生不支援的功能。比方說 Java 的 function 不是 first-class function,得用物件包一層,再自成一套體系。例如用 functionaljava
  • 和原本語言本身的風格不合,增加其他人的維護成本。

第二點是比較大的問題。自己用了一套「functional Java」的寫法,其他人也得學習如何解讀。以 Python 舉例,下面是同一件事用三種不同寫法:

for-loop

numbers = []
for i in range(0, 10):
    numbers.append(i * 2)

list comprehension

numbers = [i * 2 for i in range(0, 10)]

map

numbers = map(lambda i: i * 2, range(0, 10))

對沒學過 Python 的人來說,for-loop 比較直覺,沒學過 Python 也可推敲出意思;初看 list comprehension 不明白這是什麼,不過花點時間看幾個例子就懂了。map 比較困難,得有 functional programming (FP) 的概念才能明白。

Python 本來就有這些語法,我們不會責怪這樣寫的人。但用在 Java 裡就有爭議了,為什麼要另外包一套框架,寫出要別人花額外工夫才能讀懂的程式?

後來我察覺到更深層的思維:OOP 也好、FP 也好,目的都是結構性地管理資料和邏輯,只是表達的方式不同。掌握到這點後,我將時間花在有助於真正目的事上,而不是寫得更 OOP 或 FP。原本遇到的困擾 (如何在 B 語言裡善用 A 語言的特性) 自然就不存在了。

舉例來說:

  • 為什麼 FP 要強調 immutable,還有習慣用 map? 因為 immutable 的資料沒有 state,自然沒有被誤改的風險。有沒有用 FP 的 map 到不是重點,map 只是方便套用一套控制邏輯產生另一批 immutable 資料。是實作層級的小幫手,不是設計準則。
  • 為什麼 OOP 強調 Tell, Don't Ask? 也是為了減少外漏 state。即使用沒有 OOP 的語言,也可這麼做。像 iOS 的 Quartz 2D,API 需要用的 CGContextRef 是 struct CGContext* 的 typedef,但是使用者不知道 struct CGContext 是什麼,只知道呼叫函式時要傳入 CGContextRefQuartz 2D 封裝需要的 state 在 struct CGContext 內,隨時可改變它的結構。
  • 為了方便模組之間獨立開發,不用擔心日後改寫模組內部的實作造成外部程式出錯,最重要的是定出乾淨的介面,讓別人不會誤用。再來才是善用語言的特性讓別人想誤用也不行。例如 C++ 的 anonymous namespace 是隱藏實作的好方法;Java 的 package + (default) class 也是好方法;Python ... 只能用底線命名然後祈禱不要有人亂搞。

從問題的本質 (管理資料和邏輯) 來看,自然會明白這些設計準則,還有區分出設計和實作 (準則和表現方式)。剩下的是衍生想法,像是管理 state 方法的優先順序:

  • 沒有 member field (沒 state 最好!)
  • 有 immutable member field (非得有 state 不可的話,至少讓它不可修改)
  • private mutable member field (非改不可的話,盡可能減少能改它的程式碼)
  • ...

重點還是掌握問題的本質,避免迷失在解決的方案之中。

結語

總結來說,一套程式語言有一套思維,試過多套不同特性的語言後,藉由它們之間的同異處,可以察覺這些語言想解決的問題是什麼。繼而明白真正的問題是什麼,再回饋到解決問題的思維中,避免落入特定解決模式裡 (如道地的 OOP 或 FP)。起初學不同語言只是覺得好玩,誤打誤撞得到這樣的體會,滿有意思的。

相關文章

2014年3月2日 星期日

事情的另一面: 測試與除錯技巧

回顧過去數年的經驗,最大的體悟除《從需求出發理解背後技術的思考脈胳》以及《專注於滿足需求而非工具或方法》之外,大概就是沒有所謂的鐵則,許多事情都有另一面,端看你處於什麼情境、從什麼角度看它。

上份工作以 machine learning 為主 (2008 ~ 2011),針對一個特定的應用問題,實驗不同的演算法,看看能否實際運用在產品上。結果不如預期時,會陷入不知該懷疑演算法、演算法參數、還是程式寫錯的窘境。因此,對於程式正確的要求,遠高於其它產品。這樣才能專注地藉由實驗修正理論模型。在這個需求之下,Test Driven Development 就成了相當有用的開發方式。經過數次用 TDD 開發的經驗後,總算能夠達到和不用 TDD 差不多的開發效率。這意味著用了 TDD 也不會拖慢完成第一版的時間,並且程式更易於理解和維護。儼然是 TDD 的完全勝利!

但這個經驗有個代價。代價不是學習 TDD 的時間,畢竟學什麼工具或方法 (如OOP) 都要時間。學習 TDD 對立的結果是: 學不到除錯的技巧。

起初我以為除錯是果,治本的方式自然是避免有因。那麼,TDD 貌似完美的解法,學 TDD 即可。但是開發專案免不了團隊合作,也免不了使用第三方程式。換句話說,錯誤遍地都是,有時甚至是作業系統或編譯器的錯。拙劣的除錯技巧無法適應這個時代。我在和別人一起除錯時才發覺這事。對方能力很好,不過開發習慣不太好,常犯一些「我無法想像的錯誤」。但也因為他的習慣,讓他可以比較快看懂別人寫的亂糟糟的程式,可以比較快想到問題可能出錯的地方。我費了不少力氣才補足這塊,實在是始料未及的事。只能說,該走過的路還是得走,無法省去。

目前的工作以 C++ 為主,為了在大量的原始碼裡除錯,偶而會視需求加強一下 gdb 的使用技巧,還有練習寫 python script 簡化 gdb 操作程序。相對於兩年前,gdb 的操作技巧進步不少。另外也寫了 gj 幫助閱讀程式碼。但是在觀察同事的開發方式後,發覺我有時過於依賴工具的便利性,反而減少全面性的思考。最後還是得有系統地一步步思考、推論,才能有效率地解決問題 (關於這點,之後有適當材料再另寫文章說明「系統化的解決問題」)。換句話說,熟練除錯工具反而無意識地減少我系統性思考的時間,也滅少我系統性思考的經驗。我的意思並非捨棄除錯工具,像 Sherlock 那樣全部都在自己的思維宮殿裡解決 (雖說那樣實在是相當地帥啊!)。凡人如我等,還是需要工具輔助搜集情報和記錄訊息。只是這兩件事是相斥的,愈是熟練除錯工具,愈少思考;愈長思考,自然也愈不依賴除錯工具。

再回頭看 TDD,目前的工作絕大多數情況不適用 TDD。一來不像開發 machine learning 工具那樣,要求近乎100%正確。二來大部份的程式和 GUI 相關,本來就不容易測試。若要達到像以前一樣的開發效率,我得先熟悉 C++,再熟悉 C++ 基本的 unit test 工具,再熟悉和 GUI 相關的測試工具和知識。對照達到後帶來的好處,相當地不划算。不過其中一個和 GUI 無關但和網路高度相關的子專案,我很自然地用 Python + TDD 的方式開發核心部份。日後上線時,也從中獲得明確的回報: (1) 極少的錯誤。以及 (2) 透過單元測試輕鬆地重制線上偶而才會發生的網路錯誤,只更新一次程式碼就修正了問題。可參考《寫出容易測試的程式》了解類似的處境和用到的技巧。當然,我也因此「失去」一些線上除錯的經驗。

除了測試與除錯的心得外,軟體設計模式和軟體開發準則,也讓我經歷了幾次「打破鐵則」的心路歷程。之後再另寫文章補充。

回顧這些事,讓我明白個人經驗的侷限,而減少過度歸納和推衍的習慣。看別人的論述時,會多想想自己和對方的情境,從中得出目前我能用到的部份。不會過於尋找或遵從「聖杯」。白話來講,就是比較務實吧。

2013年7月20日 星期六

了解 C/C++ 程式行為的技巧

多數情況下我們會使用別人的程式,或是參與別人開發已久的程式,比較少是自己重頭撰寫所有程式。由此可知,了解程式行為的技巧相當重要,但是很少看到有書籍討論這部份的事,或許是因為很難理出一個主題吧。

下面以了解 C/C++ 程式為主,列出自己一些零散心得。

動態分析

在程式碼裡填入觀察碼

加入程式 log function call 是直接有效的作法,而且做起來不如想像的麻煩,詳見 《trace C/C++ function call 的方法》

或是找看看前人是否有了下除錯的 flag。有經驗的程式設計師在開發時必定有一套除錯方式,只是在正式使用時關掉了這些除錯功能,詳見《C/C++ 檢查和打開 debug 功能的小技巧》

附帶一提,自己開發程式時,除了留下除錯程式之外,記得要考慮使用 gdb 的影響,讓除錯程式更易於使用,詳見 《除錯小技巧: 在程式中直接中斷及偵測是否被 gdb 監控中》

觀察使用的函式庫

程式碼通常會用到第三方函式庫,有時我們可以從中得知一些線索,《python BaseHTTPServer 速度緩慢的原因》是一個簡短的例子,說明如何使用 ltrace 找出關鍵的第三方函式,繼而找出效率瓶頸。使用 ltrace 無須重編程式或加入 debug symbol。不過有可能讓程式變得不穩定,這時可使用 -l filename 只觀察部份函式庫,執行的時候會比較穩定一些。

其它兩則和 ltrace 相關的例子:

觀察使用的 system call

system call 是程式和 kernel 溝通的途徑,介面數量相對少,容易集中觀察。有時藉由觀察 system call 夾帶的參數,可以提供一些線索。比方說,不論上層函式如何包裝,開啟檔案最後會用到 open(),可以從 open() 的參數 找出程式目前使用的設定檔或 log 檔位置。或是像《用 strace 找出 Ubuntu 如何提示未安裝的指令》,使用 strace 觀察 system call 直接解答疑惑。

ltrace 一樣,無須重編程式也不用 debug symbol。更棒的是,使用 strace() 執行程式還滿穩定的。

其它 strace 相關的例子:

使用 gdb

debugger 的重要性無須多言,《gdb 初步心得》條列我常用到的指令。另外值得一提的是,有 core dump 的時候,core dump 裡一定會記錄產生 segmentation fault 的 thread,不用擔心找錯 thread,原因見《linux thread 與 signal》

編譯上線的程式時,多數會加上 -O2 最佳化效能,讓程式實際執行的狀況和程式碼有些出入。雖然如此,仍然可以加上 -g 觀察程式的 core dump,但要留意觀察的結果不見得是對的,詳見《debug info 和 optimization》。平時觀察程式行為還是用 -O0 -g 編譯較適當。

以下是其它和 gdb 相關的小技巧:

從 kernel 切入

有些情況下我們沒辦法從程式內部或 gdb 取得資訊,比方說程式莫明奇妙地收到 SIGKILL 而結束。由於程式無法攔截 SIGKILL,不方便查出凶手是誰。雖然可以在相關程式內直接對 system call kill() 設中斷點,但若凶手是外部程式,就沒輒了。

這種時候可用 SystemTap 直接從 kernel 內觀察是什麼程式請求使用 kill 送出 SIGKILL。我自己還沒有第一手經驗,這個例子是從同事那邊學來的,在這裡備忘觀察程式時,還可以用 SystemTap 觀察更底層的行為。

靜態分析

從執行檔找出關鍵字

《配合 c++filt 讀程式》說明如何從執行檔中找關鍵字,可藉此找出 UI 相關字串或是可能的函式名稱,有時比直接從程式碼下手容易。

找出 symbol 的定義或使用到 symbol 的程式碼

C 的情況比較單純,相關工具比較正確,也有工具可以產生精美的 call graph。但 C++ 的情況複雜許多,最後我決定無視 C++ 語法,直接找出所有和目標 symbol 有關的程式。《閱讀 C/C++ 原始碼的好幫手》有整理相關工具, 《查 C/C++ symbol 定義的方法》有一點關於我使用 gj 的方法。

使用 doxygen 產生 class 階層關係圖

大型專案會依功能切成數個模組,模組本身亦有一套自己使用 class 的方法。直接看程式碼容易陷入見樹不見林的困境。這時可用 class 階層關係圖協助了解整體架構。產生階層圖的方式見《用 doxygen 產生 class hierarchy diagram》。再輔以 gdb 產生 backtrace 觀察類別、模組之間的使用關係,比較容易明白整體架構。

善用編輯器或 IDE

目前是逐步學習如何強化用編輯器或 IDE 讀程式,加快在相關程式碼中探索的時間,還沒整理出一套完整的流程。

比方說用 gj 跳到某個使用函式 foo() 的檔案後,再來我會想知道目前在那個函式裡,《vim 顯示目前函式的名稱》就是針對此情境在 vim 內加了快速鍵做這這件事。或是像《使用 vim script 自動開啟 C/C++ 程式的標頭檔或程式碼》說明如何在 .h 檔裡按 F4 快速開啟 .c 或 .cpp 檔,或是反過來在 .c 或 .cpp 裡開啟 .h。

預防勝於治療

理解到了解程式是如此不容易後,行有餘力時,別忘了加強測試碼,讓日後使用相關程式的人更快進入狀況。良好的 unit tests 也是不錯的使用範例,有助於了解模組的行為。

核心分析

2014-01-05 更新。

現在來看,這段的概念才是理解大型專案時最重要的技巧。經驗愈多,愈覺得如此。說來容易做來難,需要累積經驗後才能體會。

想像自己會如何進行開發

有經驗的軟體工程師,做事的方式十之八九也差不多。特別是需求愈嚴苛,最後的解法也不會差太多。先自己想像可能的脈絡,之後比較容易縮小觀察目標,專注驗證自己的假設,並從驗證結果獲得新的契機製定下一步。

尋找原有專案的除錯工具

經年累月的專案必定有自己一套除錯工具,不然很難長期維護。有經驗的工程師應該也會在開發過程中研發出負責專案所需的除錯工具。所以,最有效觀察程式的方式,就是用原專案專用的工具。

對目標專案有一定了解後,可以自己想像,若自己進行同樣類型的專案開發,可能會做什麼輔助工具呢?有機會從中猜中切入點。

2013年6月29日 星期六

非同步程式心得

在這個網路盛行的時代,程式中常常需要連網路取用外部資料,並且不希望因此阻礙主程式的活動,這時會使用非同步操作。或是程式中途需要使用者輸入資料、決定繼續或取消等,若不希望阻礙主程式,也會用非同步操作。

以連上外部伺服器為例,介面可能像是這樣:

bool LoginToServer(const std::string& name,const std::string& password);
void OnLoginToServer(const std::string& name,int error);

主程式呼叫 LoginToServer() 的當下只能由回傳值知道有沒有進行登入,但不知道是否登錄成功。過一段時間後,某個 thread 會呼叫 OnLoginToServer() 透過 error 的值告知主程式登入成功或失敗。

非同步溝通介面

以下以 caller 和 async module 分別表示「使用非同步 API 的函式」以及「提供非同步 API 的模組」。async module 提供的 API 有兩種選擇:

  • 使用 callback 的方式,async module 有結果時通知使用 callback 通知 caller。
  • caller 事後自行詢問先前的呼叫是否已有結果。

兩種介面各有優缺點,不過 callback 似乎比較盛行?

使用 callback 的方式

async module 會保證通知 caller,這有以下的好處:

代價是 caller 需要記下呼叫當下的 context,callback 時才知道怎麼完成剩下的工作。並且不方便閱讀以及用 debugger 追踪程式。

以續傳上傳為例,假設網站提供的介面如下:

  1. /create/?filename=X&size=Y: 得知上傳檔名和大小,回傳檔案代碼
  2. /upload/?filehandle=X&data=Y&offset=Z: 持續接收檔案內容
  3. /done/?filehandle=X: 結束上傳

client 使用 http 函式庫時,需要注意以下事項:

  • 需要類似 state machine 的結構,callback 時才明白目前進行到那一個步驟。
  • 需要記錄輔助資訊,像是目前在處理那個檔案,上傳到那一段資料。
  • 需要考慮中途的錯誤處理。
  • 結束後和主程式接軌的方法
  • 同時上傳多個檔案時,儲存狀態的資料結構需要更靈活一些。

使用詢問的方式

async module 可以提供 non-blocking 或 blocking 的詢問函式。使用 pthread_cond_wait() 之類的方式避免 busy waiting。於是 caller 可使用 non-blocking 的方式確認情況,或是使用另外特定的 thread 用 blocking 的方式確認情況。使用 blocking 的詢問方式時,caller 不需另外存下呼叫時的狀態,易寫易讀。

但是有多個非同步操作時,詢問的方式不容易即時延續先前非同步的操作。比方發出十個 http request 時,若是使用 callback 介面,那個 http response 好了,就先呼叫它的 callback 傳回 http response;使用詢問方式時,caller 得一個個用 non-blocking 詢問方式,或十個 thread 各用 blocking 詢問的方式,才能達到一樣的反應速度。

callback 介面的選擇

async module 提供 caller 傳入 callback 時,有幾種選擇:

  • async module 定義 class APIDelegate,caller 使用 async module 前放入 async module 規定的 APIDelegate,async module 之後會呼叫 APIDelegate 不同函式告知後續情況。比方說 iOS 的 NSURLConnection 定義了 NSURLConnectionDelegateNSURLConnectionDownloadDelegate
  • caller 呼叫 async module 的函式時,直接傳入 callback object,像 JavaScript 的 jQuery.ajax() 裡面的 success 以及 error 參數。

第一種介面明確,易讀難寫;第二種則是易寫難讀。若是長期發展的程式,我偏好第一種設計,有明確的名稱比較好追蹤程式碼。

不論是那種介面,caller 都需要自行記住呼叫當下的狀態,callback 回來時才知道如何完成後續工作。像 NSURLConnection 的 callback 只有告知 connection,connection 又不能作為 dictionary 的 key,使用上稍有不便。理想的情況下,callback 應該提供 context 帶有必要的資訊或是提供 unique ID 方便 caller 自行管理 context。比方說 Unix 的檔案操作提供一個 file handle,由系統記住檔案相關的設定和讀寫的狀態,caller 透過 file handle 可以取得所有相關資料。

async module 實作的選擇

實作 async module 有幾種選擇:

  • 新增一個 thread 讀寫外部資料,結束後呼叫 callback。
  • 使用 non-blocking I/O 讀寫外部資料,再由一個特定的 thread 持續追蹤結果,有結果後再呼叫 callback。由於可以使用系統函式 (如 select 或 epoll) 追蹤,負擔相對的低。

第一個作法易讀易寫,但是有以下的缺點:

  • 新增和消滅 thread 有時間成本。
  • thread 會占用額外記憶體。若一個 thread 占用 2MB,512 個會占用 1GB。
  • 大量 thread 同時讀寫資料時,增加額外 context switch 的成本。

若這個 module 只會偶而用到的話 (比方說登入伺服器),到是沒什麼問題。

第二個作法難讀難寫,也因此有些善心人士包好函式庫負責做這類事,像是 libeventlibev

雖然使用函式庫降低第二種作法的實作難度,程式碼還是不易閱讀。於是有人結合 coroutine 和 non-blocking I/O,做出更好用的框架。我目前只有用過 Python 的 gevent,寫起來如同寫 multi-thread,但是底層沒有用到大量 thread,免除了 thread 帶來的缺點。相信在其它語言也有類似的框架。

使用 coroutine 的時機

雖然 coroutine 兼具易寫易讀負擔又低等優點,它有個關鍵的難處:所有程式都要在 coroutine 的架構下,只要有一個 "thread" (正確說法是 subroutine) 卡住了 (比方不小心用到 blocking I/O),全部 "thread" 都會被卡住。現今的程式使用許多外部函式庫,很難保證不會發生這類事。

此外,依實作方式而定,coroutine 可能不方便使用多個 CPU,只要有一個工作需要大量 CPU 計算時間,也會拖累其它 "thread"。

所以一般的 GUI 程式或 client 端連線程式不見得適用 coroutine。但像 proxy server 這類主要工作是 I/O 且相當重視連線數量 scalability 的程式,就很適合用 coroutine

gevent 使用心得

以我自己的情況來說,主程式使用 gevent 處理 client 的需求,需要外部 http 連線時使用 geventhttpclient,滿容易達到 C10K。不過 unit test 的時候有些小問題,還有 gevent signal 處理有些 bug,踩到時要特別處理。

另外,雖然使用 gevent 的 monkey patch 可「無痛替換所有網路操作」,實測的結果效能不太好,而且我個人偏好「Explicit is better than implicit」,所以沒用 monkey patach。

結語

以上是這一年半來寫網路程式和 GUI 的心得。實作非同步程式時,要留意自己是處於 caller 或 async module 的角度。兩者需要考慮的事不同。特別是自己一手包辦整個程式時,切清楚兩者的角色才會有清楚的架構,方便日後維護。

備註

2012年8月5日 星期日

從需求出發理解背後技術的思考脈胳

最近在技術上多了不少體悟,關鍵是要掌握住問題的需求,接著不斷思考和嘗試,避免卡在特定的規則或工具裡,將重心放在隱藏在這些東西背後的核心思想。

直接看現有的工具或別人的作法很難有所體會,但自己從頭邊做邊想,會得出自己一套理論, 接著會出乎意料地更快理解這些工具和作法。畢竟在足夠嚴苛的需求下,方法可能有些差異,背後的精神卻是類似的。

比方說,若程式反應速度要求在 1ms 以內,全部資料來源都得在記憶體或網路,不能使用硬碟。那麼,針對這種應用,單機程式能做的事只有將東西塞在記憶體裡取用

然而記憶體有限,資料總有放不進去的時候。在這種速度要求且資料過大的前提下,可能的作法大概是:

  • 像 cache 那般有套機制留住最常用的資料。
  • 在記憶體裡存 index,index 內存必要的資料,若需要更多資料再從硬碟讀。
  • 先做前處理「壓縮」資料成摘要,摘要必須小到可以放入記憶體內。資料的品質(正確性)也許會有一些損失,但是是必要的取捨。之前在讀 YouTube 關聯影片和 IBM 的 Watson 論文時,有看到類似的思路,工作上也做過類似的事。和前述方法不同的地方是,資料的筆數變少,轉為另一種高質量的資料。

再以 Test Driven Development 為例,最重要的不是 TDD 的三步規則,也不是相關工具要怎麼用,而是導入「在設計的開頭, 就將測試視為主要考慮項目」,其它東西都是這個想法的衍生。

從這個角度出發,實作久了自然會理解為什麼介面要開洞放入物件,為什麼需要 factory 隔離生成和操作邏輯。至於是否真的有先寫測試,現在我覺得不是鐵則,在有為測試而考慮的設計下,在必要時補測試可能更划算,將時間花在刀口上。但若一開始沒為測試考慮,事後要補就很辛苦,加測試的價值又更低了

再往上拉一個層次來看,解決問題的前提,本來就是如何確認問題有被解決,若無法確認問題有無被解決,用什麼方法也是白搭,不知成效如何。從這個角度來看,在設計之初就考慮測試,並不是什麼新穎或強人所難的事。但要走到這步,如同學其它東西一般,需要累積不少經驗

總結來說,要學習一個技術最好的方法,就是在有適當的需求時再學。平時看到和自己需求無關的知識,加減有個印象在腦裡即可。待要用到時可利用腦裡的索引找出相關資訊,先有個廣泛認知。接著根據自己的需求可以明白那些是比較相關的知識,邊看邊動手做些雛型試試。有了這些操作經驗和背景知識後,再從頭確認一次自己真正的需求為何、有那些限制和對應解法,就能理解要如何應用這些技術和工具到自己的情況裡。

2012年5月6日 星期日

The Linux Programming Interface 讀書心得

書還沒出以前就聽 Scott 推薦過 TLPI,直到因為工作需要,才買回來看。斷斷續續大概看了三個月半,來寫一下心得。先說結論,若需要寫 Linux programming,這本是極佳的選擇,它是我買過最貴最厚的書,同時也是我買過最划算的書。

全書共 64 個章節近 1500 頁,在 2010 年底出版,算是相當新且完整的書。我看的方式是:每天沒事翻個四頁,有事時針對需要的重點看,不見得每個章節都要看完,有解決當下需求即可。持續這樣運作個一陣子,效果相當好。從上面的照片可看到我插的一堆書籤,發票、鐵尺、名片、計算紙等,不知不覺就放了 14 個,看了小有成就感。

本書最大的好處是,作者對每個主題循序漸進地提供鉅細靡遺的說明,不只是 POSIX 或 Linux 能做什麼,同時也會提供必要的基礎知識,像在 network programming 的章節,會補充說明 TCP/IP 的運作方式,而且相當容易吸收。ch 41、42 則是說明 static 和 shared libraries,也是相當實用的知識。之前曾在網路上看過一些片段知識和一些動手操作的經驗,但一直沒有拼出全貌。直到看了這兩章後,才有種打通的感覺,更有把握知道底層是怎麼運作的,從而解決工作上的疑問。

所以,只要有耐心啃完書中相關內容,就有個八成把握知道怎麼做才對。相較於在網路上搜尋,更能系統地了解 Linux 能與不能做什麼事,有利於判斷可行的方案。偶而我也會看 man page 的說明,交叉對照效果更好。作者說的是他整理消化後各系統通用的知識,而 man page 可進一步反映出自己使用 OS 版本的細節。

本書另一大好處是,每個章節都有附完整的程式碼,輔以執行結果,來說明系統的一些特性。像是「20.12 Signals Are Not Queued」,為了說明 signal 只有保證送出後至少會收到一次,但不是送幾次就收到幾次,作者寫了個小程式 A 送個上一百萬次 signal 給 B,B 則是在 signal handler 裡計數,結果在作者的這次實驗裡, B 只收到 52 次。我很喜歡這種實證的方式,現在的系統太複雜,在不同平台會有不同狀況。了解知識的概念後,手邊還是要有程式實測,感覺才會踏實。

除了內容完善以外,對書中有疑問時,寄信給作者也會得到很友善的回應。舉了這麼多優點,實在是讓人想不出不買它的理由,也難怪 Amazon 上 35 個評價全都是五顆星了

2012年2月19日 星期日

專注於滿足需求而非工具或方法

看到 command 提到《Don't Fall in Love With Your Technology》,而有一些感觸。

從高中開始,我一直想弄明白 Perl、Python 到底那一個比較好用,這樣我學其中一個就可以了。後來又多了 Ruby 這個選項,讓這問題變得更複雜。大概到大學後期或研究所的時候,我才肯定這是一種信仰上的爭辯,而將這個問題拋之於腦後。

同一時期,我也花了滿長一段時間才明白許多問題沒有標準答案得視情況而定。每當對此有所體會時,就會想起大學電子學老師整年重覆強調的一句話:「沒有前提,就沒有答案」。雖然兩學期的電子學都是低空飛過,這句話深深印在心裡,只是那時我對這句話的理解仍不深,還需時時重新琢磨它的含意。

我花了更長的時間才將前面兩個心得連結在一起,從而明白任何工具或方法的爭辯很可能都是偽命題,重點在於需求是什麼?要如何滿足需求?如今回想起來,《不要自称为程序员》將這個觀念解析得相當清楚,相當值得一看。

舉例來說,「vim vs. emacs」是個偽命題,這取決於自己當下的環境為何。若團隊內多數人使用 emacs 且自己兩者都不熟,那麼 emacs 是較為合理的選擇。反之,若自己相當熟 vim 而團隊內多數人兩者都不熟,那繼續使用 vim 較為合理。重點在於「如何有效率地在自己的環境下解決問題」,而非「一般而言,那一個編輯器比較強?」

再以軟體開發的方法來看,「agile vs. 某個軟體開發方法」也是偽命題,不論 agile 公認的定義為何,重點在於滿足需求,而滿足需求不見得需要一套完備的軟體開發方法;有完備的軟體開發方法不見得能滿足需求。要滿足需求有太多事要做,研讀相關技術、軟體開發、市場行銷等,軟體開發可能是滿足需求的其中一項基石,但不是全部。若滿足需求的前提需要改善軟體需求,自然需要改善它;反之則否。《Joel on Software - 別讓架構太空人嚇到你》對「開發軟體的方法 vs. 滿足需求」提了生動的描述。

舉另一個具體例子,「是否需要重構?」往往帶來許多爭議性的討論,各方人馬(PM、RD、QA、...) 對此有不同看法。若這段程式一直都不需要加新功能,那的確不需要重構。重構只會花費時間讓程式碼變漂亮,對於滿足需求沒有任何影響。反之,之後需要繼續大幅加功能,逐步重構部份功能,則對完成產品(滿足需求)大有幫助。

最近幾年有一個很紅的議題,開發網站是用 Ruby on Rails 好,還是用 ... 好。最近一年可能還會多一些人問是否要改用基於 node.js 的新 framework。要回答這問題得先看需求為何,若只是做幾頁的小網站,用什麼方法差異都不大;若是做長期維護的大網站,要看目前團隊成員熟悉的工具和程式語言為何,再來評估使用 Rails 的相對成本。若再涉及和後端整合,又和既有的 code base 有大幅關聯。而要回答這一切一切衍生的議題,還是得先看:究竟需求為何,基於什麼原因而採用 X 會更好?以 Justin.tv 為例,《Django: Why is Justin.tv porting their codebase to Django from RoR?》說明 Justin.tv 轉換的主因是全部程式都是用 Python 寫的,此外,他們也想藉機重新設計一遍架構,去除 legacy codes,以符合現今的使用需求。

舉這些例子的用意不是無限上綱地說工具和方法都不重要,而是強調將焦點放在如何滿足需求,若有需要選用好工具,才有必要討論它。問錯問題的話,永遠不會得到有用的答案。