顯示具有 Software Engineering 標籤的文章。 顯示所有文章
顯示具有 Software Engineering 標籤的文章。 顯示所有文章

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)。起初學不同語言只是覺得好玩,誤打誤撞得到這樣的體會,滿有意思的。

相關文章

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,以符合現今的使用需求。

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

2012年1月19日 星期四

在 Linux 下開發 C/C++ 的新手指南

2018-08 更新

依近幾年的經驗更新了另一份介紹,見這裡



新加入一個專案,最先面對的課題是如何正確地編譯和執行專案,可從 "It works on my machine" 如此地風行,印證這件事的困難性;再來則是閱讀負責工作相關的程式碼。至於發揮程式語言的特性,運用高階設計模式等,都是另開新專案或熟悉狀況後才有機會發揮。

過去數年沉浸在愉快的 scripting language 和開發新專案中,一直沒踏入這殘酷的世界。這篇記錄在這樣的情境下,可能需要的技能,結算一下這一個多月的心得,全都是血淚談啊 ...。

系統工具

熟悉作業系統的安裝套件是首要之務,這樣才知道如何補足需要的 header、library,或是安裝含 debug symbol 版的函式庫以執行 gdb 觀察程式或除錯。參見《自行編譯含 debug symbol 的套件 (package)》了解 Ubuntu/Debian 下的套件命名規則。

在未安裝套件的情況下,可用

  • aptitude search SUBSTRING # 找套件
  • aptitude show PACKAGE # 顯示套件用途
  • apt-file search X # 找出 X 包在那個套件裡,找 header 時很有用。

注意在用 apt-file 前要先跑 sudo apt-file update,不然搜不出東西來。

對於已安裝套件,可用

  • dpkg --search SUBSTRING # 找出安裝在那個套件,已知 header 時,適合用來找 library
  • dpkg -L PACKAGE # 列出套件內容,可用來找 header、library
  • locate SUBSTRING # 我比較常用它找 header 的位置,再觀看 header 內容

執行 locate 前記得先執行 sudo updatedb,原因同 apt-file。

《除錯技巧:在 Ubuntu 上找出第三方函式庫的程式碼》用一個小例子說明如何使用這些工具找出原始碼協助除錯。

其它參考資料: How To Manage Packages Using apt-get, apt-cache, apt-file and dpkg Commands ( With 13 Practical Examples )

編譯

連結

這一塊讓我卡了一陣子。一些粗淺心得:

執行

光只是讀程式碼就像大海撈針一樣,不太有效率。可從動態執行過程找出主要執行的路徑,再專注相關的程式碼。

1. strace 和 ltrace

srace 是分析執行行為的強大工具,google 一下會看到很多別人的個案心得,看看再自己試一試,很快能上手,不知能發揮它多少功能。這裡列自己用的兩個小案例:

反而是 ltrace 一直都想不到使用它的時機,也沒找到好的個案心得文。

2. gdb

gdb 的重要性不需多說明,之前的幾則心得:

強烈建議使用 cgdb,簡易安裝 + 無痛上手,瞬間省下大量操作和讀碼的時間。

3. 打開除錯功能

依照開發者的習性,一定會留後門讓自己方便除錯,從這角度下手也可省下不少時間:

4. 載入函式庫

除以上所言外,我另外有找過畫出程式流程的靜態和動態分析工具,像是畫 call graph 或是 C 的 cflow。不過 C++ 的靜態分析效果很糟,就沒花太多時間研究。目前用 strace 和 gdb 覺得已夠用了,不知用工具產生 call graph、class 相依圖或其它東西,是否會更有幫助。待有需求看整體的程式時再來試試。

閱讀程式碼

聽了大家的建議後,做了一些實際操作,而有些心得:

Eclipse CDT 雖然方便,後來我還是用 gj 居多。原因有幾點:

  • 我已很習慣用 vim + screen 做事,gj 最合這個情境
  • id-utils 真的是超級快
  • 我針對自己的需求更新 gj 多次,愈用愈順手

另外 ack 也滿方便的,懶得建 index 或是想比對子字串時,可直接使用。當然 id-utils 也支援子字串比對,只是暫時懶得為此修改 gj 的程式,目前大部份需求是找完整的 symbol。

熟悉 Linux 系統程式

在基本工具都上手後,打算每天抽一點時間加減讀一點相關知識。一兩年下來應該會有不錯的成果。目前打算讀《The Linux Programming Interface》,年假時試看看效果如何。

這一個月的心得以了解 /proc 為主,對觀察 CPU 用量、RAM 用量、載入那些函式庫、multi-thread、程式執行狀態等都很有幫助:

結論

即使大概知道有那些東西,還是需要實際動手的經驗,才會真的學進去。一個月下來進步了不少,不過對於要面對的戰役,還有一大段路要趕上,還有很多很多要學的。

2012-01-29 更新

補上一些後來新寫的連結。此外,《The Linux Programming Interface》 相當實用,讀 ch1 ~ 3 讓我補足不少基礎知識。ch41、42 講解 shared library 也相當值得一看。相關心得見《The Linux Programming Interface 讀書心得》

2013-07-13 更新

備忘效能分析相關的工具:

出處:Linux Performance Analysis and Tools

2013-07-20 更新

將後半部份內容抽出來,另寫了一篇比較完整的文章:《了解 C/C++ 程式行為的技巧》。

2011年2月19日 星期六

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

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

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

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

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

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

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

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

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

2010年11月4日 星期四

讓 if、else 帶有更明確的語意

最近在維護程式時對於 if、else 有更深的體會,一但邏輯分支變多,很難釐清各種控制流程,一些簡單的習慣可以大幅簡化除錯和改程式的負擔。

變數的初始值

一個常見的情境是有個變數會依條件而有不同的值,典型的寫法如下:

1
2
3
4
5
# 假設後面有一長串算式會乘上 weight,這裡先決定它的值
if double:
   weight = 2
else:
   weight = 1

( 備註,在 Python 裡 if / else 裡設的變數和它的外層是同一個 scope。 )

或是善用程式語言提供的三元運算子設值 (即 ? : ),在 Python 裡則是這麼寫:

1
weight = 2 if double else 1

若有多種情況,在其它語言裡可能會用 switch,我個人不喜歡 switch,覺得用起來不直覺,Python 裡也沒有 switch,但可以用 dict 代替:

1
2
3
4
weight = {
    'double': 2,
    'triple': 3,
}.get(condition, 1)

操作複雜時可在 dict 的 value 裡改用輔助的小函式,明確的用簡短的程式表明「這區塊在決定 weight 的值」。

別小看這一點小改變,當程式碼很多時,看到 “value = a if condition else b” 可以立即明白這裡的判斷式是用來設值,可以省下為 if、else 這區塊煩心的時間,也可以減少消耗精神和腦內暫存記憶。

提前處理簡單的分支

以用遞迴的方式實作費氏數列為例:

1
2
3
4
5
6
def fib(n)if n < 0:  # Error input.
        raise ValueError('n must be positive.')
    if n == 0 or n == 1return 1
    return fib(n - 1) + fib(n - 2)

上面的寫法先處理例外,接著就能放心處理正常的情況,再來處理特例 (初始值),最後就能專心和主邏輯奮戰,而覺得主邏輯變得單純許多,很好處理。

較大的程式,就是先寫幾個簡單輔助小函式 (例如 is_invalid()),先呼叫小函式避開特殊情況,一樣可以化繁為簡。

避免巢狀區塊和 continue、break

常見到在多層迴圈裡呼叫 if、else,並和 continue、break 混用,我個人覺得這種寫法很亂,而傾向用小函式 + return 避開使用 continue 或 break,比方像下面的程式要從一個兩層 list 裡找出每個 list 第一個負數,並算出負數的總和:

1
2
3
4
5
6
7
sum = 0
for numbers in a_list_of_numbers:
    for n in numbers:
        if n < 0break
    if n < 0sum += n

可以改用小函式配合 return 避免使用 break 並「隱藏」分支:

1
2
3
4
5
6
7
8
9
10
def find_first_negative(numbers)'''Return 0 if there is no negative number.'''
    for n in numbers:
        if n < 0return n
    return 0
 
sum = 0
for numbers in a_list_of_numbers:
    sum += find_first_negative(numbers)

