2008年8月10日 星期日

一字曰心

昨晚的綜藝大哥大『大魔競』單元,場景搬到中國大陸,各參賽者輪番上台,表演完畢後,由台下五位大陸及台灣的評審進行講評,規則為五個評審中少於三位評審按燈,即算過關。其中有一位五十多歲的張姓魔術師(姑且稱呼他為張大哥)的表演,及之後的評審講評,讓我感觸良多。那天張大哥的表演並沒有失常,但他卻沒有過關,其中一位大陸評審認為他的手法熟練,但缺乏新意,評審劉謙則認為他沒有扮演好一位魔術師的角色,『身為一位魔術師,永遠不要忘記兩件事,一.永遠不要忘了娛樂你的觀眾,二.永遠要想辦法提升自己的實力』。張大哥為職業魔術師,平時即以表演魔術為業,接觸魔術超過二十年以上,換句話說,台下許多評審,不論在年紀或是魔術界的資歷,都算是他的後輩。年紀一大把了,還要站在台上被後進小伙子嫌東嫌西,想想真是情何以堪。劉謙的話,移到軟體開發上也同樣適用。『身為一位開發者,永遠不要忘記兩件事,一.永遠不要忘了滿足你的客戶,二.永遠要想辦法提升自己的實力』(客戶除了實際的customer外,也包含主管)。

最近review一位網友的程式,驚訝地發現,一位有兩年以上經驗的職業programmer寫出來的程式,竟然跟一個程式初學者差不多,一些粗淺的觀念都搞不清楚,「這些年來他是怎麼過的?」,內心充滿疑惑,我們彼此平輩論交,沒有長幼問題,但說得太直接怕傷了他的自尊,只是淡淡地點出一些問題。思前想後,會造成這樣結果的原因可能有幾個:
  1. 他從來都沒有嚐試想要提升自己的實力
  2. 他有嚐試想要提升自己的實力,但失敗了
  3. 他有嚐試想要提升自己的實力,他認為他成功了,只是卻不是這麼一回事
1是我所無法想像的,個人自從成為開發者,無時無刻不在思考如何開發出符合客戶需求,又能兼顧效能和維護性、擴充性的系統,因此也總認為所有的programmer皆跟我有相同的想法。第2點也是令我難以理解,難道利用兩年的時間(好吧,可能在這兩年之中,有很多公事要忙),把自己的實力由初學者向上推一階會有困難?讓我無法想像的是,每日浸淫的東西,竟然會差勁如斯,如果是這樣,建議應及早轉行,以免耽誤大好青春。3是我認為最有可能的情況,他非常地努力提升,也很滿意自己提升的成果,但他是在象牙塔中做這件事情,導致無法分辨他所認為的成果是正確還是錯誤、程度是大還是小。或許比起兩年前他已有進步,但以我們對工程師的要求,他的成長小到幾乎看不見;或是比之兩年前,他已有很大的不同,但因為他的努力方向錯誤,導致他愈寫愈像新手。我想張大哥在他二十年的魔術生涯中,一定也想過要提升實力,只是或許他一直以為(或許當年的社會觀感即是如此)只要具備熟練的手法,就是一位好的魔術師。

思考至此讓我心生警惕,會不會二十年後,當我已是公司資深開發人員時,寫出來的程式會讓後生小輩看不下去?自認很有上進心,但我的努力是在正確的方向嗎?一直使用錯誤的練功方法,自然久練不成,但數十年已過去匆匆,再也不會回來。看來廣泛地學習,多觀摩前輩後進的功法,避免坐井觀天,不斷修正自省,是避免落入象牙塔的唯一方法,嗯...要用心。

2008年8月2日 星期六

網拍二三事

迷人的網路拍賣
網路拍賣迷人之處在於,身為買家我們可以用相對便宜的價格,買到理想中的商品,或許在品質上有些妥協,也許是二手的物品本身有些缺陷(如買二手書,書內可能有前一位持有者所留的筆記重點、破損髒污),但對真正有需求的人來說,大部份的缺陷並無損我們對商品價值的期望。譬如,我想買一本書,實際上的情況是,我想要得到書中所闡述的內容,因此對於書面是否有污漬、書內是否有另外一個人的筆記並不在意,只要不影響我正常閱讀即可。若在一般正常的通路購買,可能要花上五百塊,但在拍賣中可能以不到二百五的價格就可以買到。