改寫後,兩個 if 都不見了,主邏輯很清楚地表現出「找出各 list 第一個負數並加總」。

同樣的,程式愈複雜時,這些寫法省下的思考時間愈可觀。

明確的指明 else 的處理方式

這和前面提的東西有一點相衝突,視情況而定。在任何有 if、elif 的情況,即使 else 的情況不需做任何處理,仍要明確的寫出 else 並加上註解。如下所示:

1
2
3
4
5
6
if some_condition:
    ...
elif another_condition:
    ....
else:  # Do nothing.
    pass

這個作法的目的是,讓其他讀這份程式的人,明白原作者沒有漏考慮 else 的情況,不處理是符合預期的作法。函式愈長時,這樣寫的好處愈明顯。別小看這個小動作,程式碼一多,回頭讀程式碼時,這點小動作可以省下不少分心的機會。

結語

上面提的例子背後的目的都一樣,就是避免讀程式碼的人分心在分支裡,而能專注在主邏輯上。類似的例子還有「使用 iterator 少用 for + index」,平時留意一些小細節,不但能愈寫愈快 (省去煩心細節的時間),也能降低維護成本,讓其他人易於理解。舉手之勞做環保,大家一起來維護程式碼的品質吧!

2010年9月11日 星期六

每件事都有它的價值

過去我一直在尋找捷徑,希望能找到經驗以外的價值,不然只是比年資,感覺很無趣。老手比新手強,只是因為他做得久、比較熟悉公司內的程式。待新手變強後,也只是因為他經驗變多。似乎人與人之間沒什麼差異性。改善自己學習的方式是個好主意,但有智慧的人都會不斷改善自己學習的方式,以能夠更有效率地學習。我不斷地思考自己的定位,以及如何學得更有效率。期望在天份、經驗外找到其它的聖杯。

Steve Jobs 在對 Stanford 大學的畢業演講裡提到:

Again, you can’t connect the dots looking forward; you can only connect them looking backwards.

抱著半信半疑的心態,我只能先相信這個看法,繼續邊做邊想。

後來在杜書伍的《打造將才基因》裡看到:

我並末想出什麼令人拍案叫絕的答案來,但當時得到的結論卻一直到現在我都認為是正確的,...,經驗要累積得快,除了比別人更努力工作外,似乎找不到其他的方法。

看第一次時仍是半信半疑。然而,最近重讀這本書,對照近來的體悟,忽然間想通了這點。

在經歷 TDD 的洗禮後,我覺得我好像找到一個不錯的答案,至少它是我在軟體開發領域裡找到最好的答案。似乎只要練 TDD,就能在同樣時間內爆增功力。相較於過去看的 Design Pattern、程式語言實作技巧等,TDD 似乎更宏觀實用。

就在實踐 TDD 一年後,偶然看到別人的討論,而有不同想法。練習 TDD 後確實能寫出較好維護的軟體,不過除錯的經驗也變少了。寫了快一年半的程式下來,我一直沒有用 debugger 的需求,只開過兩三次 python debugger,碰一下就放棄它,改成補寫 unit test 偵錯。這才發覺,每段時間都有它的意義,只是練得目標不同。我明白照目前的方式走下去,再過個三年五年,我用 TDD 的功力會愈來愈深,不過用 debugger 功力仍然會是零。這沒好壞之分,端看目標為何,以及自己的定位。

在其它學習的經驗裡,發覺看過的知識,還是要實際操作過數次才能內化到自己的思考體系裡。操作次數愈頻繁,體悟愈深。像許多系統設計或軟體開發流程的最佳實踐 (best practice),還是要走過不同的路,回頭才會明白最佳實踐背後的考量,才知道如何融會這些點到自己的情境裡。很多東西光用看的,或光用做的,無法體會背後的精神,遇到狀況時仍然使不出來。所以,減少嘗試多讀最佳實踐,效果有限;老是硬幹不讀別人的體悟,也是效果有限。

剛才在聊看 Python 源始碼的事Scott Thinker 不約而同建議直接看原始碼,而不用看書 (即使 Scott 認為書本的大綱看來不錯)。但我覺得先看書有個概念再來看原始碼應該比較有「效率」。經 Thinker 說明,才發覺這是練得目標不同。直接硬啃 code 看來較費工,但在費工之中才會有更多練習讀碼的機會,會更熟練相關工具和技巧。而我的目的是增加系統設計經驗為主,讀原始碼為輔,自然會覺得先讀書較好。

每件事都有它的價值,每個技術都需要時間才能挖深。然而,要將走過的路合起來發揮最大功效,要將所學收斂在相關領域裡。這讓我不經花更多時間思考,往後自己的定位為何,我想發展什麼樣的知識網。總之,相信 Steve Jobs 和杜書伍的話,「Stay hungry. Stay foolish.」繼續邊做邊想吧。

2010年7月18日 星期日

學習的自我要求

學生時代我一直在想一個問題,究竟是問題導向的學習方式較佳,還是技術導向較佳?舉例來說,我想寫個留言版,我發現要完成最基本的需求,我得學 HTML、PHP 和 MySQL,於是我針對我要做的功能學相關的語法,最後完成留言版。可能資料庫設計沒用到正規化,網站外觀沒用到 CSS,一些動態功能用 PHP 實現而非 JavaScript。相較於從頭學寫網站所需的各項技術,這個作法比較務實,我用較少的時間完成目標。極端地看,用問題導向的解法時,我可能找到相關的程式碼,用它們拼貼出結果,但不確定各塊在做什麼。技術導向的解法,可能要先花一段時間弄清楚寫網站需要的技術,再各別學個基本能力,接著開始實作。當然,世界上不是只有 0 和 1,我們常混著用問題導向和技術導向的方式解問題。這裡我說用問題導向的意思,是指偏向用問題導向。

不只做產品,做研究時也是類似的情況。問題導向就是從問題開始往下挖,看前人做那些方法,將自己不熟的部份補起來,有了必要的知識後,就能回頭解決問題。而技術導向的做法,我可能會加強基礎學科知識,視類別而定可能是機率和線代,或是計算機組織和編譯器,接著才開始思考如何解問題(或重新定義問題)。

以前我沒什麼時間觀念,通常是技術導向的作法,覺得有趣就看我能從多底層開始往上學。比方說使用 Java Collections Framework 時,我會弄清楚我要用的資料結構怎麼運作的的,時間複雜度是多少。實際上大部份的使用情境不需要這些知識,稍微讀一下文件的注意事項即可。要用 machine learning 的工具,就找適合的 machine learning 課程講義來讀,弄清楚要用到的 model 怎麼運作。直到現在,我無法確認這些對於使用工具究竟幫了多少忙,當初花的時間對應到目前的幫助,真的划算嗎?

最近我試著用問題導向的方式解問題,初期進展確實比較快,不過後來遇到一些狀況,才發覺技術導向的好處。有些情況,我們根本不明白問題出在那裡,有太多可能。舉例來說,前幾天我和同事發覺不同使用者登入同一頁面,操作速度卻差了一大截。有許多可能原因:網路連線 、網頁框架、資料庫等。做了一些初步測試,我懷疑是 MySQL 根據歷史記錄做錯 query optimization。於是用 EXPLAIN  看相同的 SQL 配合不同使用者 ID,結果發現 MySQL 執行 query 的方式有細微差異,造成取出某些使用者的資料時,用較慢方式執行 SQL。於是讓 MySQL 重分析表內的資料後,問題就解決了。若不是之前稍微看過 MySQL 執行 query 的相關知識,不會這麼快就直指問題核心。也許就會用別的方式繞開這問題,一輩子都不知道怎麼解它。待發生類似問題時,又用別的方式繞開它,長遠來看浪費開發時間又增加維護成本。

另一個反面的例子是,我一直沒用 lock 的習慣,教科書告訴我們 deadlock 很可怕,所以我會想辦法避開用 lock。結果最近有個小專案因為沒用 lock,真的發生 race condition 造成有一點點資料不正確。實作前我明白會有這樣的狀況,但這個問題對我們的目的沒什麼影響,衡量開發時間後我決定寫下註解強調會有 race condition,而選擇不處理它。對照最近的體悟,我明白這樣下去我不可能學會用 lock,這不是個好現象,所以又找時間回頭看 MySQL 怎麼 lock table,結果比想像中來得簡單,之前多慮了。