身為賣家,我們可以把有價值,但用不到的物品,以低廉的成本(拍照、上架所花費的人工)出售。在以往若我們持有這種用不著,但卻明知其價值的物品的處理方式可能為︰
  1. 送給需要的親友
  2. 賣給資源回收商
  3. 敝帚自珍,留給子孫
  4. 在門市出售
1可能是我們以往最常使用的方式,既為親友,又名為『送』,當然無法跟其收取費用,這不失為敦親睦鄰的好方法。但想到古人言:『己所不欲,勿施於人』,這個自己不想要的東西,有時也不怎麼好意思送給別人,也許收到的親友嫌髒舊而未可說。所謂親友,即是在我們周遭,就算不是常見面,也總是遇得到,一個搞不好弄得以後見面尷尬就麻煩了。

2是一般非做生意之民眾最常用的方法,筆者在開始經營網拍之前,就經常將用不到的舊書籍賣給資源回收商。目前廢紙的回收價格約為一公斤六塊錢,一本書能有多重?想到當初花費數百元買的書,如今卻以不到二十元賣出真是情何以堪啊!在這樣的心理下,部份人會採取3的方式,「老子帶到棺材裡總行了吧!」為了賭這一口氣,將無用之物堆滿已經沒啥空間的家中,沒學過經濟學也知道庫存是需要成本的,自己家裡雖然不收租金,但狹小的空間影響生活品質,颱風淹水時還要分神照顧,白蟻蟲咬再來心痛惋惜。

4的方式大概只有平時就是做生意有門市,或是有親友可以寄賣的民眾才能做得到。實體門市的成本高,需要人工去上下貨、門市清潔、收銀、管理、人員...太多事務需要人工去handle,對於有其他正業的人來說,通常難以當做業餘兼顧。

網拍訂價是一門藝術
在網路上賣東西,訂價是一門藝術。在實體商店盛行的時代,資訊較不對稱,貨比三家所付出的代價可能高過價差。今天我在這家鞋店看中意一雙鞋,但要到隔兩條街去對同一雙鞋進行比價,到了兩條街的那家店後發現反而比較貴,這時我應該繼續到第三家店呢,還是回到第一家店去直接買下?網路上比價所付出的代價極小,最多是再開一個視窗google一下,跟您賣同樣物品的商店可能不止一家,若您的訂價比較高,憑什麼客戶要光臨?

東西只要能夠deliver到需要的人手上,其價值是不可限量的
訂價應根據什麼?若是以自身無用做為出發點,那麼不管價格訂得多低,抱著『反正沒用,只要賣得出就賺到』的心態,可能會把價格訂得太低;若是以市價做為出發點,『當初買時花了五百元,現在要賣二百元怎麼賣到下去?』恐怕會把價格訂得太高。訂價太高商品在架上乏人問津內心焦急,訂太低在東西被買走時搥心肝『心痛啊!怎麼不把價格訂高些!』,筆者就曾經在未經調查又急著出門的情況下,匆匆將商品貼標上架,結果瞬間被眼明手快的買家,以迅雷不及掩耳的速度買走,當時可是心痛久久不能平復,做生意誠信最重要,高價物低價賣出只能認了,以當做廣結善緣的想法,希望東西能夠對她(他)有所幫助,並期望自己早日在這樣的複雜心境中,學到訂價策略之奧義。

2008年7月28日 星期一

沒有人可以教(會)你任何事

記得上高中前,在學校中常常有一個困擾-老師教的我都聽不懂。看到這邊可能有人會覺得我是那種功課很差的學生,正好相反,我在班上總是名列前茅,且經常是同學們在課業上的小老師。我發覺那些功課不如我的同學,能夠聽得懂老師的講解,而那些來請教我的同學,在經我解說後,大部份也都能理解。我比別人笨嗎?心中不由得昇起這樣的念頭,是的,我認為我不比其他同學聰明,但這樣的話由一位總是在段考拿第一名的人口中說出來沒有人會相信。

由於上課聽不懂,個性又內向不敢發問,在課後總是要自己再多花時間在書本上,這種情形讓我開始思考學習和教導的本質。在我們開始說明一些東西之前,先定義一些名詞,以避免後文讓人混淆:
  • 老師 任何意識的傳授方我們稱之為老師。
  • 學生 任何意識的接受方我們稱之為學生。
  • 意識 老師意圖讓學生接受並瞭解的所有知識、常識、資訊等。
  • 領悟 學生理解老師所傳授的意識的過程。
教導及學習的過程為:
  1. 老師提供教導
  2. 學生領悟並學會
教導可以分為兩個部份:『教』及『導』。單純指的是老師將意識以任何方式呈現給學生,而指的是老師以各種方式,建立起學生達成領悟的基礎。由上1可知,老師只負責這個過程的教導部份,而學不學得會的關鍵還是在學生身上,如果學生資質不行或基礎太差,恐怕是不成的。「智商太低的學不會」這樣的說法可能會招來許多抗議,我們在成長的過程中,一向被灌輸「勤能補拙」的觀念,這句話在大部份時候還是對的,因為絕大多數的人都有足夠的資質,能夠學會老師教導的東西,但若是駑鈍之資或是朽木不可雕,孔子再世也會束手無策。

某些老師只注重教,而常忽略導,造成的結果是學生經常覺得那位老師的課艱澀難懂,許多學者式的教授都容易落入這樣的巢臼。補習班的名師之所以是名師,是因為他們在教的表達上能夠很清楚,在導的技巧上又熟諳,而上他們課的學生,通常是具有某些基礎的,因此往往在他們的課堂上會覺得如沐春風。在武俠小說中,身懷絕技的高人在教弟子時總是顯得高深莫測,是因為在這樣的領域到達一定程度後,若直接用「教」的方式,學生的資質基礎足夠也就罷了,若學生的基礎不夠,自行曲解,只是一些拳腳刀槍則易走入偏門,若是高級的內功則走火入魔甚至失去性命。

沒有人可以教(會)你任何事,學不學得會關鍵還是在自己身上,學不會的東西有可能是因緣不足、條件不夠,這時放棄是稍嫌過早;也許有些東西一輩都學不成了,或是花了一輩子也不及某些人的一個月,這不打緊,每個人都有他的優勢天份所在,不一定要鑽牛角尖跟著湊熱鬧,找到自己的興趣和長處,好好培養發展,終究能夠有好的成就。

2008年7月15日 星期二

Interface的應用:Dependency Inversion

所謂DIP(Dependency Inversion Principle) 依其定義:
  • 高階模組不應相依於低階模組
  • 高階模組和低階模組皆相依於抽象層
  • 抽象層不應相依於實做細節
  • 實做細節應相依於抽象層
這樣的設計方式,可讓所有物件/模組間的相依關系減至最低,最佳狀況下能夠達成物件鬆藕合的理想。但理想和現實是有差距的,為了達成理想而必須付出難以忍受的代價是不切實際。能夠想像所有的物件定義,皆須有一至多個對應的interface的情況嗎?有誰真能在寫程式時,所有變數皆以抽象層或interface替代?本篇並不是要提倡DIP,甚至在接下來的內容是違反DIP的,若想研究DIP應試著用google key入DIP關鍵字,本篇只在說明如何使用interface達成dependency inversion(相依性反轉)。

所謂Dependency Inversion
假定我們有套件及類別如下:

套件

內含類別

用途

EIP

UpdateUserData

更新使用者資料

EIPLibrary

WriteToDatabase

寫入資料庫