有很多類似這種的逃避例子,像多人一起寫程式容易有問題,於是大家傾向將功能切乾淨,每人寫沒有交集的功能,最後再來整合。但是,對照近年來的軟體開發的趨勢,愈早整合愈容易解決問題。一個人開發容易有盲點,互相協助可以降低初期錯誤,以利後期整合。問題是,要能順利地多人共同開發,得做對不少事才行。像是版本管理系統、coding style、天天合併程式等。每一項都需要時間練習。若一個人開發時有好好練習,和別人合作時會減少許多問題,比較容易推動密集的團隊合作。

在面試別人時,我發覺一個問題:有些人學到的技能剛好只能應付他負責的專案。問題在於,若平時我們都處理簡單的專案,要怎麼轉去負責困難的專案?兩者之間有個斷層。像這類的例子不勝杖舉。比方說要從資料庫表 A 取出部份資料塞入表 B,最「簡單」的作法是寫個程式用 SQL 取出資料,用程式做些處理再用 SQL 一筆筆寫入表 B。另一個作法是直接用一個較複雜的 SQL 直接搞定。當資料量大時,後者執行速度會快上不少。並且,學會後者的寫法後,之後只要花一點時間就能處理類似的情況,不用再寫一個小程式。其它像寫程式的習慣、用程式工具的習慣,都是一樣的。多數情況我們可以用最「不費力」的作法滿足需求,但長遠來看卻是毫無長進。實際上有更有效率的作法,這裡的效率包含開發時間和軟體品質。

對照大學的程式來看,這一年來我以為自己程式已寫得頗有品質了,雖然知道一些小問題,但覺得並不迫切,也不知怎麼查相關的解法,就放著它們。最近翻了一下 Effective Java 第二版Growing Object-Oriented Software Guided by Tests,才發覺還有太多的東西要學,自己和資深的軟體工程師差了一大截,照我目前的學習方式,和他們的差距只會拉大不會縮短。若只專注完成眼前的工作,我永遠無法補足和更難工作之間的差距,這才驚覺問題導向的盲點。

走過天平的兩端後,我現在的體悟是,得雙向夾擊來解決問題。一方面用問題導向解問題以符合時程,確保時間有花在刀口上。另一方面再抽時間用技術導向的方式強化自己的實力。如此一來,在完成當下的專案的同時,也有一點一滴地補足技術斷層,取得挑戰更難專案的機會。題外話,英文也是很重要的「技術」,這一年來我半強迫地讓自己盡量搜英文文件,思考關鍵字比以前敏銳不少,閱讀速度也變快,獲得答案的速度比以前快、品質也較佳。

2010年5月29日 星期六

養成寫程式的好習慣

我很喜歡 Kent Beck 說的這段話
“I’m not a great programmer, I’m a pretty good programmer with great habits.”
從學生時代開始,我就習慣照著書上的建議寫程式,一直沒覺得什麼特別的。後來和一些人合作,或是看到別人抱怨那裡又出錯了,才驚覺那些好習慣有這麼大的影響力。試著在一些場合向別人說明如何改進寫程式的方式,才發覺很難改變寫程式的習慣 — 大概就和我一直無法早睡早起一樣難...。

大前研一說許多人在解決問題時誤把結果當原因,沒有深入追究問題的根本,只是解決問題根源造成的表面問題。寫程式自然會有 bug,但是有許多 bug 並不是 bug,而是壞習慣造成的。像是在 PHP 或 JavaScript 裡不用 === 和 !==,而被 type casting 誤導造成 bug。或是在 if / while 裡用 assignment 又不小心漏了括號,寫出 if (a=foo() > 0) 這類 bug。
好習慣得隨時間慢慢累積,做得愈多愈久,功力自然會變深。就如同龜仙人要悟空和克林送牛奶那般練基礎功,帶著好習慣寫程式,經年累月下來,學到的東西會更多。以下針對一些特定的習慣提我自己的感受。

Coding style

網路上有不少 coding style 建議,有些如 Google coding style 甚至會解釋為何要這麼寫,這樣寫的好處和壞處為何。可以學到不少寫程式的小技巧。
最近遇到的實例是遵守 Python coding style 所說,在 module 開頭寫 import,並照順序 import 內建、第三方模組和自己的模組。在程式寫到近兩萬行後要做些修改時,我發覺這個簡單的習慣,讓我很容易明白那些模組有關聯,很快就能找出要修改的程式。反過來說,閱讀一些 Django 程式碼時,發覺常在函式裡 import module,不易掌握模組之間的關係。
還有控制函式的行數在螢幕的高度內、區域變數的使用範圍(要用到時再「宣告」)、避免用全域變數。遵守這些習慣使我容易掌握變數的影響範圍,除錯時可以省下不少心思思考變數是否被別的地方改到。
最近初學 JavaScript,就在 coding style 的建議裡找到減少 global object 和管理變數 scope 的技巧:使用一個 global object 存放所有變數和函式,藉此在 JavaScript 中做出模組的效果。

Version control (以 Mercurial 的指令為例)

即使一個人寫程式,version control 仍有很大的用處。保持每個 commit 精簡,每個 commit 只完成一個小功能,就能輕易追踪過去的改變。
以下是幾個我常用 VCS 協助的情況:
  • 寫程式較不怕被中斷,只要 hg diff 就知道剛才改了什麼。commit 前也能清楚明白這次做了那些修改,去掉忘了除掉的 debug code。
  • 可以放心地修改,改到昏頭就 hg up -C 清掉剛才不知所云的修改,不用花費力氣將程式弄回正常的版本。
  • 寫到一半發覺要先完成另一個功能,hg shelve 暫存目前的修改,接著將另一個功能做完並 commit,再 hg unshelve 回頭做原本的事,可以輕鬆地切換目標,隨時專注在目前的目標上。
  • 若發覺某個功能忽然不能運作,hg up 切回舊的版本,做個 binary search (或用 hg bisect) 立即找到改出問題的 commit。由於每個 commit 都很精簡,看一下就會找到改爛的原因。
我最近用 jQuery 寫的程式,過了一陣子後發覺某個功能不能運作。用 hg up 和 binary search 的方式,很快地找到在一百多版前改爛的,而且改爛的原因很奇妙,我將目標 tag 的 id 設為 “submit” 後就爛了,但若換個名字或不設 id 就沒事。若沒有 version control,我想我在原本的程式裡找半天也不會找到,根本不會懷疑問題出在這裡。最後大概會重寫該段,然後莫明奇妙地避開這個問題。
和人合作時 version control 就更有用了,方便和其他人共用程式、做 code review、自動跑測試確保各版運作正常,好處不勝杖舉。相較於每個人各自寫程式,多個人同寫一份程式不但方便討論,容易互相支援,開發時士氣也會較好。每次看到別人 push code,就會覺得待做事項漸漸變少,而寫得更有勁。

Refactoring

在寫新功能或修 bug 前,若發現有重覆程式碼或一些有潛在風險的程式,先重構程式再回頭做原本該做的事。重構前記得要確保重構後行為不變。若情況太糟很難補 unit test 或是沒時間補太細,準備好幾組常用的輸入資料,記好它們對應的輸出結果,寫個自動測試的 recorded test 會比較安全,也可加快後續的重構。配合 VCS 做起來更容易,改改發現無法通過 recorded test,就 hg up -C 重頭改一次。然後別太貪心,一次改一點比較不容易犯錯。視情況寫 recorded test 的方式有所不同。通常我會用 shell script + diff 這類指令很快的拼一個可以用的小工具,重構完就丟了,節省準備 recorded test 的時間還有免除日後維護它的負擔。但若打算長久維護的話,自然是照規矩一步步做會比較穩當。

Unit test / Acceptance test / TDD

這部份在先前的文章已提了不少,隨著時間演進,實例愈來愈多。最近將 Django 1.1 昇到 1.2,跑 unit test OK, 但跑 Selenium test 時看到 CSRF 的錯誤訊息。稍微修一下,測試程式全過。讓我立即確信所有用到的 plugin 都順利地在 1.2 版下運作。
開始用 TDD 可說是我寫程式生涯中的重大里程碑,踏入完全不同的格局。讓我明白如何寫出易於長期發展的程式,不用像在玩踩地雷般辛苦。