EIP package定位為高階的模組,用於處理使用者相關資料;DbLibrary定位為低階模組,用於協助EIP package將使用者資料寫入資料庫。沒錯,這是典型傳統的結構化程序設計中,高階模組依賴於低階模組的情況,今日我們並沒有打算改變讓DbLibrary相依於EIP,但考慮以下情況:
DbLibrary的部份定義為
class WriteToDatabase {
void WriteNow( UserData obj ) {
WriteName( obj.GetName() );
}
void WriteNow( string userName );
}
由於WriteToDatabase中使用了UserData,所以我們說WriteToDatabase相依於UserData,同時也意味著DbLibrary相依於EIP;但一開始我們已經說明,DbLibrary的目的,是協助EIP,也就是EIP本已相依於DbLibrary,今WriteToDatabase的撰寫方式,豈不是讓WriteToDatabase相依於EIP?循環相依,明顯的是一個設計上的錯誤,同時WriteToDatabase相依於EIP的情況,也讓WriteToDatabase的用途受限(引用WriteToDatabase時,勢必同時要引用EIP package)。有什麼方法可以fix這個窘境?有,答案就是interface,我們可以在package DbLibrary中定義一interface:
interface IUserData {
string GetName();
}

接著讓UserData implement IUserData:
class UserData implements IUserData {
string GetName();
}

再來我們修改WriteToDatabase的定義:
class WriteToDatabase {
void WriteNow( IUserData obj ) {
WriteName( obj.GetName() );
}
void WriteNow( string userName );
}

由於IUserData是定義在DbLibrary中,因此WriteToDatabase並不需要相依於EIP;而UserData因為參考到IUserData,所以反倒是依賴於DbLibrary了,如此即反轉原本程式的相依方向了。

2008年7月13日 星期日

關於Interface

『我不太曉得何時該用Interface』最近一位後進沮喪地這麼跟我說著。當下我只是安慰著他『多用幾次就會了解了』(實際上也是如此),但想起自己當初也曾為了Interface這個奇怪的型別而困惑,因此決定寫一篇文章,期盼能夠對後進同好能有所幫助。

一切要由Lauguage實做的特性說起。一般Language於實做時,對於型別的檢查可分為以下幾種類型:
  • Statically typed language:語言的型別檢查發生於compile time,大部份的statically typed language compiler要求使用者在使用變數前須事先宣告型別。例如:Java、C、C++、C#,好處是多數語法上的錯誤,能夠於早期發現,以及程式執行上更有效率。
  • Dynamically typed language:相對於Statically typed language,語言的型別檢查發生於run time,於第一次指定變數值時,compiler(or interpreter)始決定變數型別。例如:PHP、Perl、Python,好處是使用上具有彈性。
  • Strongly typed language:語言於執行期,其型別不可隨意更改,例如:一個儲存integer的變數,無法用於儲存字串,Java,C,Python等語言皆是。
  • Weakly typed language:相對於Strongly typed language,變數的型別可勿略,例如於PHP中,我們可以將字串'12'和'3'concate在一起,最後再把'123'當作整數123來進行數字運算。
Interface的使用和Lauguage的Statically typed有關,起因於compiler於compile time要求型別的檢查。以下以一個例子說明會較清楚,假定有一要求傳入一個型態為SomeType參數的SomeMethod函式:
void SomeMethod( SomeType inputObject ) {
inputObject.DoSomething();
}
class SomeType {
public void DoSomething() {
//DomSomething implement
}
public void DoOther(){
//DomOther implement
}
}
因為型別檢查,compiler已確定inputObject 確實為型別SomeType,故我們能很放心地於SomeMethod中執行 inputObject.DoSomething() ,而不必擔心inputObject是否已定義了DoSomething()。但SomeMethod這個函式似乎不怎麼好用,因為它要求的參數型別固定,我們無法傳入其它型別的參數,接觸過物件導向Programming的人,應該很快可以想到,我們可以使用繼承的方式,從SomeType衍生出子類別,而此類別的物件,亦能用於呼叫SomeMethod:
class SomeTypeChild extends SomeType {
public void overrides DoSomething();
}
SomeType obj = new SomeTypeChild();
SomeMethod( obj );

類別繼承的好處是,我們可以繼承父類別既有的介面(interface)和實做(implement),若今日我不想繼承SomeType的實做部份,但又想使用SomeMethod呢?又或著我只想繼承SomeType的DoSomething介面,因為SomeMethod中,只需要使用到SomeType的DoSomething?這時Interface的功用就突顯出來了,我們可以改變SomeType的宣告,接受一個有DoSomething介面的物件:
void SomeMethod( InterfaceSomeType inputObject ) {
inputObject.DoSomething();
}
interface InterfaceSomeType {
DoSomething()
}