Pair programming

這不算是個人習慣,順便記在這裡。
這部份我沒太多經驗,有時運作的不錯,有時不太順。執行 pair programming 前要先確保兩人的背景知識差不多,才不會有一人跟不上進度,讓另一人空轉。運作順利時,可以很快地完成較複雜的設計,並確保至少有兩個人可以繼續維護這份程式。而且程式也會較易懂:兩個人覺得好懂的程式,遠比一個人覺得好懂的程式易懂多了。
Pair programming 比寫下規範更容易讓大家有一致的開發習慣,像是 coding style 或是 commit 的規範。藉由一人帶一人的方式連結開發習慣。也方便分享實作技巧,像操作工具的技巧、使用函式庫的經驗或是寫程式的技巧。

其它

除養成好習慣外,偶而抽點時間學習工具的操作,像是 Linux 架站裝軟體之類的,開發軟體時很難避免這些事。像我習慣用 Linux terminal 開發程式,多熟悉 screen、bash、Vim 的設定和操作,開發速度可以快上不少。
最近的例子是使用 Firefox 的 plugin Firebug。以前改 CSS 都笨笨地存檔、重讀網頁,用 Firebug 讓我用十倍以上的速度完工,令人不勝噓唏。

結語

剩下的就是熟悉函式庫、框架,還有學會資訊工程一些基本知識,了解程式背後運作的原理。一但每個人都能保持寫程式的好習慣,團隊合作將會簡單許多,大家方便共用程式,方便互相支援 (寫相依的元件或除錯),既能加快開發速度,也會比較有趣。

2009年6月13日 星期六

為什麼要寫 unit test?為什麼要先寫測試?

這篇說明寫 unit test的好處,以及為何先寫 unit test 再實作,比實作完再補 unit test 更好。這裡先列出摘要:

  • 測試碼可以簡省手動測試的時間,但有錯時無法告訴我們錯誤的源頭在那。
  • unit test 可以告訴我們錯誤的源頭在那,可是 unit test 有時間成本和維護成本。寫過多 unit test 反而有害。
  • TDD 藉由先寫測試避開 unit test 的成本問題,並帶來其它好處。

為什麼要寫測試碼?

吃燒餅那有不掉芝麻,寫程式自然要驗證程式是否正確。想像平時我們如何驗證程式?大概就是寫一寫,執行看看,看看輸出符不符合預期,換些不同輸入,再看看結果,有錯再改,改完再重跑...。測試碼可以協助我們自動化驗證,省時省力。

也許有人會懷疑寫測試碼不划算,功能常改變,到時測試碼又要重改。這個問題視情況有不同種解法,比方用較「便宜」的方式寫測試,像是準備好輸出入檔,用 shell script 執行,配合 diff 看輸出是否一致。或是用 scripting language (如 Python) 寫簡單測試。而更根本的解法,個人認為是配合 TDD,待後面說明。

手動執行測試,執行個三十年,每次花的時間還是一樣,也許還會因為手酸變慢。但寫測試碼則相反,我們會愈寫愈快,愈寫功能愈好。

為什麼要寫 unit test (單元測試)?

首先介紹不同級別的測試碼,由小到大依序為:

  • unit test:測試對象為單一函式。
  • integration test:測試對象為數個單元函式的複合體。我沒特別區分這個和 unit test [*1],以下兩者一起用 unit test 表示。
  • system test:測試整個系統。
  • acceptance test:客戶驗收用的測試。將使用情節或使用需求轉成 acceptance test,藉以確認產品滿足客戶要求,方便起見,以下以 system test 統稱。

備註 *1:在談論 mock 時,unit test 和 integration test 的差異較明顯,本文不談這個議題,總得讓大家有意願寫測試,再來談如何寫測試才有意義,你說對吧。

以測試的觀點來看,system test 的 test case 夠多,system test 應該能涵蓋到大範圍的產品碼,如此一來也達成我們一開始提的「驗證」目的。從實務觀點來看,寫愈多測試碼,負擔愈多,規格變動時要改的東西也多。更何況 unit test 著眼點更細,變更實作方式也有可能得改 unit test。如此一來,為什麼要寫 unit test

首先,unit test 著眼點比 system test 小,也意味著當測試結果不對時,unit test 可以指出更明確的問題點,而 system test 只能粗略地和我們說輸出不對,接著我們得比對正確和錯誤的輸出,開始推測 bug 在那,並用 debugger 設中斷,或在程式內輸出訊息一步步找出問題源頭。相信大家都能明白除錯的痛苦。而 unit test 可以協助我們釐清那些程式是對的,那些是錯的,將問題範圍縮小。若 unit test 寫得好,幾乎用不到 debugger 和輸出訊息,光看那個 unit test 錯誤,就知道 bug 在那。

除了幫自己省時間外,unit test 也可幫助別人維護和理解程式。維護的人不如自己熟悉,難免不小心改爛程式,unit test 可以減少這類問題,也讓原作者安心給其他人修改。新手要閱讀程式時,unit test 可看成是各函式的使用範例,相信大家都同意讀例子比讀全部程式碼來得容易吧。好的範例有時比註解還有用。

為什麼要先寫測試?

好吧,若讀者大人耐著性子看到這裡,想必已對測試碼已有些心動,也認為寫 unit test 有那麼幾分道理,那先寫測試和後寫測試有什麼差別?若後來寫的程式不管用,需求變更,先寫測試不是搬磚砸自己的腳,自找麻煩嗎?事實上正好相反。

我們得先問自己,為什麼程式會不管用?為什麼需求會變更?去除客戶或主管找麻煩的因素外,一部份原因為思慮不週,寫的功能不夠貼近目標。如同產品一般,沒試用過我們不會明白產品的缺點在那,函式也是一樣。先寫測試碼就是先想像要如何使用即將要寫的函式,在寫測試碼的同時,我們同時也在設計函式。有時會發現難以測試,而想出更簡潔的介面。當我們能輕鬆寫出測試碼時,也意味著目標函式易於使用,之後才方便重用。

再者,先寫測試,相當於先列需求,規範中的輸入為何?預期輸出為何?可能有什麼例外情況?於是,我們接著寫出的程式,一定是有用的。我們不會忽然想說「要不要加個 foo(),之後大概會用到吧。」先寫測試,確保之後寫出的每一行產品碼都是有意義的。同時,我們寫的測試碼也是有意義的,若事後再補測試,不明白加入一個 unit test 有何幫助,有時會多寫不必要的 unit test,如同前面提過,測試碼不是萬靈丹,它也同時是負擔,需求變更時,有時也得一併改測試碼。再者,先寫測試表示有先考慮程式的可測性 (testability),使得程式容易測試,通常會將程式拆得很細,待寫產品碼時容易將寫好的小函式組成目標功能;完成產品碼後再來補測試則困難許多,由於寫產品碼時沒有考慮到寫測試的需求,結果是影響測試碼的品質,甚至變成過於難寫而不補上測試。

除幫助設計、減少寫冗碼的機會外,先寫測試還方便我們之後進行重構。沒有測試碼的重構是很危險的,而沒重構的程式碼也是很危險的 [*2],就像在底層不穩的地基上不斷加蓋偷工減料的高樓一般。先寫測試的好處還有一點,早寫早享受,先寫測試,就能先自動化驗證,省去所有手動驗證。

備註 *2:為什麼要重構?這是一個值得獨立討論的議題,主因為減少隱藏的 bug 和強化程式碼的可讀性,並更容易重用,以及增加新功能。詳見 Martin Fowler 寫的 《重構:改善既有程式的設計》

綜合以上所言,先寫測試花費的時間,可以攤算在設計時間,以及減少寫無用程式、減少手動驗證、縮短除錯的時間,所以先寫測試不見得會增加額外時間,我們只是把投入不划算事上的時間,先拿來寫測試。

特別加贈:何時該寫新的測試碼?

雖然寫測試好處多多,也別卯起勁來狂寫,最後卻覺得投資沒回本,浪費過多時間寫測試。遵照 TDD 的原則,先依規格寫測試,別浪費時間寫也許有用的測試。

那麼,何時才是跳出 TDD 迴圈,另外加寫測試的時機呢?依我個人經驗,我發現以下三個情況,都是寫新測試碼的好時機:

  • 在程式裡輸出訊息找錯誤:與其花時間寫 print、看輸出訊息、再回頭砍掉 print,不如寫個 unit test,之後能持續受惠。
  • 執行 debugger:同上,你還是得花時間設中斷,一步步看訊息,不如寫個 unit test,之後能持續受惠。
  • 不知錯誤在那,該如何進行下一步:這表示一次貪心實作太多功能,把目標縮小,一步步補 unit test 吧。

另外,在解 bug 時,先寫個 unit test 重製問題,再開始除錯。如此一來可確保自己明白成因,之後同樣的 bug 也不會再出現。

超級加贈:若 TDD 這麼好用,為啥它不遍及?

施主,這個問題得問你自己啊,快來加入推廣 TDD 的行列吧!

說正經的,雖然上面將寫 unit testTDD 說得如此美好,寫 unit test 不是簡單的事。若寫得不好,就會有「改變實作,也得改變測試」的副作用,或是測試執行過久,不方便反覆執行,或是寫出無用的測試。如同學習 design pattern 一般,unit test 也有 design pattern,對沒接觸過寫測試碼的人來說,相當於要花「兩倍」時間學習。加上前面提到大家可能有的誤解,使得 TDD 難以落實。

另一個可能是,若你的組織同時有兩批人馬對同一產品進行開發和除錯,程式碼分成兩個 branch:一個加入新功能,一個除錯。重構會造成程式碼難以合併,決策者因而選擇不頻繁重構。並且,這類組織可能有龐大的人力另外進行測試 (可看成是 system / acceptance test),而誤以為不需要 unit test。

然而,長遠來看,第一個理由顯然是偷懶,只是把現在的問題延後到日後再解決。趁早把除錯的時間省下來,投資到 TDD 這積優股吧。第二個情況可能是公司考量,專業分工使得公司容易找人和培訓,我沒在這類公司工作的經驗,不知能如何對應。

詳細的 TDD 風險,可參照 What is the downside to Test Driven Development?。結論是團隊要有熱情學一堆新技術,或是有個經驗老道的開發者帶才適合用 TDD。不過,我個人的觀點是,現實世界的選擇不是非一即零,不是所有程式都能 TDD,若成本過高用 TDD 不划算,那就暫時別花時間學相關技巧,挑軟柿子吃。至少,有用 TDD 的部份就能享受到 TDD 帶來的好處。別因為一些可能想見的問題,而全盤否定使用 TDD

我對 TDD 充滿熱血,請問如何入門?

好吧,這個標題是我寫來自 high 的。若有興趣的話,可以參考 The Bowling Game Kata投影片,由設計保齡球計分程式的小例子,一步步展示 TDD 進行的過程,相信跟著例子走一次必能有所啟發,親身體驗 TDD 如何帶來新設計。若能自己先寫一遍,再來看投影片,感受會更深。或是參閱我之前寫的介紹文:《TDD 推廣:背景知識和簡介》以及《最近用 Python + TDD 心得(與 Java 做對照)》

2009年3月15日 星期日

軟體開發技巧的待讀書單

經過這陣子的評估,終於列出了夢幻般 (?) 的讀書清單。我的做法是先在網上看到有人推薦,到書局翻一陣子,再回來查 Amazon 的評論,最後決定是否有必要看。附帶一提,待這書單決定後又查了一下,結果發現每本書都有得到 Jolt Award,一瞬間好像以為 Jolt Award 不值錢了 :-)。

以下依暫定的閱讀順序依序說明,除前兩本外,後三本都有中文版:

1. Test Driven Development by Examples

Amazon 評論 4 顆星,由 Kent Beck 所著。全書只有 200+ 頁,用大量的例子,一步一步說明怎麼進行 TDD,相當易讀。這裡有別人寫的詳細書評

2. xUnit Test Patterns: Refactoring Test Code

Amazon 評論 4.5 顆星,不過只有七筆評論,樣本略嫌不足。 在書店試翻的感想是:好書但不易讀,而且實在是太厚了。ThoughtWorks 專出這種書嗎?

3. Refactoring to Patterns

Amazon 評論 4 顆星,但評論兩極化落在 3 和 5,最中肯的評論為: "Good ideas, but needs refactoring",值得一讀,但不好消化。讀過 Martin Fowler 的 Refactoring 並有一段實戰經驗後的最佳書藉。本書中文版《重構-向範式前進》,且是侯捷合譯的 。

4. Head First - Head First Object-Oriented Analysis and Design

Amazon 評論 4 顆星,評論裡指出本書適合初學者,另外最多人同意的評論 (Decent Introduction to OOA&D) 指出:本書不夠簡潔,並有不少小錯,若有第二版才值得推薦。 

5. Head First - Design Pattern

Amazon 評論 4.5 顆星,且是壓倒性的一堆 5 顆星。Head First 的書以易讀出名,但通常也寫得很厚,需要花不少時間消化 。 對照過於精簡典雅的 Design Pattern Bible Book,這本書親切不少。附帶一提,在書店還有看到 Head First - Algebra,真是太有趣了!可惜是教國中代數,不是教線性代數。

依目前心得來說,有四件事要學:

  1. 寫出良好的測試程式 - book 2
  2. 寫出良好的 OOP- book 4, 5
  3. 提昇重構技巧 - book 3
  4. TDD (其實就是上述三者的綜合體) - book 1

前往軟體開發聖殿之路是很遠的,希望一個月至少能解決一本,並持續地應用到實戰中,半年後就出師啦!

備註

這裡補充我曾看過且大力推薦的書藉:

2009年3月10日 星期二

TDD 推廣:結語和參考資料

前篇,這篇說明我和組員近兩週開發過程中的心得,並附上幾篇不錯的參考資料。

結語

目前我和組員只有做到半套的 TDD,有時有照三步驟的順序進行,有時是先寫程式碼,再補測試碼,或兩者交替寫。目前組員有感受到測試程式的好處,例如對程式有信心、避免因修改而破壞既有功能。組員也漸漸感受到重構的必要性。但對於先寫測試碼這點,仍有疑慮而難以進行。至少,我們盡量遵守 TDD 的結果,在寫另一個 class 時,會先補好測試碼,保持程式碼的品質。附帶一提,配合 IDE 的功能,先寫測試碼可以輕鬆地建出產品用程式的框架。像 Eclipse 可以自動產生不存在的 class、method、field,所以我常先寫測試,再用 Eclipse 建出這些東西的殼,之後再填入內容,寫起來相當快。

開發過程也不盡是順利。藉由頻繁的 code review 組員的程式碼,還有回顧自己的程式碼,我發現許多問題。像是單一測試函式太大、測試碼和程式關聯性過高、測試時間過長,若沒有第一時間更正,後果不堪設想,也會使得組員對 TDD 有不當的負面認知。這方面的相關知識可以搜尋 TDD antipattern,或 JUnit antipattern

此外,由於組員少了前備知識,進行開發過程裡會看到許多 code smell,常常在重構一段程式前,必須先重構另一段程式,而重構另一段程式前,又得先補對應的測試程式或重構測試程式。這才體會到良好品質的程式碼得來不易,需要全組成員一同堅持和維護,大家的相關知識也需要一同逐步提昇,才能達到良好的軟體開發流程。雖然初期摸索很花時間,相信日後會有相當高的回報。

說了這麼多,其實仍抵不過自己親身嘗試,強力建議親身找個小專案,用 TDD 的三個步驟寫個千行程式,相信程式設計思維會有很大的轉換。

參考資料

我到 slideshare 找了一陣子,看到三份不錯的教學,但各有些不足,沒有一份完全符合我預期的入門文件。若有時間的話,自己再來作份投影片。

相關閱讀

TDD 推廣:相關工具

前篇,這篇說明我近兩週開發過程中選擇工具的心得。

開發環境

我採用 Eclipse,配合 Eclipse 的 refactoring tool,大幅加快重構的速度。人都有惰性,好工具讓我們更願意重構,而且降低重構過程除錯的風險。對照之前我自己寫 C++ 和 Python 的經驗,現在我更常做 rename variable/method、change method signature 之類的操作。