接下來,只要是繼承了InterfaceSomeType,而有DoSometing介面的型別,皆可用於SomeMethod的參數呼叫,而不用繼承原來SomeType的實做部份,和SomeMethod中不需要用到的DoOther部份。

介面繼承(或介面實做)是類別間的約定,實做該介面的類別『同意』並『保證』要提供介面中所定義到的method和property。由以上的簡單例子,我們可以了解到Interface可以讓我們專注於物件的介面,而不須牽涉到到類別的實做。

下篇,Interface的應用:Dependency Inversion

2008年7月11日 星期五

備份永遠無法全面

Visual studio或是其他的IDE開發工具似乎都有一嚴重缺點,在專案變得龐大時,所耗費的系統資源愈多,電腦回應的時間爆長。這在專案累積至一定規模後簡直是一個惡夢,即使是修改一個小功能,編譯測試也要等半天,有些懷念當時在開發PHP式時的靈便。因此我習慣在現有專案中加入新Features的方式,是新增一個temporary專案,在該tmp專案中撰寫測試無誤後,才將新的程式碼porting至原有專案。這在舊有專案已經肥得不像話時特別好用,小型專案的好處是,不論在開啟、編譯、測試、啟動都是飛快。通常tmp專案是沒有Commit至Subversion的,因為「總有一天」它是要被整合到原有專案的。
今日跟平日一樣,一到坐位上,開啟開發用的PC,正覺得電腦怎麼回應的比平時還要慢上數倍,就聽到硬碟發出哀嚎聲,接著電腦就不理我了,硬碟掛了...心裡的OS這麼說著。正在慶幸好加在平時就有固定把程式上到Subversion,突然心中一痛,想起最近寫好測好,放在tmp專案中還沒整合到原有專案的程式...-_-|||(第二次之後應該會快很多,心裡邊安慰自己邊流淚...)

2008年7月7日 星期一

Code Complete

Code Complete

據說在programming界要成為神的人,都必須看過這本,先把祂請來供在書架上。

系統開發之閉門造車

不曉得各位在學習ASP.net時,看的是什麼書,仗著對http protocol的小小了解,我是沒有看任何關於ASP.net的書,知識全由網路上先進的文章而來,但看了前輩的文章,發現這會是有盲點
(的確是有很多盲點,從自己目前開發系統版本某些特立獨行的地方就可以知道了)

以下是一些引用:
“一直寫 Code 而不看書是不實際的,這樣的寫 Code 過程必須面對許多因為觀念不對而導致寫錯程式的狀況”
“沒有完整的觀念會導致寫程式缺乏效率”
“部分程式設計師都很忙,都在忙著寫 Code,真要讓所有人想清楚再寫是不太可能的,所以大多數的人都一樣先寫了再說,最後如果有時間再重整自己的程式碼,因為程式設計師都是蠻懶的,所以我還蠻懷疑有幾個人會主動重整自己的程式碼,大多應該是沒出 Bug 就得過且過吧!不過會自己主動重整自己程式碼的人大多是有潛力的人才”
(你會重整自己寫寫的程式嗎? Refactoring是個好習慣)

作者推荐了一本入門書Professional ASP.NET 2.0 Special Edition,我想可以做為參考。

檔案下載solution

關於使用browser下載檔案,針對http header的設定方式,因為client端的異質性,著實讓我們吃了不少苦。以下是我認為最佳的header設定方式,同時在IE6,IE7,Firefox中work又符合user期望,雖然是用VB,但http header的部份其它language並無不同,其中有幾個地方特別值得我們注意:
1. 使用IE下載檔案,預設是用inline方式,直接開啟在browser中,此點是為了避免IE在下載檔案時的cache defect,至於其它browser,因為可用browser自身的選項決定是否直接開啟,並無設定inline需要。
2. 使用IE下載檔案,若user不想inline開啟,可教導user於下載連結處按右鍵,另存目標即可。
3. 雖然我們設定了response header是用utf-8編碼,但IE仍無法辨識特殊字元的檔名,因此檔名的部份用了url encode,但其它browser並無此問題。
4. 對firefox來說,既然已指定了response header為utf-8,此時若在對檔名進行url encode,反而會得到不符合user期望的檔名。
5. 檔名的部份,最好一定要用引號括起來,以免檔名在包含空白字完時被截斷; IE下載時,因用了url encode,故無此問題,但其它browser因未用url encode故未加引號會導致截斷。