Unit Test

經過評估後,我決定用 JUnix 4.x。JUnit 4.x 改用 annotation 的方式表示 test method,可以去除必須繼承 TestCase 的限制。如此一來 test method 命名彈性更大,而且也可以直接在 class 裡加 test method 測 private method,解決因 private 不方便測試的困擾。不過我不確定這種作法是否正確,仍需多點經驗判斷。其它 JUnix 4.x 的好處可以參照《An early look at JUnit 4》

關於 unit test 的技巧, ThoughtWorks 有出相關的書:《xUnit Test Patterns: Refactoring Test Cod》。有機會再去書店翻看看。

Mock lib

關於 mock 的知識可以參見 Martin Fowler 的《Mocks Aren’t Stubs》,解釋得鞭闢入理,除介紹 mock 外,更進一步指出用與不用 mock,其實測試思維是截然不同的(確認物件行為或確認物件狀態),有機會的話再對此另寫心得。文中的範例程式有點過時(畢竟是舊文章),現在 mock lib 的用法更方便一些。Java 的 mock lib 有許多選擇,最後我選用 EasyMockZ。除了好用外,主要是因為 Python 的 Mox 是從 EasyMock 來的,這樣寫 Python 時可以花較少心力學 Mox。

2012-08-05 更新: 我現在改用Mockito了,更為簡單易用 。

Database unit testing

若只是用到簡單的 database 操作,可以用 EasyMock 處理,。測試前準備好 mock connection、mock statment、mock resutSet 即可。一方面是初學 mock 的緣故,另一方面也是 database 相關操作太瑣碎,我花了一整個下午才用 EasyMock 完成測試程式並寫好對應的功能。事後修改挺複雜的,比方說我程式裡補了 close(),mock statement/connection 也要記得加。感覺不是理想的作法,這方面經驗尚淺,需要多點實戰。

若要測得更深更全面一些,測試對 DBMS 本身的操作( insert / delete / update)的話,用 DBUnit 會方便許多。DBUnit 的基本精神是幫測試程式準備好已知狀態的資料庫,以及比對兩個資料庫是否相等。它的核心功能是將資料庫內容和 XML 檔案互轉。所以,先建好一個資料庫,透過 DBUnit 將整個資料庫存成一個 XML 檔,日後隨時都能用該 XML 檔還原回原樣。

對測試程式而言,執行速度是非常重要的,執行速度快才能反覆地執行多次,才方便反覆開發、重構以及整合測試。所以我改用 SQLite 做為測試用的資料庫,真正的產品再用 MySQL 之類的 DBMS。SQLite 的好處是支援 SQL92 的語法,整個資料庫只要一個檔案即可。如此一來只需要讀寫本機檔案,省去網路連線的負擔,也方便建置測試環境。

由於使用 DBUnit 前得先準備好資料庫才行,我得先從既有的 MySQL database 建出同樣 schema 的 SQLite database,才能用 DBUnit 做後續的操作。原本打算寫個程式讀出 MySQL database schema 再建出 SQLite database schema。但兩者建 table 語法差太多,無法直接套用。最後我是利用 DBUnit 產生的 XML 檔,寫程式從 XML 檔中讀出 table 和 column 名稱,再建出簡單的 SQL 指令來建出 SQLite database。如此一來,每次自動測試前我先用 DBUnit 將 SQLite database 填入備好的資料,就能快速地重覆在同樣的狀態下測試資料庫操作的程式碼。待有更多相關經驗後,再針對此議題寫篇心得。

相關閱讀

TDD 推廣:影響和個案心得

前篇,這篇談 TDD 帶來的改變,以及我自己的個案心得。

TDD 的影響

只要用 TDD 重寫以前寫過的小專案,就會發現最後的產出,和原本寫的程式不同。舉例來說,會發現少了很多 setter function,因為大多情況只要用 constructor 放值即可。更進一步,會發現少寫了許多功能,而這些功能其實是原本不需用到的。

TDD 同時也幫助設計出易用的介面,透過寫測試程式的過程,可以釐清程式的使用方式。當發現程式不好測試時,通常意味著程式設計不良。測試程式也可以當作文件使用,提供使用程式的範例程式碼。

除此之外,在 TDD 的過程中,開發的速度很順暢,很少會落入長時間的除錯裡,因為程式被強迫切成許多小單位,並且每一單位都有被測試。一但出錯馬上會發現,而且知道錯在那裡。不像以往得用 debugger 追個老半天,最後發現 bug 在很遠的一小段程式碼裡,有時還是簡單的打錯字。在除錯的過程,藉由自動化測試的幫助,修改和執行可以快速地反覆交替進行,減少除錯以外的精神損耗(如手動準備執行需要的資料、設置和操作 debugger )。即使寫測試程式花的時間和除錯一樣多(我相信會是較少),至少以 TDD 的方式進行,心情會比較愉快,不會卡在一個地方太久。

TDD 的個案心得

以我最近的實作案例來說,我一開始知道會用到數個算數函數,而且這些函數日後需要抽換為不同的算法,方便我比較何種組合效果最好。舉例來說,我需要實作排序演算法 Sort(),由於資料分佈特性的改變,我可能需要實作各種演算法如 quick sort、heap sort、insertion sort、radix sort 等,並讓這些演算法的輸入輸出介面一致,方便視情況抽換不同的演算法。在這個案例裡,我需要寫多個這類型的演算法,把它們整合成一個較大的程式。

若照我原本的習慣,大概就全部套 strategy pattern,於是我可能會先寫數個 interface 或是寫個 abstract class 再套多型。至少會花半天完成這些工作,並且增加一堆「未來可能會用到」的 class。但這回我忍住了,我遵從 TDD 的三個步驟,我先統統用 static method 寫,因為這是最簡單的實作。結果寫個兩天後,我發現這幾個算數函數,其實只有一個真的需要套 strategy pattern,其它用 static method 就夠了。也許那一個最複雜的函數也不需用到 strategy pattern,總之,由於目前仍無需求,我全部都保留原樣。於是我將時間花在刀口上,優先完成必要的事。並且,我的 class space 沒有被一堆「也許有用」的 interface 和 class 汙染,避免 over-design,提高整體的可讀性。

相信大家看到這都會大喊,全部都等用到才寫,日後難道不會付出更大的代價做變動?改程式碼的代價很高,所以應該多費些心力設計好才對吧?但別忘了,「改程式碼的代價很高」是一般的情況,在有充沛的測試程式做為後盾的情況下,改程式的代價其實沒那麼高。況且,程式碼隨時都有重構,程式應該是保持在容易修改的情況,改程式的代價又更低了。

至於初期的設計要精確到什麼程度才開始寫程式,仍需經驗拿捏。我目前的作法是先有個大概的整體設計,再開始用 TDD 的方式實作各個 class,並完成細部的設計。

相關閱讀

2009年3月9日 星期一

TDD 推廣:背景知識和簡介

之前我曾獨自一人用 TDD (Test Driven Development,中譯為測試驅動開發) 的方式,分別用 Python 和 C++ 各寫了一千多行的小程式,感覺滿好的。最近剛好有機會寫一個新專案,就趁這機會開始第三次的 TDD 練習。和前兩次不同的是,這次要和一位組員合作,以 Java 開發。看來正是測試 TDD 威能的最好時機。

由於我的組員缺乏寫測試程式的經驗,剛好可以用來評估是否能在組內推廣 TDD。雖然我之前有過兩次 TDD 開發經驗,但也只是自己邊看文件邊摸索,經驗仍嫌不足。這次進行 TDD 讓我多花了不少心力查相關資料。但是為了擺脫軟體開發後期的泥沼,現在先下點苦工是值得的。這裡記錄一下過程中的心得。

背景知識

了解 TDD 前,得先知道重構 ( Refactoring ) 和單元測試 ( Unit Testing )。個人大力推薦 Martin Fowler 寫的《重構:改善既有程式的設計 》,可以了解何謂好的程式碼、如何找出該改善的程式碼以及如何改善它。書中列出多種簡單的操作流程,相當淺顯易懂。附帶一提,Martin Fowler 的個人站有許多經典好文,值得細細咀嚼。

TDD 簡介

TDD 是個知難行易的道理。知難,是因為我們很難相信它,到不是因為 TDD 概念很複雜,只要三句話就可以交待完 TDD:

  1. 先寫簡單的單元測試,並執行它。
  2. 用最簡單的方法實作需要的功能,讓程式能通過測試。
  3. 重構程式,並確保重構後的程式仍能通過測試。

實務上,這三條規則蘊含了經年累月的經驗法則。若沒親身體驗過,再怎麼解釋它們帶來的好處,也很難令人信服。這三條規則是環環相扣的,由於有先寫測試,才能安心地重構;由於有重構,才能方便地擴充功能;由於專注於最簡單的實作,省掉 over-design 的時間,才能用更快的速度完成該作的事,並有多餘時間寫測試程式和重構。藉由測試程式,程式設計師可以提高對程式碼的信心;透過重構,程式碼易於修改和重用。注意這裡強調「最簡單」的實作,這正是 TDD 精神所在,簡單的實作易於進行,複雜的考量如程式碼是否能重用、是否易於擴充,都留待第三步再做。專心正是將事做得又快又好的祕訣。

天下沒有白吃的午餐,要做到如上所述的理想世界,必須學許多寫測試程式的技巧。如同平常寫程式有輔助工具、函式庫、設計模式等,測試程式亦有對應的東西要學。舉例來說,產品用的程式需要重構,測試用的程式也需要重構。關鍵在於「treating their tests like first-class citizens. 」反之若將測試程式視為可有可無的附帶程式,只會陷入惡性循環,改變產品用程式的細節得改變測試用程式,反而變成得改兩倍的程式,以及在兩份程式中除錯。結果變得更糟,並誤認為測試程式是累贅。我剛用 TDD 時就因為沒把測試程式寫成許多小巧的函式,或是沒有寫成正確的檢查方式,造成產品用的程式是對的,卻常在測試程式裡除錯,造成無謂的損耗。

相關閱讀

2006年12月29日 星期五

獨立思考體系

上篇《管理學和實戰經驗》《資訊分享.無可取代的能力?》點出類似的廢話結論,以管理來說,如果下面這句是對,那我的優勢何在,大家立足點都一樣吧?

管理本來就是由人而來的,最好的做法還是到人群中學習

答案還是像廢話,獨立思考。

以justin為例,justin沒學過古典鋼琴,靠著自己亂彈苦練三年,發展自己一套「樂理」和演奏方式,比我這正規耍廢學六年的人強太多了。任何音樂,justin抓一下主弦律,就能自己配出伴奏。在自修樂理之後,justin將自己的經驗整理得更透徹,音樂能力更上層樓。

我斗膽用自己為例,觀看我寫過軟體工程和管理的心得,也許有人覺得”這傢伙好像懂點東西”,也許有人修過軟體工程或管理學,覺得我寫得有點道理。我想補充一下,我沒修過軟體工程和管理相關的課。不是想自誇,我懂得東西不多,這樣就沾沾自喜只是井底之蛙。有一天我會去修這些課的,也可能不會。如果我去修這些課,目的是對照自己的系統,補足缺失,修正自己的系統,而不是想學會用「課程教的系統」。我想強調的是沒有建立自己的系統,學什麼都是白費力氣,另一種說法,就是「知道」和「活用」的差異

我相信不管是學樂理、軟體工程,還是做研究,方法都是類似的,要從自己思考做起,自己去想、去猜、去試,再參照眾人在歷史中提煉出的系統,這過程是循序漸進的,什麼時候試,什麼時候參照別人的經驗,因人而異。

粗略的將人分成四類:只會做、做很久後才開始想、一直想而很少做、一邊想一邊做。大家都認同一邊想一邊做的人最有潛力,可是看看自己和身邊的人,這樣的人有幾個?我聽過太多人回答”光是趕完就快來不急了,那來的時間找新方法或新工具?”,於是有良性循環和惡性循環之分,花一樣的時間做事,能力成長的差異卻如滾雪球般愈來愈大。Hacker的精神是寧願更費事的找出聰明的解法,而不願用第二次笨解法。

我忽然覺得這篇的本質和《思考和語言的本質》一樣,比方說這句:”一定是先有認知才有代表的符號”,將符號的意義擴大來看,就是樂理、軟體工程或管理學。

喔喔,我竟然把兩個長年”思考歸納實戰”再”思考歸納實戰”的結果串起來了,真是太有意思了。不小心變成自high文了 XD

管理學和實戰經驗

我認識三位想從事管理的朋友,剛好背景差異很大,綜合他們和我本身的心得,整理出一些想法。

大二時我觀察自己平日打混做的事,發現大多集中在軟體工程,於是決定要走軟工的路,但後來發現軟工不適合在學界混,它只是種概念無法證明,本質很務實,研究起來卻不夠務實,於是我封殺這條路,將它當興趣,後來陸續發現不少學長也這麼想,了解軟工而把它當興趣,而非研究主修。我滿愛找人合作參加比賽,或修開在研究所的課,通常要三五人一起coding一學期,誤打誤撞補了些實戰經驗,更誤打誤撞的在大一大二時看了XP的書和《人月神話》,那時有所體會,每次回顧合作coding和書的內容,又有新的體悟。

justin是管科系、管科所的學長,今天和他聊到管科背景的優勢,他覺得管理學不讀也沒問題,但讀這些理論可以加強信心,畢境不是每個人自信都強如狂人,100%相信自己的做法是對的(一般老闆大概也不敢用狂人吧)。不過管理學實在太虛無飄渺了,也許管科背景的人多讀了三五年,而能讀出些感覺,這是其它系來讀一年管科而不能體會的。至於競爭優勢,大概是「知道」和「活用」的差別,這不管學什麼都一樣,知道概念很容易,要在對的時機對的地點用對方法就難了。justin另外提到創意和水平組合,水平組合需要了解多些領域,才能發揮,所以我們都同意平時認真打混,才有超出一般人的價值 :P ,以我來說,不務正業的打工和讀軟工,就是一個優勢。更明確的說,找出所愛的事,認真的實踐它們,有一天它們會奇妙的組合,而發揮價值,這是Steve Jobs的看法。當然,既然是自己所愛的事,沒有它們發揮的那麼一天,也無所謂 :)

If you find a job you love , you wil never work a day in your life.

這是題外話。

ltw是資工系學弟,原本想考管科所而唸了一堆書,後來卻改變主意。我問他唸了這麼多管理學,有學到什麼嗎?他說驗證了他在AIESEC裡學到的事,不然光唸那些東西,只會迷惘,AIESEC真的是很成功組織,他學到很多,他無法逐一說明給我聽,於是我舉情境問題,問他帶組織的方法,他回答要看情形,做法差異很多,我描述的問題太一般化。聽ltw這麼回答,雖然無法增加我的知識,但我明白他真的學到很多,若我是他,別人問了一樣問題,我也會回答看情形(路人:你來亂的哦!)。

ltw學管理學的過程,和justin提的加強自信,或我的學習背景相似。我另外想到大三學Network Programming (NP) 和修Operating System (OS) 的事,前者是實際coding,後者是理論架構,照理說先學理論再實作才對,一般都是由上而下的做法。我反過來做,結果許多同學修OS時覺得太虛無飄渺,而我從NP的例子裡,一一驗證OS講的概念,而更能掌握整個系統。原本OS就是從實作中層層歸納、抽象化的概念,它的本質是實作,不是那麼的「科學」,軟體工程亦是如此,我想管理學也是這樣。它們應該要有實戰經驗,再由別人統整好的理論,協助建立自己的理論,再增加更好的實戰經驗(已懂得轉向時機,可以跌輕一點),再看理論建立出自己更好的理論,我想特別強調「自己的理論」,無法融入自己的思考體系,只是知道而已,無法活用

我認識一位教官,他退休後到大公司裡上班。他認為要了解大公司的體系,最好的方法就是進去看。教官退休前有修管科所的課,唸不到一年感到太空洞而不讀了,他覺得管理本來就是由人而來的,最好的做法還是到人群中學習