Response.ContentType = dt.Rows(0)("file_type")

Response.AppendHeader("Content-Length", dt.Rows(0)("file_size").ToString())

If IsIe = True Then

Response.AppendHeader("Content-Disposition", "inline; filename=""" & Server.UrlEncode(dt.Rows(0)("file_name")) & """")

Response.AddHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0")

Response.AddHeader("Pragma", "public")

Else

'檔名加入引號, 可避免檔名有空白時,被截斷的問題

'上方的IE,因用UrlEncode雖無此問題,但還是加

Response.AppendHeader("Content-Disposition", "attachment; filename=""" & dt.Rows(0)("file_name") & """")

Response.AddHeader("Pragma", "no-cache")

End If

Response.BinaryWrite(dt.Rows(0)("file_content"))

ASP.Net自動測試

.NET Test Automation Recipes

書中講得很實際,直接告訴你怎麼做,書名雖然為.net test,但其中的東西若用其它的language/平台實做,也都適用,我只針對Web Application做了閱讀,以下是一些心得。

[ASP.net自動測試] 簡單來說,就是研究asp.net的運作時的特性(有哪些回傳值,什麼動作應回傳什麼等),寫出模擬browser一連串動作的程式。

書裡有提到自動化測試ASP.net的方法,一些tricks:button被按下時,送出對應的post data為何,checkbox被選取時,對應的post data又是如何,在一開始,如何截取__ViewState,以作為下次post之data。
test之指令稿類似以下:
0001!DropDownList1=red&Button1=clicked!You picked red
0002!DropDownList1=red&Button1=clicked!Bad choice!deliberate fail
0003!DropDownList1=green&Button1=clicked!You picked green

001模擬DropDownList1選取red,而Button1按下的情形,應得到的回傳值為"You picked red",因此自動測試時,測試程式需要事先取得:
1. 畫面上server control render成html時的id(DropDownList1,Button1)。
2. html control之設定值(selected value),有些tricks,像是button被按下時,是設定Button1=clicked。
3. 畫面預期的回傳值。

[心得結論]
Tester(即使是developer)要取得1,2,3資料,勢必要實際用真的Browser連過一遍,寫test script很花時間,一旦原程式有變(refactoring或其它原因),test script必須隨著修改,可以把測試的框架程式做些改良,這樣同一web page的複雜動作(第一動,第二動,第三動...要得到什麼結果)就可以全部寫在test 指令稿內表達,一旦寫好test指令稿,以後要做重複的測試就快多了(如果原程式沒有變的話),test case指令稿,誰來debug?第一次測試,為了生出指令稿(html control的id要在這裡知道),和驗證指令稿,一定要手動測一次之後產品有了新版release,再用這些test case去自動測試時,就會快多了(而且較不易出錯),也就是第一次測時,會花相當多的時間。

[問題]
複雜的畫面(很多control的,或是user control一層包一層),指令稿會很難寫(煩),寫/維護指令稿是很無聊的(做這個要幹嘛?看不到希望),誰要來寫?

關於品質

最近在客戶端,有一經常出現的問題,一直困擾我們--問題資料,經過調查,很可能是某個已存在的bug,在修正前所造成的錯誤資料,所謂[很可能],基於資料完整度,我們無法百分之百確定是不是這樣,但種種跡象似乎都指向這個defect,另一因素是因為,也許這樣下結論,可以讓我們心理好過點,暫時不必戰戰兢兢地去想,何時會再出現這樣的問題資料。

同一個defect,前前後後花了我們不少時間,在追查問題來回源,回覆客戶,和修正資料,雖然已經修正很久了,甚至在程式中已不復存在,但直到現在,我們仍在為它所造成的影響而付出成本,而我也預期接下來的一兩個月,我們仍要付出時間,去追蹤這個問題。

這幾天,我反覆思考,到底是哪個環節出了問題?在思考的過程中,我曾經想:
1. 是在一開始交代工作時,沒有說清楚嗎?
2. 是在review階段不夠詳盡,導致遺漏?
3. 是在test階段沒有疏失,造成這個漏洞?


以下是我的思考結論:
1. 交代工作,由A交代給B,A沒實際去做過,因此只能抽象地交代B,不管A在腦袋中如何沙盤推演,實際去做時,一定會有不及之處,如果A能鉅細靡遺地交代B,可能表示:
(a). A天 英才。
(b). 或者是A花了非常多的時間,在腦袋中沙盤推演,甚至實際模擬。
我想(b)較有可能,A花了非常多的時間?事情是要交由誰做?如此不就失去了交代的意義?

2. review如果夠詳盡,勢必花費相當於實做的時間,這是不可負擔的。
3. 以目前的test case覆蓋率來看,只足以滿足評鑑,test case的覆蓋率不高,

就不能對測試後的品質抱太高的期望,而且短期內,高覆蓋率的測試,是不可負擔的。以上2,3短期內做不到,為了避免問題,所以我們要期待鉅細靡遺的交代?鉅細靡遺不如不交代 = 自己做,所以公司內大小事務,最後都會落到一個人身上-總經理,鉅細靡遺的交代是很有問題的。

竊以為,負責implement的人,有最好的條件,可以最清楚狀況,最有可能最有機會發現問題,千萬不要抱著,[我只負責我被交代的事務,其它不在我範圍內的心態],當你在implement,發現某處可能有問題,或是隱隱覺得可能會出包,是的,沒錯,你是第一個發見這個問題的人,請試著把它紀錄下來,跟交代你的人或co-worker一起討論,或是在會議中提出,試著在它上線前解決它(即使已經上線),如果不這麼做,可能會發生[看吧,我的第六感是正確的],這時必須付出更多的成本。

要培養發現問題的習慣及能力,可以時時思考user使用時之scenario,個人認為,軟體品質,比軟體時程還要重要,雖然有時我們不得不在這之中做些妥協。

From Index to Archive

如果今天我要在系統中,查詢存在Database中所有2008年的資料我會怎麼做?一個很直覺得做法:

select *
from hum_note
where YEAR(humnt_sdate) = 2008

我相信對大部份人來說,這是很直覺的,但在where條件中使用function,會令資料庫中,賴以增加效能的index失效。所謂index,是排序後的資料,經由對排序資料的查詢,以減少必要搜尋資料的數量,進而減少搜尋時間,使用者直接感受到的,是[系統變快了]!說到這邊,先以一個簡單的例子,來說明index在查詢時,得以增效的原理,假設有一10個成員的整數陣列:

[4,2,9,6,3,8,7,1,5,0]

若想在這個陣列中,篩選出 >= 5 的成員,需要進行多少次的比對處理?因為資料並未經過排序,因此10個成員,至少須比對10次,如果資料是經過大小排序,想篩選出 >= 5 的成員,又需要進行多少次的比對處理?

[0,1,2,3,4,5,6,7,8,9]

以人腦來說,大刀一切,可能有人回答: 一次
以電腦來說,接觸過演算法的人,會回答: log10 (以2為底的對數,10為總資料數,即所謂時間複雜度為logN),若是上例未排序的資料,時間複雜度則為N,這意味著,複雜度為N,若資料量增加二倍,處理的時間會增加二倍(2* N),複雜度為logN,若資料量增加二倍,處理的時間會增加2 * logN倍,有感受到index的威力嗎? 有index的資料,不但處理時較快,在資料倍增時,處理時間的增量也比較小,我們再回到一開始,查詢系統中2008年所有差假的問題,

select *
from hum_note
where YEAR(humnt_sdate) = 2008

這個查詢,資料庫引擎需要處理多少次,時間複雜度是多少?需要處理多少次,無法回答,要視hum_note中有多少資料而定,因為資料庫引擎沒有預知的能力,因此它要計算出每一筆資料的YEAR(humnt_sdate)後,再拿計算結果和2008進行數字比對,才能知道某筆資料,是否符合這個查詢條件,也就是說,
若hum_note中有10000資料,資料庫引擎需要處理10000次
若hum_note中有30000資料,資料庫引擎需要處理30000次
時間複雜度為N,因為資料庫引擎沒有預知的能力,在where條件中使用function,會令資料庫中,賴以增加效能的index失效我們可以試著把查詢做些修改,讓它可以利用到index的好處

select *
from hum_note
where humnt_sdate >= '2008-01-01' and
humnt_edate <= '2008-12-31'

這個query達到的效果和上個query相同,但時間複雜度只有logN (也許挑剔的人會說是2*logN,因為進行了兩次的比對),在這邊要大力呼籲programmer的是,勿在SQL query where條件中,使用任何的function,因為這樣的用法會令資料庫中,賴以增加效能的index失效。 也許有人會問,是不是這樣的用法,我完全都不能用了?我的回答是,如果資料表中的資料是有限的,這個用法倒是無仿。 何謂資料有限的table?像是people_ldap(人員基本資料),department_ldap(部門組織資料),這類的table,如果今天單位內有100個部門,到了明年,單位內會有多少部門?也許105,所以增加了5%,如果今天單位內有1000人,到了明年,單位內的人數會是多少?了不起增加一倍,2000人。何謂資料無限的table?像是hum_wkrecord(每日出勤資料),CardLog(人員刷卡資料)之類的table,這類資料表,其內的資料量,應視為無限大,如果今天單位內有10000個人,hum_wkrecord內有100000筆出勤資料,一年後hum_wkrecord會有多少筆資料? 100000 + 10000 * 365 = 3750000,年增率是3650000 / 100000 = 36.5倍,如果時間複雜度為N,今天出勤查詢需要花費1秒鐘,明年同一個查詢就要花37.5秒了,到了後年,使用者經驗大概會差到無法忍受,也許有人會說,100000筆資料,查詢只要0.001秒,3750000筆也只要花費0.0375秒,user根本感受不出來,是的,或許user感受不到,但效率卻差了37.5倍,如果時間複雜度為logN,37500000資料需要的時間,絕對比複雜度N少,又或許有人會說,不管我的複雜度是N還是logN,過了10年,系統還不是跟蝸牛一樣慢?沒錯,在硬體不升級,資料不斷澎漲的情況下,恐怕是如此情況,這時又帶出另一個議題Archive(資料封存),定期將舊資料,封存至與線上使用分開的database,並針對這些封存資料,提供限制式的查詢,如此可解決資料爆炸造成的效能問題,這個我想是差勤系統在接下來要列入的部份,即使如此,但站在開發著的立場,還是希望系統在當下有限硬體,能夠有良好的效率。 這邊再提一個在差勤資料查詢上,我們經常的sql query用法,如果我想查詢系統中,和@__sdate ~ @__edate有時間重疊的差假單,我會怎麼做呢?以下是我們常用的方式:
select *
from hum_note
where greatest_datetime(humnt_sd,@__sdate) <=
least_datetime(humnt_ed,@__edate)

多麼簡潔明瞭的寫法啊,但存在一個嚴重效能問題: 我們在where條件中,使用了function call,hum_note是資料無限的table,有資料爆炸的問題,因為每筆資料的humnt_sd要做一次處理,humnt_ed又要一次處理,這個查詢式的時間複雜度是N,有沒有什麼方法,可以改寫這個查詢呢? 如果把它改成:
select *
from hum_note
where ( humnt_sd >= @__sdate and humnt_sd <= @__edate ) or
( @__sdate >= humnt_sd and @__sdate <= humnt_ed )


以上達成相同的效果相同(效果有無相同,可以請各位驗證一下,若有不同,請不吝一起討論),但複雜度只有logN。

[結論]
1. 盡量不要在SQL query where條件中,使用function,請優先思考非function的solution
2. Archive是勢在必行