如同水平組合的想法,藉由三位管科相關的朋友,我又釐清一些軟工的想法。如同一再強調的,軟體工程有技術層面的議題,也有人的議題,而人才是核心部份,所以軟體工程不是單純的技術而已,更包含管理學。這些想法,兩年多前就在《人月神話》裡讀過,今日更有體會。

The Joel Test: 12 Steps to Better Code

看到kgo轉中文翻譯的12項目到個版上,想到以前有看過這篇,拿出來備忘一下。光看12項目可能不懂它在說什麼,對照原文看就清楚多了。

一半的項目在強調隨時保有所有(過去、現在、”未來”)可執行的程式碼,讓程式能還原到任一階段,並且是能動的程式,觀念和XP相通。

出處

1. Do you use source control?
2. Can you make a build in one step?
3. Do you make daily builds?
4. Do you have a bug database?
5. Do you fix bugs before writing new code?
6. Do you have an up-to-date schedule?
7. Do you have a spec?
8. Do programmers have quiet working conditions?
9. Do you use the best tools money can buy?
10. Do you have testers?
11. Do new candidates write code during their interview?
12. Do you do hallway usability testing?

2006年12月22日 星期五

開發速度和執行效率的取捨

最近發現在學弟版上回的問題,比我自己版上po的東西有營養多了 XD ,有些回文改改就適合放在Blog上分享。這篇是討論技術層面的軟工,人事管理的軟工請見”軟體開發與管理的思維:人月神話”

我看到的 書/文件 都強調不要早期做最佳化自早苦吃,把時間花在刀口上,最好是能動,快速開發出prototype,再漸漸改好它。更何況最佳化可以從很多層面執行,舉凡演算法、coding技巧、libaray、compiler、OS、hardware等,不見得要拘泥在一處。

在和人合作寫project,或自己寫project的經驗裡,快速開發士氣較好,士氣會嚴重影響到產品的存亡。

至於最佳化的議題,可以用profiler抓bottleneck,確實調整最該調整的code,在我的經驗裡,用profiler常抓出想破頭也不會注意的bottleneck。

舉例來說,我用Java寫了統計文章裡所有單字出現的次數,我辛苦tune hash table半天,把linked-list改成binary tree也沒快到一秒,用profiler一測,發現1/3的時間耗在class String的split(),改用StringTokenizer一步步取單字,而不是一口氣轉成String array後,就省下1/4的時間了。另一次的經驗是用profiler發現object包太多層後,有些object被重new一次,增加一倍的初始化overhead,memory用量凶時會因garbage collector運作而嚴重delay,去掉多餘的 new 後,程式快了一倍以上。

這裡不是說演算法分析沒用,那是基本能力,實務上除了complexity分析外,更要配合profiler雙管齊下。

另外可以透過refactoring的技巧,將快速寫好的破爛code趨於完美,讓它更易擴充,更易最佳化。

做個總結,就是:

快速開發prototype -> 反覆refactoring + 再開發 -> project尾聲使用profiler做最佳化

參考書藉:

  • eXtreme Programming (XP):我忘了看過的那本書名,相關的書應該有不少。
  • Refactoring:這本講概念並以Java實作的code說明,現在各家IDE漸漸支援Refactoring,做起來簡單又不用擔心改出bug。

2006年12月14日 星期四

駭客與畫家 - LISP和創業

書藉基本資料: 駭客與畫家

讀完全書了,本書大概分三種內容,程式語言、創業和其它,其它包含有趣的《垃圾郵件大作戰》和一些有趣想法。作者認為程式語言要活,而不是把programmer當笨蛋限制,作者大力反對Java,大力支持LISP,Perl、Python也較合作者的品味,似乎hacker都很喜歡Perl。

作者對軟體開發和創業的觀點一樣簡單,小而美,快速開發一個破爛再來改。比較特別的是提到用最少行數達成一樣功能的程式語言最有戰力,行數可以視為產能指標,用LISP寫千行能做到的事,用C、Java可能要寫到萬行,搞不好還因過於龐大而寫不出來。作者以Viaweb的創業心得做為實例輔證,說明他們開發核心是LISP,再輔以其它語言。

我對程式語言的看法和作者大部份相似,主要的差異在於我喜歡Java,我學過一點Scheme,感覺還不錯。看過作者觀點後,提高許多學functional language (FL)的動力。似乎懂FL的人對OOP和其它程式語言的特徵都有另一番解讀,像是更上層、更為抽象的觀點,作者有點嘲笑OOP的design pattern,認為那是將pattern搬到人的思維處理,而不是程式語言本身,FL的功能就隱含一些pattern(作者引用另一位研究OOP的人寫的文章)。書中提到某位hacker說過,學FL不見得能用FL完成你的工作,但能為你帶來許多新思維。作者則是進一步認為FL可以完成更多工作,關於FL的看法,可以參閱作者網站。去瑞典的查默斯理工大學讀書的command和大中應該很有心得吧,摸索一陣後一定要好好討教。

談到創業,作者先提一個有趣旳觀點,一生可能要花30年賺一筆大錢,你為什麼不把這30年零散花的時間集中在幾年內做完,還比較有效率。沒錯,進行複雜工作時,要先花一段時間回憶,進入狀況要花時間和精力,若能加長單次工作時間,減少總工作次數,一定更有效率。而現今的制度,很難依個人成效發薪,結果是大家不想多做事。sales是成功的例子,業積容易考查,分紅容易,但programmer不然,無法判定是那個小組的功勞,更不用談某小組裡的某個人。總結來說,自己創業成效最大,才會甘心做更多事。

不過相對於創業的麻煩,我還是比較喜歡悠閒的學界生活,只要不缺錢就好。

2006年12月13日 星期三

管人和被管的心得

最近管人和被管的時間較往常多,有些心得,這篇姑且分類在軟工吧,反正軟體工程的核心在人,最難搞的也是人。以下用組長通稱管人的,用組員通稱被管的。省了實例避免尷尬,心得參照自身和別人的經驗。

組長不懂拿捏事情輕重,想到什麼就要求什麼,若組員照單全收,結果是組長看不到關切的成果,組員做得要死沒成效。

組員處理的方法有二,學會說不或是先忽略,重要的事會被重覆提,不用擔心沒有關切的成果。這個做法讓組員有餘力執行最該做事,但沒效率,或是組長太忙,期限快到才想起來,結果事情真的不能延,組員只好爆肝用力救,成果不好,士氣低落。至少,說不和忽略的策略能讓自己好過點,不會做一堆白工。

組長應該吩咐事情時就拿好優先順序,並告知明確的期限,”愈快愈好”和沒期限一樣,被拖延的機率較高。

但組長給彈性時間會有新問題,大多組員直接忽略彈性時間,不管有多彈性,都是期限前動工;有優先性的工作,表示低優先性的可以繼續擺爛。組長最好把工作切細而明確,讓每件小事的期限不會拉太長,但叮進度會累一些。

另一個問題是組長只有大概想,組員做下去才會發現問題,但組員通常不愛想,抱著儘管做的心態,快完成時組長發現不對就尷尬了,士氣低落,相處不順,改不改都不是。所以我當組員時,會問得很詳細,包括目的、原因,讓自己有全面思考的能力,避免發生尾期才發現不對的慘案。當組長時,找會想會做的組員,若組員不會想,進度就要問緊些,比較累。

實作時,如果組員很有想法,也得視情況先提供備案或限定方向,太多方向讓人無所適從,Brooks在《人月神話》裡的名言:”Form is liberating”(形式即解放),適當的限制有助於明確的發揮。

開會前應準備好流程表,事先寄email給大家,slide或純文字都OK,slide方便條列式和放圖。必要時要求大家想好再開會,或要求各小組在大組會前討論過,避免開會時進行多人討論,人多嘴雜沒效率。會後一定要有會議記錄,口說無憑,事後有爭議傷士氣,會前有流程表,會後做記錄很容易。

不見得每個組員都吃同一套,開始共事的第一步,就是確認好共事的方法,比方溝通用媒介(手機、email、MSN),需要用的工具(SVN、Wiki、Calendar),逐步建立共通習性(非組長習性)。

溝通時帶點幽默很有效,可以化解尷尬、提高士氣,幽默可能需要靈感和練習。無論是管人還是被管,最有效的做法還是以誠待人,推己及人。很嘴炮,也很實用。