利用 DataContractJsonSerializer 實現物件序列化與反序列化

12/21/2009
DataContractJsonSerializer 類別是 .NET Framework 3.5 的新成員,它可以用來將物件序列化成 JSON 格式的字串,或者執行反序列化,也就是根據 JSON 格式的字串內容來建立物件,並還原物件的狀態。

亂碼 1/2:Unicode 字元不要用 char 型別來處理

12/16/2009
最近在處理一些中文字的問題時,發現一個已經用了兩年多的程式,碰到一些特別罕見的中文字就無法正確執行。當初寫這個程式時,只有測試一般的 Unicode 字元,而沒有考慮到那些特別罕見的中文字。試看這個例子:

string str = "風";
char ch = s[0];
MessageBox.Show(ch.ToString());

不意外,畫面顯示的結果是 "風"。如果用 str.Length 來取字串長度,得到的結果也是預料中的 1。

對於一般常見或「稍微罕見」的中文字,上面的寫法並不會有問題。但如果碰到特別罕見的中文字就會出狀況了,例如這個:

string str = "𩗴";
char ch = str[0];
MessageBox.Show(ch.ToString());

輸出的結果是.....亂碼。



由於 Unicode 的總字數超過 65536 個字元,有些罕見字的編碼會超過 2 bytes,例如此例的「𩗴」(讀作四聲「ㄅㄥˋ」),這個字的編碼佔 4 bytes,若使用 str[0] 的方式取出第一個字元,就會只抓到整個 Unicode 字元的一半,因而出現亂碼--姑且叫它亂碼 1/2 :p (此例的 str.Length 是多少?)

有關 surrogate pairs 和 combining character sequences 的說明,這裡就偷懶跳過,寫程式時只需牢記:在 .NET 裡面,Unicode 字元必須以字串的方式處理

因此,前面的範例應該改成這樣:

string str = "𩗴";
string ch = StringInfo.GetNextTextElement(str);
MessageBox.Show(ch);

StringInfo 類別隸屬 System.Globalization 命名空間,其 GetNextTextElement 方法可以傳回第一個 Unicode 字元(注意型別是 string),或傳回特定索引位置的字元。詳細用法請參考 MSDN 線上文件

註:這篇的 "𩗴" 字,在網頁上輸入時,是寫成:
  𩗴
  這個數字可以經由呼叫 Char.ConvertToUtf32("𩗴") 得到。

在 Windows 7 上安裝 VS2008 SP1 失敗:vsvars32.bat 無法寫入

12/10/2009
終於把家裡和辦公室的電腦升級完畢,分別裝了 Windows 2008 和 Windows 7。Windows 2008 的部分很順利,但是 Windows 7,在安裝 Visual Studio 2008 SP1 英文版的時候卻失敗了。

我到 VS2008 SP1 安裝光碟的 vs90sp1 目錄下手動執行各個安裝程式,順序是:
  1. VC_x86Runtime.exe
  2. WebDesignerCore_KB945140.msp
  3. WebDesignerCore_KB950278.msp
  4. WinSDK-KB946729.msp
  5. WinSDK-KB946733-enu.msp
  6. WinSDK-KB950424.msp
  7. vstor30sp1-KB949258-x86.msp
  8. VS90sp1-KB945140-X86-ENU.msp (主要是這個)
  9. VC90sp1-KB947888-x86-enu.msp
執行到 VS90sp1-KB945140-X86-ENU.msp 時出現錯誤:

Error 32003. File 'C:\Program Files\Microsoft Visual Studio 9.0\Common7\Tools\vsvars32.bat' could not be opened for write.

並且問你要 Retry 還是 Cancel,此時你可以立刻修改 Tools 資料夾的安全性:開啟 Tools 資料夾的內容,切到「安全性」頁籤,讓 TrustedInstaller(或者你目前登入的使用者帳戶)有寫入權限。參考下圖:



檔案權限改好之後,再回到剛才的錯誤訊息對話窗按「Retry」就可以繼續完成安裝程序。

Crystal Reports 的網頁列印對話窗無法列印

12/10/2009
這個問題已經是 FAQ 了,我自己也一再碰到,實在很煩人。

問題描述

環境:ASP.NET + Crystal Reports XI R2 SP4 + IE 8.0

Web 應用程式使用 Crystal Reports 來列印報表,當使用者在 CR viewer 視窗中點列印鈕時,新開啟的列印對話窗會掛掉,裡面一片空白,如下圖:



以下是解決此問題的幾個方法,碰到難纏的狀況時,不妨全都用上。

Web.config 裡面加入設定

參考這篇官方文件:
http://devlibrary.businessobjects.com/businessobjectsxir2/en/en/CrystalReports_dotNET_SDK/crsdk_net_doc/doc/crsdk_net_doc/html/crconfeatureprinting.htm

注意裡面的 http://myserver/PrintControl.cab 要改成你的網站 URL。


[2010-4-19] 註:Crystal Reports XI SP 5 的壓縮檔解開後,在 win32\m151.cab 這個壓縮檔裡面藏有 PrintControl.cab 和 PrintControl.dll。

自己打包 PrintControl.dll

PrintControl.dll 可以在這裡找到:
C:\Program Files\Business Objects\Common\3.5\crystalreportviewers115\ActiveXControls\

你可以利用安裝程式製作工具(例如 InnoSetup)把這個 DLL 檔案包起來,並且在安裝時自動註冊 DLL(利用 regsvr32.exe)。以下是我的 InnoSetup 安裝腳本:

[Run]
Filename: regsvr32.exe; Parameters: /s PrintControl.dll; WorkingDir: {sys}
[Setup]
AppName=Crystal Reports 報表預覽元件 (CR11 SP4)
CreateAppDir=false
DisableProgramGroupPage=true
UsePreviousGroup=false
DisableReadyMemo=true
AlwaysShowComponentsList=false
ShowLanguageDialog=auto
Uninstallable=false
AppVerName=11.0
LanguageDetectionMethod=locale
DisableStartupPrompt=false
OutputBaseFilename=SetupPrintControl
[Files]
Source: PrintControl.dll; DestDir: {sys}
[LangOptions]
LanguageName=Chinese
LanguageID=$0404
[Languages]
Name: ChineseTraditional; MessagesFile: compiler:Languages\ChineseTrad-2-5.1.11.isl

如果要使用上述安裝腳本,可以將它們複製到記事本,儲存成 SetupPrintControl.iss,再以 InnoSetup 編譯即可。安裝程式建立完成之後,便可以放在網站上讓使用者自行下載安裝。

調整用戶端的 IE 安全性設定

即使前面兩項都做過了,ActiveX 控制項還是可能被 IE 擋住而無法執行。而且,就算把「網際網路」區域的安全性都調到最寬鬆的設定(所有 ActiveX 都給它啟用),問題可能還是無法解決。碰到這種狀況,可以試試看把你的網站加入 IE 的信任網站(前兩個方法再加上這個動作才真正解決了我的問題)。

考慮有些使用者可能不知道如何設定 IE 的安全性,也可以寫個小程式或 script 幫使用者自動設定 IE。參考這篇文章的做法:How Can I Add a Site to Internet Explorer's Restricted Sites Zone?


註:Crystal Reports viewer 的 Web 應用程式名稱為 "crystalreportsviewer115",路徑指向 C:\Program Files\Business Objects\Common\3.5\crystalreportviewers115。

《學習要像加勒比海盜》讀後感

11/26/2009

書名:學習要像加勒比海盜 (博客來書店
作者:James Marcus Bach
譯者:洪蘭
出版社:遠流
出版日期:2009年10月30日


看完這本書之後,也許有些人會嗤之以鼻,因為書中對學校教育和文憑社會的現象多有批評。有些人可能心有同感,覺得受到激勵和啟發。我是屬於後者(一部分原因可能跟我研究所念了五、六年都還沒畢業有關 XD)。

這是一本談學習方法的書,但跟我之前看過的,討論讀書方法、如何考試拿高分、或如何迅速記憶之類的書不太一樣(作者是反對速讀的)。這本書裡面沒有什麼教條、理論,內容主要是作者 James Bach 描述自己「學習如何學習」的一些過程和真實故事,以及自行歸納整理出來的學習觀念和方法。同時,你會看到很多關於「我」的想法、行為、經驗等等的描述。書中充滿了作者對自我學習歷程的觀察與剖析,有些細微之處描寫得相當到位,而且字裡行間可以感受到作者那份與人分享學習奧秘的熱誠,這是我喜歡這本書的主要原因。儘管很多想法都是以比較主觀的方式描述(我覺得怎樣怎樣,我會怎麼做.....諸如此類的),但是跟那種刻意用被動式、隱藏主詞來強調客觀性的寫法,我更喜歡作者直截了當的表達方式。

關於學校教育
我不會讓學校阻擾我的教育。  -馬克吐溫
I will never let school interfere with my education.  -Mark Twain

作家黃春明(求學過程也很不順遂)的推薦序裡面有這麼一段:
目前的學校教育,就像過去勞力密集的加工廠,老師們就像坐在生產線上輸送帶兩旁的女工,學生就像由輸送帶送出來的電器商品的基座(chassis),什麼都還沒裝上。

他們從幼稚園一直到大學,經過這一條生產線,由沿途的老師拼裝電器零件,並做電流電壓等各種測試,不合規格的剔出線外,符合規格的看它是哪一所學校畢業出品,台大的就貼上台大牌,政大的就貼上政大牌。
儘管同意這樣的說法,但如果抱著「反學校」的心態去讀這本書,只是為了替自己的學習怠惰找藉口的話,恐怕就辜負作者的心意啦。每個人的資質、後天環境都不一樣,所以,要小心這樣的念頭:「你看,作者是個中輟生,高中都沒畢業,最後還是備受業界和學界肯定,甚至有人稱他博士。何必一定要拿到那張文憑呢?」

學校教育是一種學習的管道,也許有的人適合,有的人不適合玩這樣的(文憑)遊戲。但如果我們不想「玩別人的遊戲」,那就要有心理準備,可能要付出更多努力和更大的代價(與多數價值觀衝撞),找到自己的學習出路,做一個真正的 self-educationer,或書中所說的「海盜學者」(buccaneer scholar)。

海盜式學習

加勒比海盜是個比喻,描述我們有些人的學習,它給我們這些有著喜歡流浪心智的人一些安慰和鼓勵,我們的心是沒辦法不流浪、不漂泊的,有人把它稱為「注意力缺失症」(attention deficit disorder, ADD),但它不是,它比較像無法停止的好奇心。 (p.46)
有時候,我心裡會有一股不安,或罪惡感,因為某件答應別人的工作,明明已經接近 dead line 了,可就是打不起勁,或者卡在某個問題而無法前進。打不起勁的時候,我都在做什麼呢?我到線上書店四處晃,看別人的 comment、找自己感興趣的書,然後下單買回來看,這一看可能兩三個星期就過去了。又或者,「找」幾部電影來看,發現中文字幕翻譯得真糟糕,便自己動手改。諸如此類的,似乎只要能讓自己暫時逃避那個「該死該做的工作」就好了。

我以我的智慧謀生,但不太講求自我紀律。我必須聽從內自的韻律去學習和思考,才可能成功。我不能強迫我的思考去適應別人認定的思考模式。 (p.100)
除了拖拉的毛病,我還容易分心。我買的書很少有全部看完的(金庸小說除外),有時一次就買好幾本,可是就東看西看,一本書才看了一點,覺得好像沒什麼意思了,就換另一本。因此,有時會懷疑自己的注意力無法集中,無法持續一段夠長的時間來提升工作和學習的效率,並為此感到不安。我曾把完成一項工作或學習想像成開車前往一個目的地,別人可能是專心開車,一路直奔目標,我卻老是心有旁騖,看到路邊有什麼新鮮好玩的,就停下來看一下。(不知道這跟我唸書時,人坐在教室裡,心思卻經常跟著窗外小鳥到處飛有沒有關係 XD)

然而,從作者描述的一段親身經歷--撇下手邊工作,跑去退潮的沙灘研究蛤蠣--才發現或許我不用太過擔心。在陷入困境,或者心智想要來一場小小的叛變時,不妨就先脫離當時的情境,將注意力轉往別的地方,但前提是要從事有益學習的心智活動,或者休息(例如睡覺)。也就是說,儘管在某個地方停下腳步,但仍不要停止學習。像我那樣放著工作不管,連續看好幾個小時的電影來麻痺自己,恐怕就不太健康啦!

犀牛與小狗

Bach 在書中用了犀牛和小狗來比喻心智叛變或分心的強烈程度。當心智像犀牛一樣衝到餐廳吧檯上大快朵頤,我們所要做的是去引導它,而非把它強壓下來。當心智變得比較溫馴,像隻小狗,只是沿路上偶而到處嗅嗅,則不妨把狗鍊稍微放長,讓它接觸一些不同的東西,只要別像脫韁野馬就行了。這是作者在書中提到的「長皮帶經驗法則」:

長皮帶表示我鼓勵心智漫遊,但是每十分鐘或每小時,我會拉緊皮帶,把狗兒叫回來工作。......長皮帶的重點在允許分心:我培養負責任的分心。 (p.124)
觸類旁通隨時學習

即使是看電影,如果能從其中獲得有用的東西,或找到有助於解決問題的靈感,似乎也沒什麼不好。海盜學者就是要做任何事都能從中吸收一些對自己有用的東西,如作者所說:

學習任何東西,即使是蚌類,都會延伸和幫助我未來學習其他的主題或領域。新的知識如果能找到舊的知識架構做連接,它會依附得比較牢固 (p.111)
作者建議的學習方式,不是一條學習路線完成後,再處理另一條,而是比較像現在的 Internet,像蜘蛛網那樣,不斷向周圍連結延伸出去,並逐漸填補知識拼圖中空白的部分。擴散的速度也許比較慢,但卻能從周邊接觸的各類知識,豐富自己的專業領域。這是海盜學者的另一個特色:對於學習的範圍,從不畫地自限。

一個海盜的教育是不受傳統學科的範疇侷限的,我們直穿越這些學科的界線。這使我們有更豐富的點子,使我們在各個不同領域上成功,使我們不容易被別人的聲勢或氣燄所壓抑而顯現出膽怯的樣子。 (p.53)
所以,看電影也是可以學到東西的呀。我又為自己找到一個看電影的正當理由啦 XD

小結

一條路走不通,就試別條路。一本書看不懂,就換別本,不用一味責怪自己愚笨;也許是自己尚未具備足夠的基礎知識,也可能是作者的表達方式跟你腦袋裡的天線接收頻率不合而已。同樣的,如果覺得學校的課程或教學方式不適合自己,就想辦法找到自己的學習方式,自己教育自己。要成為海盜學者,需要的不是過人的天資,而是不斷學習的熱情、好奇心、以及一股不服輸的精神。

最後想提一下,前教育部長曾志朗在推薦序中的一段話:

「作者一路排斥制式教育的安排,但當他在電腦程式測試管控的領域上,有了不凡的造詣之後,他卻又非常傾心於這些制式學府對他成就的肯定,......好像只有回到那個舞台,他才會感到自我實現的尊嚴!這會是自學成功者的宿命嗎?」

有點悲哀,但答案似乎是肯定的。
==========================
對中文版的一點牢騷:譯者把 heuristic 翻譯成「捷徑」,讀起來挺彆扭的。我每次碰到就將它替換成「方法」、「法則」、「經驗法則」、或「啟發」。

相關文章

安裝 Team Foundation Server 2010 Beta 2 真的只要 Next、Next....

11/22/2009
微軟官網於 2009/10/21 發佈了 Team Foundation Server 2010 Beta 2,下載回來嘗試安裝看看...安裝步驟果真變得簡單多了。

我的作業環境是 Windows Server 2008(有 IIS),在安裝 TFS 2010 之前,還需要安裝 SQL Server 2008。基本上整個安裝過程就如 Channel 9 的教學影片所示範的一樣順利,這裡也有圖文並茂的安裝指南:

Team Foundation Server 2010 Beta 2 Installation Guide (也有 pdf 版

一點筆記:
  • 只要 IIS 和 SQL Server 2008 等必要元件都有事先裝好,安裝步驟就很簡單:Next、Next、Next....就裝好了(中間可能會經過一次重新開機)。
  • TFS 的建置包含兩個部份:安裝,以及設定組態。所以安裝完成後,最後一個步驟會幫你開啟 Team Foundation Server Configuration Wizard,以便接著進行組態設定工作。這個程序幾乎也是點 Next、Next、Next....Configure、Finish,完全不用勞駕茶包射手。比起前一版,真的是容易多了。

比較大的問題恐怕是....裝起來之後,如何說服老闆用啊?! >_<|||

什麼是 Test Oracle?

11/01/2009
審稿時發現譯者有一句沒有譯出來:

A test oracle is a source of expected test results for a test case.

可能是因為當中的 "test oracle" 不太好處理吧。

Visual Studio 2010 與 .NET Framework 4 Beta 2 開放下載

10/24/2009

使用 Oracle 資料庫儲存 ASP.NET Session State

10/23/2009
速記一下使用 Oracle 資料庫儲存 ASP.NET Session State 的方法(我的資料庫是 Oracle 10g)。

C# 4.0:Covariance 與 Contravariance 觀念入門

10/19/2009
.NET Framework 4.0 進一步支援了泛型(generic types)的 covariance 與 contravariance 型別參數,這兩個術語有點抽象,不是很容易說明白。這裡嘗試用一些簡單的例子來說明相關的基礎概念。

將 ASP.NET 網站移轉至 IIS 7 時碰到的問題

9/28/2009
總算有機會把原本在 IIS 6 上面跑的 Web 專案移轉到 Windows Server 2008 + IIS 7 環境,但部署完成後,瀏覽網站時卻發生錯誤:

Request is not available in this context.


Framework Design Guidelines 筆記 (4): 低門檻原則

9/23/2009
關鍵詞彙
  • Framework(框架;類別庫)
  • The principle of low barrier to entry(低門檻原則)
低門檻原則指的是:
框架必須提供一個較低的入門門檻,讓初學者可以透過簡單的小實驗來學習這套框架。 (p.21)
然而,一套類別庫要做到功能強大、完整,同時又要易學易用,卻不是那麼容易。像 .NET Framework 這類比較大型的類別庫,裡面的類別非常多,為了方便學習、查找,就一定得要有適當、合理的分類和命名,否則可能寫了好多好用的工具,卻藏在類別庫中某個很深層的角落沒人發現。這分類的關鍵,主要就在 namespace 的運用。

因此,若要設計出容易學習的 API,應做到下列幾點(p.22-23):
  • 平常寫程式經常用到的類別要能很容易找到,而且成員的數量要適中。比如說,一個 namespace 裡面包含 500 個型別,結果其中只有少數幾個是真正關鍵或常用的。這種齊頭式平等的分類方式不僅不易學習,在撰寫程式的時候也多少會帶來困擾(IntelliSense 一次列出一拖拉庫的成員清單)。
  • 要讓開發人員隨手捻來就能立即使用。如果使用某個類別之前,還得先建立其他類別的 instances、設定一堆必要的屬性、以及寫很多初始化的程式碼,那麼,初學者為了寫一點實驗用的小程式就得大費周章,可能試個幾次就打退堂鼓了。
  • 要很容易找到錯誤的原因,以便迅速解決問題。例如,API 應該儘量考慮可能發生的 exceptions,並且在例外發生時丟出清楚的錯誤訊息,讓程式設計師能夠從訊息來判斷哪裡出了問題。
關於最後一點,我個人的感受特別深,因為印象中我總是經常重複這樣的牢騷建議。十年前碰到隱藏錯誤或訊息曖昧不明的情況,十年後似乎也沒有太大的改變,還是得不斷拜託寫底層元件的人不要再丟出「發生錯誤了!」這種只有神才知道原因的訊息。(更別提 catch 區塊裡面什麼都沒有的寫法)

據我瞭解,支持「精簡錯誤訊息」的一個主要考量是:end users 根本看不懂那些底層的詳細錯誤訊息;顯示太詳細反而造成使用者的困擾。

我想問題的關鍵在於:錯誤訊息是有分類型或等級的。一般使用者的操作錯誤,應用程式自然很容易顯示最精簡、易懂的訊息(例如:請先選擇這個,再按那個),那是比較偏向 warning、information 之類的告知訊息。而寫底層 API 的設計師要處理的卻是比較特殊的「例外狀況」,這些例外狀況所引發的錯誤訊息雖然對 end users 來說沒太大用處,但卻是維護人員接到電話或 e-mail 時一個非常重要的線索。把這個線索隱藏起來,就像鴕鳥把頭埋進沙堆一樣,對解決問題真的一點幫助也沒有。

詳細描述錯誤訊息只是設計 API 的最基本要求而已,Chris Sells 認為,好的錯誤訊息除了描述 what,還要提示如何解決:
In my own programming, I dearly love error messages that say what I did wrong and how to fix it. All too often, all I get is the former, when all I really care about is the latter. (p.23)

相關文章

審校碎碎念(二):靈活的翻譯?

9/20/2009
例 1:These techniques do not replace other approaches to software testing but can significantly increase testing effectiveness, help identify and reduce redundancy, and also reduce the susceptibility of the testing effort to the pesticide paradox.

Greenfield 是綠海還是綠地?

9/08/2009

Framework Design Guidelines 筆記 (3) : 情節驅動設計

9/04/2009
這是 Framework Design Guidelines 2nd edition 筆記的第 3 篇,基本介紹和表示法請參閱第 1 篇第 2 篇
關鍵詞彙
  • scenario-driven design(情節驅動設計)
  • use cases(使用案例)
  • test-driven development, TDD(測試驅動開發)
以下開始內容摘要,我自己的話用暗橘色標示
==========================================
情節驅動設計的原則

我們建議框架設計師先就框架使用者(按:我們常說的 app programmers)所欲處理的主要情節來撰寫程式碼,之後再根據這些範例程式碼來設計合適的物件模型。

此做法有點類似 TDD 或使用案例驅動的設計方式,但 TDD 算是比較重量級的方法,因為它除了驅動 APIs 的設計之外,還有一些別的目標;使用案例則是從較高的層次來描述情節,而不是從個別 API 呼叫的角度來思考。(scenario、use case、requirement、function,這幾個名詞也許是因為涵義相近或有重疊之處,翻譯成中文時常見有混用的情況,例如把 scenario 稱作「需求」,把 use case 稱作「功能」等等,如此固然「平易近人」,在某些場合卻可能過於含糊籠統。作者在書中對 scenario 和 use case 有比較明確的界定,閱讀時不妨留意一下。)

框架設計原則

設計框架的第一步,必須先從一組使用情節,以及實作這些情節的範例程式碼開始。

Krzysztof Cwalina:對此原則,我有句話要補充:「想要設計一個好框架,除此之外沒別的辦法。」如果這本書只能介紹一個設計原則,我一定會選這個。

框架設計師經常犯的錯誤,是先以各種設計方法論來設計物件模型,然後為這些設計好的 API 撰寫範例程式。問題是,大多數的設計方法(包括物件導向設計)都是以可維護性為主要考量,而未著重在 API 的易用性;它們非常適合用來設計僅供內部使用的程式架構,但不適合用來設計大型框架的共用 API 層。
  • DO 先為主要情節撰寫範例程式碼,然後再從這些範例程式發展出對應的物件模型。
舉例來說,假設要設計一個 API 來計算某段程式的執行時間,你會先寫出類似下面的情節程式範例(有點像虛擬碼,注意裡面同時有 C# 與 VB 語法):
// scenario #1: 計算執行時間
Stopwatch watch = Stopwatch.StartNew();
DoSomething();
Console.WriteLine(watch.Elapsed);

// scenario #2: 重複使用 stopwatch
Dim watch As Stopwatch = Stopwatch.StartNew()
DoSomething();
Console.WriteLine(watch.ElapsedMilliseconds)

watch.Reset()
watch.Start()
DoSomething()
Console.WriteLine(watch.Elapsed)

然後,根據這些程式碼發展出以下物件模型:
public class StopWatch
{
public static StopWatch StartNew();

public void Start();
public void Reset();

public TimeSpan Elapsed { get; }
public long ElapsedMilliseconds { get; }
...
}

也就是說,情節驅動設計就是從 API 使用者的角度出發,先想想看會怎麼用這些 API,並寫下範例程式碼。這些範例程式碼等於是 API 物件模型的需求規格,而接下來的工作就是根據這些範例程式碼來設計符合該需求的物件模型(類別)。

這種設計方式,也可以說是由上而下,從外(外部需求)到內(內部實作)吧!

相關文章

.NET Framework 4.0 新功能:Code Contracts

9/03/2009
MSDN Magazine 2009 年 8 月號CLR Inside Out 專欄主題是 .NET Framework 4.0 的新功能:Code Contracts,由 Melitta Andersen 執筆。正如其名稱所揭示的,這項新功能的目的即在於進一步支援合約式設計(design by contract)的概念。


Framework Design Guidelines 筆記 (2) : 漸進式框架

9/01/2009
摘自:Framework Design Guidelines 2nd edition by Krzysztof Cwalina & Brad Abrams
Key Terms
  • progressive framework: 漸進式框架
表示法
  • DO: 表示強烈建議、務必遵守的設計原則。
  • CONSIDER: 建議考慮這麼做。
  • DO NOT: 禁止事項。
  • AVOID: 儘量避免的做法。
=============================
  • DO 設計框架時要兼顧功能強大與容易使用。

    如 Alan Kay 所說:「簡單的東西就要有簡單的設計,而碰到複雜的情況時也應該要能應付。」(Simple things should be simple and complex things should be possible.)
  • DO 設計時應考量廣大開發人員的各種程式設計風格、需求、以及不同的技術水平。
  • DO 設計時應考量支援各種程式語言。
Krzysztof Cwalina:設計框架給跟你相似的使用者是很簡單的,而要設計給跟你不同的使用者就非常困難了。許多 APIs 都是由領域專家所設計,但老實講,只有領域專家覺得它們好用。

Brad Adams:我們在設計 APIs 時往往只考慮到自己,而非客戶的需要。
漸進式框架

一套框架要能同時兼顧各類開發人員、需求、和程式語言,是相當困難且昂貴的事業。以往,開發軟體元件的廠商是針對特定開發族群和特定需求來提供多種解決方案,例如微軟的 Visual Basic、MFC、ATL 等等。

這些特定用途的 APIs 雖然強大好用,卻有個主要缺點:當開發人員要從目前的技術領域移向另一塊領域時,學習曲線會非常陡峭,因為那等於是要學習另一套新的東西。

比較好的解決方式,是提供所謂的漸進式框架(progressive framework)。這種框架的特色是兼顧各類型的開發人員,且適用範圍較廣,從比較簡單的需求到進階的應用都能應付。

.NET Framework 即屬於漸進式框架,它有單一的程式撰寫模型,支援廣泛類型的應用需求與技術層次,學習曲線也較為緩和,能夠由簡入繁、循序漸進。

相關文章

Framework Design Guidelines 筆記 (1) : 優良框架的特徵

8/31/2009

小引
網路上有不少人推薦這本書,我也覺得是本內容紮實的好書。原以為應該很快就能看到中文版了,但一直到第二版都沒看到影子(中國大陸有出第一版的簡體版)。我想,這多少是因為從書名來看,它似乎是專門寫給 framework designers 看的,目標讀者的市場太小了。

其實,在目前的程式設計環境,開發人員多少都得碰觸到共用類別/元件的撰寫,即便是元件的使用者(或 app programmers),瞭解好的程式寫法與設計原則,對提升軟體品質也大有幫助。

另一個原因,也許和書中所提的類別設計原則與程式寫法(如命名規則、exception 的處理建議等)比較細緻有關。撰寫一般的 app code 時,多以結果導向,即程式能跑、功能正確就行,至於 end users 看不到的程式碼,自己想怎麼寫就怎麼寫。尤其在專案時程很趕,與時間賽跑的情況下(哪個案子不是這樣?),哪還有閒功夫去琢磨:這個變數怎麼命名比較好、那些 code 是否要抽離成共用類別或函式、有哪些 patterns 可以 reuse 呢?

但我一直覺得,就像人品一樣,程式碼的品質會反映在軟體系統的外在行為上,使用者必定能感受到。也許他們看不到程式碼寫得怎麼樣,但是操作介面難用、執行效能差、不易擴充(要求稍微改一點東西就說程式很難改)、大小 bug 不斷...等等,都顯示出軟體系統的內在(架構與設計)出了問題。

我打算節譯部分內容,放在部落格上。一方面作筆記,一方面也向大家推薦這本書。至於會寫多少篇,倒也沒認真去想,若有時間的話,看到哪兒就寫到哪兒吧。

以下開始內容摘要,我自己的話是用暗橘色標示

優良框架的特徵
  1. 優良的框架一定很精簡

    大部分的框架都不缺功能,因為只要需求明確,增加功能就很容易。
    另一方面,當開發過程中出現時程壓力、功能蔓延(feature creep)等問題,或想要滿足每一項枝微末節的需求時,簡單性(simplicity)通常就會被犧牲掉。
    如果多考慮一下設計的複雜性,你會發現,把目前版本的一些功能砍掉,並將時間花在思考下一個版本該如何設計才適當,這麼做通常會有比較好的結果。
    如框架設計師常說的:「功能隨時可以加,但要將它移除可就難了。」
  2. 優良的框架須付出較高的設計成本

    框架設計應該是開發流程中的一項明確且專屬的工作,因為這項工作需要謹慎規劃、組織人力、並有效執行,它不應流於實作過程中的附屬產品。

    (問題是有多少軟體公司願意聘用專職的 framework designer?看他好像閒閒沒事在看書的時候,可能還是忍不住叫他去打雜吧。正所謂物盡其用..... >_<|||)
  3. 優良的框架充滿權衡取捨

    沒有所謂「完美設計」這種東西。設計其實就是一連串的權衡取捨,然後做出正確決定。你得瞭解各種選項的優點和缺點。如果在設計時都沒有碰到需要取捨的情況,那不是你發現銀彈了,而是可能很多重要的地方都沒考慮到。
  4. 優良的框架會善用既有成果
  5. 優良的框架能持續演進
  6. 優良的框架易於整合
  7. 優良的框架具備一致性
===================
說到設計的簡單性,還真的不簡單。
碰到需要處理大量資料的情況就直接開個超大陣列,這種寫法簡單嗎?簡單,而且通常過於天真。

我曾看過一個 .NET 類別庫裡面有個工具類別提供一個叫做 CreateDirectory 的 method。好奇它跟 System.IO.Directory 的 CreateDirectory 有甚麼不同,挖出原始碼一看:

public void CreateDirectory(string path)
{
  If (!System.IO.Directory.Exists(path))
  {
    System.IO.Directory.CreateDirectory(path);
  }
}

這函式設計得簡單嗎?簡單,但是多餘。這種連雞肋的價值都談不上的 method,除了增加類別庫的體積和重量之外,別無益處。

也許設計者希望使用此類別的程式設計師少寫一些 code,但問題是,當欲建立的目錄已經存在時,System.IO.Directory.CreateDirectory 本來就不會發生錯誤,何必多此一舉呢?

元件或框架設計師必須非常瞭解基礎類別庫的行為,才不會為了提供方便的工具而重新發明一堆輪子。增加 methods 時若未審慎考量,一旦使用類別庫的程式設計師用慣了,專案中到處充斥這些 method calls,將來要拿掉可就麻煩了。

請神容易送神難。框架設計師豈可不慎?

相關文章

審稿碎碎念:有審校人員把關,譯稿的品質會更好還是更差?

8/30/2009
有審校人員把關,譯稿的品質會更好還是更差?
類似的問題:有了測試人員,程式碼的品質會更好還是更差?

[C#] 泛型 = 樣板?

8/11/2009
Eric Lippert 的部落格看到一段挺有意思的 C# code,以下是我稍微修改過的版本:

《玫瑰的名字》閱讀筆記 3

7/04/2009
上一篇筆記一樣,這篇也是記錄我讀《玫瑰的名字》時碰到的怪句。
有時候,書上的每個字都認得,兜在一起卻完全看不懂。在《玫瑰的名字》裡面就有這麼一段,反覆看幾遍還是不懂:

《玫瑰的名字》閱讀筆記 2

7/03/2009
都說不雞蛋裡挑骨頭了,還是忍不住..... >_<|||

《玫瑰的名字》閱讀筆記 1

7/02/2009

書名:玫瑰的名字
作者:Umberto Eco/著
譯者:謝瑤玲
出版社:皇冠
出版日期:1993年09月15日


這本小說的中文版已經出版超過 15 年了。在網路上看到推薦文,便將屢屢啃食不下的《罪與罰》先晾在一邊,改看這本。

SQL Server Connection Forcibly Closed

7/02/2009
又一個有點麻煩的問題:在 server farm 環境下,利用 SQL Server 資料庫來儲存 ASP.NET 網站的 sessions,可就是有某一台 app server 會三不五時(每週一兩次或者完全沒有)出現無法連接 SQL Server 的錯誤訊息:
TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host

當此問題發生時,我的伺服器當機偵測程式會發送 MSN 通知和 e-mail 給我。麻煩的地方在於,如果在收到通知訊息時立刻用瀏覽器開啟出問題的 app server 的網頁,卻又發現是正常的。也就是說,這個問題既不定時出現,出現的時間也很短暫,很難將它「鎖定」。

在 MSDN 論壇上找到一帖相關討論:
http://social.msdn.microsoft.com/forums/en-US/sqlnetfx/thread/4895d56b-716f-4f82-860f-0aa161d327cc/

看來還挺多人碰到這種怪問題,但是對發生的原因仍莫衷一是,建議的解法也五花八門。大略爬完這一長串討論之後,打算嘗試兩種解法。

解法 (1):修改 SQL Server 的遠端登入逾時時間

如下圖所示:



此解法似乎有點駝鳥心態:反正可能是某種神祕未知的網路傳輸或連線資源管理問題所致,而且出現的時間很短暫,過幾十秒就恢復了,那就乾脆延長逾時時間,把這些可能發生的短暫延遲給壓下去。

解法 (2):關閉 Connection Pooling

這是我最不希望的做法(因為會影響效能),但不得已時也只能試試看了。

結果

先採用解法 (1) 之後,經過一個月了,一直都沒有再發生同樣的問題。

那就樂得當個鴕鳥吧 :)

後記:結果後來終於找到真正原因,說出來還真的很不好意思....原來,SQL Server Agent 的排程出了問題,無法正常執行定期刪除 ASP.NET session 的工作,以至於 session 資料表持續累積、長大,大到令我難以想像的地步,因而影響了 SQL Server 的運行。

天堂的階梯(The Good Death)

6/21/2009
偶然在延伸的網路連結中發現此書,書名 The Good Death(好死)頗吸引我,於是從圖書館借了一本。這本書詳細描述了多位病人臨終前的痛苦經歷,才看完第一個個案,便感受到很大的衝擊。當然,生病沒有不辛苦的,但書中描述的個案,看了實在是令人不忍......怎一個「慘」字了得!同時也才稍稍了解,當活下去已經沒有任何意義,連尋死都成為一種奢求時,是多麼的悲哀。

這些發生在美國的真實案例,有的是因為罹患癌症、愛滋病等疾病,有的則是因為用藥不當或車禍等意外而導致失去行為能力的病人。由於當時(大約 1970 年之前)醫界與一般民眾對於安寧療護、疼痛控制的認知還相當貧乏,對臨終病人與家屬能否決定拔管(呼吸器、鼻胃管)也存在歧見,使得那些重症病患在往生的前幾個月,不僅自己痛苦,家人也承受了極大的身心煎熬;有的家屬甚至奮戰近十年,與各種團體和政治勢力對抗,才促使美國政府制定相關法令,包括預立醫囑的合法性、將「全腦功能停止」納入死亡的定義(原本僅有心肺、呼吸功能停止)。作者透過採訪當事人所作的記錄,如實呈現臨終病人的心理變化與種種痛苦,同時對安寧療護、使用止痛藥與麻醉劑來控制疼痛的劑量如何才算適當、醫生的權威、病人與家屬自決的權利、安樂死等牽涉道德與法律層面的問題也多有探討。這才發現,原來這當中牽涉的問題是如此複雜:當我躺在病床上無法表達意見時,誰來決定我的生死?何時才應該放棄維持器官運作的治療?萬一還有一線生機呢?誰來執行(醫生也會擔心被告)?病人或家屬有權利不斷提高止痛藥的劑量嗎?

安詳離世:茱迪絲的旅程

書中的第一個案例,茱迪絲,因罹患乳癌而切除乳房,中間歷經數次化療和放射線治療,也試過各種非正統的醫療方法,如禪修冥想、印地安巫醫的召喚聖靈療法等(人到無可奈何時總會求助各種偏方),最終仍不幸離世。幸運的是,她最後選擇了與勇敢面對死亡,放棄進一步治療,而在家中與親人共同度過最後一段寧靜而美好的時光。就如茱迪絲的丈夫所說:

「很顯然,我的妻子過世了,但對我們所有人來說,她死去的方式是那麼強而有力、獨特非凡。它讓我體悟到,死亡是一段旅程,而茱迪絲的旅程是美好的。......我唯一能說的是,對於我、賈斯汀和希瑟莉而言,茱迪絲的死讓我們深覺自己何其有幸,也何其不幸。」

現代醫學訓練教些什麼?

作者在第二章討論了一些醫病關係與醫學倫理的議題。例如她提到,原本死亡是人生的最終結果,但是在醫生的手中死亡,卻似乎成了醫生的失敗或過失。「因此之故,教學醫院訓練年輕醫師不顧成功機率的多寡,盡可能去嘗試每一種賭博。他們一次只把焦點放在一個器官上,專科醫師的狹窄見識使他們不去質疑,一個經過修復的心臟,能否與一個受損的肺臟一起正常運作;或者,對於一個纏綿病榻、承受極大痛楚的人,或肺臟、腎臟衰竭的人來說,修復修臟有什麼幫助?」(p. 76)

原來,醫學技術的進步,不止延長了人的生命,也同時延長了病人的痛苦,以致於在這個國家,「死亡竟是如此艱難」。

艱難的休止符:只想從疼痛中解脫

第二個案例,彼得,是一名罹患愛滋病的同性戀者。當時(1986 年),人們對愛滋病幾乎一無所知,對愛滋病所引發的各種疼痛,包括頭痛、腹部絞痛、以及最棘手的神經病變引發的疼痛,也鮮少有醫生知道該如何給予適當的治療。經過一段時間,彼得終於認清自己的處境,有一天,他對他的伴侶朗恩說:「嘿,這病是一天天節節進逼。它現在在我的脊椎裡,一路往上進攻,等到它攻進橫膈膜,我就一命嗚呼了。朗恩,我們得談一談。我再也不想回醫院去了。」

彼得後來如願進入安寧病院(hospice)。剛開始,病院的醫師准許彼得日以繼夜的服用口服嗎啡,彼得總算有一段比較輕鬆的日子。但是到後來,疼痛愈加劇烈,他一而再,再而三的索求止痛藥,連護士和醫生都已經厭煩他的「討藥」行為,認為他對嗎啡已經上癮了,甚至有無病呻吟的嫌疑。朗恩回憶:

「當我坐在床邊,陪伴逐漸油盡燈枯的彼得時,聽到他一再懇請、拜託、哀求醫護人員給他更多嗎啡--即使當時已經每小時靜脈注射四十或五十毫克的嗎啡溶液--直到他再也無法言語為止。即使在那個時候,我還是聽到醫護人員說不。彼得終於放棄希望,不再哀求。當他呻吟時,我撫摸他的額頭,當他嘆息時,我握住他的手。他的疼痛似乎未曾稍停,除了生命終了的最後時刻。」(p.109)

真的是痛到死!

彼得過世後,當時負責治療他的醫生反問:「何謂適當醫療?難道是指醫師隨著病人的意思照辦嗎?還是讓病人服用麻醉藥物到飄飄欲仙?如果這才叫作適當的醫療,何必浪費良藥?......我們遇到一些病人要求高劑量的藥物,因為他們一心想死。我們不幫助病人安樂死。」

另一位醫生持不同看法:「對於一個垂死的愛滋病人而言,每小時五十毫克的靜脈注射嗎啡通常是不夠的。所謂足夠,乃是指舒解病人疼痛的劑量。『相信病人』是良醫良藥的首要原則。」也就是說,疼痛是主觀的,病人喊痛就是痛,應盡量給予治療。若懷疑病人過度索求止痛藥而不給予治療,這樣的醫師......病人碰到了恐怕只能自求多福。

到底該不該拔管?

第三個案例,凱倫,一個昏迷十年之後才過世的年輕女孩,她的父母為了讓她早日脫離人間煉獄,不惜和醫院打官司。而凱倫在生日時向朋友吐露的心聲,竟成為日後打官司時有力的證詞。當時,凱倫對朋友表示,如果有一天她回天乏術,活著只會延長痛苦折磨,她寧願死。

儘管美國最高法院最後判定醫院應依照凱倫父母的意願,拔除凱倫的人工呼吸器。但醫師還是不肯照做,只願意採用逐漸讓凱倫試著不依賴人工呼吸器一段時間的方式。結果等到正式將人工呼吸器關機時,凱倫卻奇蹟式的能夠自行呼吸了。這個「好消息」讓凱倫和家人陷入更長的磨難,直到昏迷十年後才得以解脫。

「醫師指出,事實上,當餵食管或靜脈注射點滴被移除,或停止洗腎,死亡就不那麼可怕或痛苦。如果能夠妥善控制,拔除所有的插管,身體將慢慢地停止運作,自然而然地進入自我麻醉的睡眠狀態,病人便能平靜安詳地死亡。」(p.162)

病人自決

在凱倫的案例中,法院後來雖然允許其父母拔除凱倫的餵食管,但他們卻一直沒有這麼做。而本書第四個案例的主角--南西--卻在其父母欲拔除其鼻胃管時引發激烈爭議。甚至有牧師帶頭策劃「營救行動」,欲強行進入醫院幫南西重新插上鼻胃管。他們的標語是「你想嚐嚐被餓死的滋味嗎?」「當所有美國人在聖誕節前大祭五臟廟,南西卻活活挨餓,脫水至死。」

歷經宗教團體、醫界、反墮胎委員會等各方政治角力,南西的鼻胃管終究被拔除了。而這些爭議也催生了各種臨終法的制定,包括「生前遺囑」、「醫療委任代理人委託書」、以及「家屬同意書」。這些法案的內容我並不清楚,但從字面上可大致了解,其核心概念就是要尊重病人對自己醫療方式的決定權。

這本書出版於 1999 年,距今已十個年頭,而且談的是美國的情況,不知道台灣是否有制定類似的生命臨終法案?萬一將來有一天我成了活死人,身上插滿各種管子,無法言語時,我應該也會希望盡快解脫,也免除家人的身心折磨吧。

ps. 本書已絕版,目前大概只能到圖書館或二手書店才找得到。

使用共享的 AssemblyInfo 讓多個 .NET 專案的版本保持一致

6/14/2009

如果你希望某個 solution 包含的全部專案都建置成同一個版本編號,可依下列步驟進行:

  1. 編輯一個共用的組件資訊檔(Assembly Information File),命名為 SharedAssemblyInfo.cs(假設這是個 C# 專案)。此檔案的內容只包含版本資訊,例如:

    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;

    [assembly: AssemblyVersion("1.8.3122.0")]
    [assembly: AssemblyFileVersion("1.8.3122.0")]

  2. 將此 SharedAssemblyInfo.cs 以 Add As Link 的方式加入 solution 中的各個專案。作法為:在 Solution Explorer 中的某個專案名稱上點右鍵 > Add > Existing Item...,在新開對話窗中選取 SharedAssemblyInfo.cs,然後,不要直接點 Add 鈕,而是點 Add 鈕旁邊的向下箭頭,此時會出現 Add As Link 選項,點它。每個專案都如法泡製。這個新加入的項目預設會放在專案的根資料夾下(只是個連結設定,根目錄沒有實體檔案),你也可以用滑鼠拖曳的方式,在 Solution Explorer 中將它移動到專案的 Properties 資料夾,和預設的 AssemblyInfo.cs 放在一起。
  3. 開啟每個專案原本已經存在的 AssemblyInfo.cs,將原有的 AssemblyVersion 和 AssemblyFileVersion attributes 刪除(若不刪除,編譯時會發生重複 attribute 的錯誤)。
以後要發行產品時,只要修改 SharedAssemblyInfo.cs 裡面的版本資訊,然後 rebuild 整個 solution 就行了。

這是在不使用額外工具的情況下,還算簡單的解法(至少比每次發行產品之前逐一修改各專案的 AssemblyInfo 要方便迅速多了)。如果你需要對 AssemblyInfo 的內容做更複雜的控制,可以試試 MSBuild Community Tasks 裡面的 AssemblyInfo task。

也來噗浪!

6/08/2009

申請推特(Twitter)和噗浪(Plurk)帳號已有一段時間,但並沒有花太多時間去寫這類微型網誌。我比較喜歡 plurk 的展現方式,以後也許會比較常在我的 plurk上碎碎念吧!

網誌右邊也加上了 plurk 區塊,可以隨時看到兩則最新的噗浪訊息。

寄送郵件的封包被防火牆檔掉

5/31/2009
記錄一個困擾很久(半年以上),最終由網管人員解決的問題:
應用程式利用 .NET 類別來發送郵件,大多時候沒有問題,可是偶爾使用者會反應沒收到信件。從應用程式的 log 發現 exception 訊息為:遠端伺服器已經中斷連線。

這種偶爾出現(約一兩個月才出現一次)的問題最讓人頭痛,因為開發人員比較不容易抓到問題的真正原因,一開始大都只能用猜的,例如:可能是網路瞬斷所造成。同時,沒有明確的證據,可能也不知道如何請網管人員協助查看是哪一段網路傳輸的部分可能有問題。

由於我對防火牆、路由器等網路設備相關知識不熟悉,郵件伺服器也不在我的管轄範圍,因此只能先以程式設計的角度嘗試找出問題發生的原因。經過反覆的測試,發現同一份 HTML 格式的郵件,如果將內文加入一些 Enter 鍵斷行,原本寄送失敗的信件就可以寄送成功了。我第一次碰到這種情況,實在太詭異了。

此外,同樣的測試郵件內容,在機房裡面的應用程式會寄送失敗,在我的機器上卻能發送成功。由於我的機器與機房裡的機器的主要差別就在防火牆設備,於是將這兩條線索提供給網管人員。

經網管人員查看網路傳輸的 log 和防火牆設定之後,發現是被 CISCO 防火牆設備的 ESMTP 檔掉了。也就是說,當欲發送的 HTML 郵件含有特定 pattern 的字串時,CISCO 防火牆會認為那是不安全的封包而將它檔掉。

以下是網管提供的 log (實際的 IP 位址已改掉):

2009-04-10 14:20:02 Local4.Info 123.45.67.100 %ASA-6-302014: Teardown TCP connection 3734887 for outside:123.45.2.64/25 to inside:123.45.67.152/3913 duration 0:00:00 bytes 1644 Flow closed by inspection
2009-04-10 14:20:02 Local4.Info 123.45.67.100 %ASA-6-106015: Deny TCP (no connection) from 123.45.67.152/3913 to 123.45.2.64/25 flags PSH ACK on interface inside
2009-04-10 14:20:02 Local4.Info 123.45.67.100 %ASA-6-106015: Deny TCP (no connection) from 123.45.67.152/3913 to 123.14.2.64/25 flags PSH ACK on interface inside
2009-04-10 14:20:02 Local4.Info 123.45.67.100 %ASA-6-106015: Deny TCP (no connection) from 123.45.2.64/25 to 123.45.67.152/3913 flags ACK on interface outside

網管把 CISCO 的 ESMTP 功能關閉之後,系統的發信功能就全都正常,再也沒發生同樣的問題了。底下是關閉 ESMTP 選項的操作畫面:


問題雖然獲得解決,但我還是不明白 CISCO 的 ESMTP 為什麼會把正常內容的信件檔下來。似乎該去上一點網路管理方面的課程了..... @_@

Visual Studio 2010 與 .NET Framework 4 Beta 1 開放下載了

5/30/2009

Visual Studio 2010 與 .NET Framework 4 Beta 1 開放下載了。

在下載安裝之前,可以先看看這個教學影片:http://tinyurl.com/vs2010beta1


最糟糕的物件導向分析設計文件

5/28/2009
最近 review 一份有如天書的「物件導向設計文件」,不禁想到《物件導向分析設計與應用》書中有這麼一段:
「最糟糕的物件導向分析設計文件,是將每一個類別各寫成一份文件,然後在每份文件裡面描述該類別的所有方法。這種作法會產生很多沒用的文件,沒人會看、也沒人會信賴這樣的文件,而且它也無法呈現跨越單一類別的重要架構議題,也就是類別與物件之間──尤其是元件之間──的合作情形。比較好的作法是,將這些高階結構用 UML 圖形表現,然後提示開發人員可以到哪裡找到某些重要類別的詳細說明。」 (摘自第七章,p.322)

Visual Studio 單元測試專案要怎麼增加額外的部署檔案

5/11/2009

在開發應用程式時,經常有一些額外的組態檔必須在 build 專案時一併輸出到建置路徑下(即預設的 Bin\Debug 或 Bin\Release),這些檔案只要加入專案,並設定其 Copy to Output Directory 屬性為「Copy if newer」就行。但如果是單元測試專案,就還得做額外的設定。

Visual Studio 2008 每次執行單元測試時都會新建一個目錄來存放執行結果,該目錄是建在 solution 目錄下的 TestResults 資料夾底下,目錄名稱以 [使用者名稱_電腦名稱 年月日 時分秒] 的格式命名。在這些目錄裡面,你可以看到一個名為 Out 的子目錄,而你會發現,那些建置專案時一併輸出的額外檔案並沒有丟到這裡。因此,你的單元測試程式碼如果需要載入這些額外的檔案,執行測試時便會失敗。

解決方法:在 Solution Explorer 視窗中展開 Solution Items,雙擊副檔名為 ".testrunconfig" 的項目(若無此項目就自己加一個 Test Run Configuration)以編輯其內容。接著在開啟的對話窗點左邊的 Deployment,再按 Add File 鈕,把應用程式專案的建置輸出路徑下的額外檔案加進來就行了。參考下圖:

《物件導向分析設計與應用》勘誤

5/04/2009
物件導向分析設計與應用》勘誤
p.58 倒數第三段

原文:我們對階層(hierachy)的定義如下:
更正:我們對階層(hierarchy)的定義如下:

p.110 「實體包含」小節

原文將 aggregation 和 composition 的意思說反了:

A less direct kind of aggregation is also possible, called composition, which is containment by reference....Hence, the lifetimes of these two objects are not so tightly coupled as before: We may create and destroy instances of each class independently.

中文版沒有發現這個問題,將原文照譯為:

有一種沒那麼直接的聚合關係,叫做複合 composition),這是以參考(reference)的方式所形成的包含關係......因此,這兩個物件的生命週期就不像之前那樣緊密了─我們可以個別建立及摧毀其中一方。


更正:aggregation 是比較鬆散的 whole-part 關係,可以個別建立及摧毀其中一方。composition 才是實體包含。

p.228 的圖 5-63 少了說明文字

原文:圖 5-63
更正:圖 5-63:分岔進入複合狀態

p.283 的圖 6-7

原文:詳細的元素語意
更正:詳述元素語意

p.340,圖 8-2

排版問題:「衛星導航系統」這幾個字應該印在方塊內,不應壓在方塊的邊線上。

p.344 最後一段,第二行

原文:案例圖,如圖 8-1 所示
更正:案例圖,如圖 8-6 所示

p.363 圖 8-9

原文:SNS 區的部署
原文:SNS 區的部署

p.531 第一段的倒數第二行

原文:的 sessio bean
更正:的 session bean

強制要求輸入送交說明的 Subversion Hook

4/11/2009
剛用 C# 寫了一個 Subversion pre-commit hook,這個小工具能夠在開發人員執行 Subversion 送交命令時檢查是否有輸入送交說明(log message),如果沒輸入的話,就告訴 Subversion server 停止 commit 動作,並傳回錯誤訊息給用戶端,例如:

使用 VisualSVN 迅速建立 Subversion 伺服器

4/11/2009
目前 Subversion 官網上面列了三種不同口味的 server 打包版本:
以往我一直是用 CollabNet 打包的版本,這個版本(我用的是 v1.5)有個小缺點:若要使用 Windows 帳戶驗證,還得自己手動調整一些設定(參考這篇:Subversion 從 1.4.x 升級到 1.5 的注意事項)。

近日手癢,試了一下 VisualSVN v1.7。安裝方法非常簡單,幾乎都是按「下一步」就裝完了。在安裝過程中會讓你指定身分驗證機制要採用 Windows 帳戶驗證還是 VisualSVN 本身維護的帳戶,以及要用 HTTP 還是 HTTPS 協定。參考以下畫面:


不用三分鐘就裝完了,裝完之後不需要手動編輯組態檔,也不用其他額外設定,一台 Subversion 伺服器就能開始運作了。連我原本使用 CollabNet Subversion server 建立的檔案庫,也都是直接複製到 VisualSVN 的檔案庫目錄下就能直接使用。不過,我原本的檔案庫就是用 VisualSVN 預設支援的 FSFS 格式,若檔案庫是之前的 BDB 格式,我就不確定能否直接掛上了。

VisualSVN 安裝完後,Windows 系統會多出一項服務:VisualSVN Server,如下圖所示:

此外,程式集裡面還有一個管理工具:VisualSVN Server Manager。你可以利用此工具修改 Subversion server 的相關設定,例如身分驗證方式、傳輸協定、檔案庫所在路徑等等;也可以管理你的檔案庫,包括:檢視檔案庫、設定檔案庫存取權限(No Access、Read Only、Read/Write),甚至編輯 hooks。

缺點

雖然 VisualSVN 有著安裝簡單、對 Windows 帳戶驗證的支援較強(Active Driectory 帳戶也支援)、提供視覺化管理工具等優點,但也有些缺點(這裡指的是 v1.7 的版本),例如:
  1. 不支援 svn:// 協定(svnserve)
  2. 預設的檔案庫目錄底下不能再建立子目錄
第二項缺點需要解釋一下。以往使用 CollabNET Subversion server 時,我可以在檔案庫路徑底下建一些子目錄來區分不同類型的專案,比如說,檔案庫位置在 D:\SvnRepos,我可能會在此目錄下另外建立三個子目錄:Books、Projects、Thesis,然後在這些目錄底下建立真正的 subversion 檔案庫。如此一來,檔案庫的 URL 會類似這樣:http://localhost/svn/Books/ooad。這種方式的好處是可以讓我依自己的習慣組織檔案庫所在目錄的樹狀結構。

可是 VisualSVN 預設不支援這樣的目錄配置。以剛才的例子來說,若嘗試 svn checkout http://localhost/svn/Books/ooad,結果會無法取出檔案。解決辦法是在 Apache 的組態檔中增加一個 URI 位置的設定,像這樣:
<Location /svn/Books/>
DAV svn

SVNListParentPath on
SVNParentPath "D:/SvnRepos/Books"
SVNIndexXSLT "/svnindex.xsl"

AuthName "Subversion Repositories"
AuthType Basic
AuthBasicProvider visualsvn
AuthzVisualSVNAccessFile "D:/SvnRepos/authz-windows"
AuthnVisualSVNUPN Off

require valid-user
</Location>
組態檔位於 Program Files\VisualSVN Server\conf\,此目錄底下有兩個組態檔,一個是預設的 httpd.conf,另一個是 httpd-custom.conf。從檔名可以看出,我們只要把自訂組態寫在 httpd-custom.conf 就行了,這樣就不用去動預設的組態檔。附帶一提,你不見得一定要用 SVNParentPath 來指定檔案庫路徑,還有一種方法是設定 SVNPath(適用於檔案庫散置各處,不在同一個父資料夾路徑下的情況)。

奇怪的是,CollabNet 和 VisualSVN 都是用 Apache HTTP Server,同樣的 SVNParentPath 設定卻有不同的作用,不知是否為二者採用之 Apache 版本不同所致,或者我漏掉了某個設定?

軟體設計方面的書籍

4/09/2009

有位未具名的朋友在《物件導向分析設計與應用》書籍相關資訊的回應中問到:「還有什麼樣的書,類似這本,把關於IT設計的概念、方法、目的、應用等,都整理出來的好書。」在回覆時,寫著寫著覺得內容有點長了,於是拉出來自成一篇。

我第一個想到的是四人幫的《Design Patterns》。雖然手邊還有其他討論軟體設計的書籍,但每次我有設計方面的疑問時,都會先從這本書下手。名副其實的經典!這本書有中文版:《物件導向設計模式》,葉秉哲翻譯。書中的範例是以 C++ 撰寫,也許有些人會覺得不夠親切吧。

其他討論 design patterns 的書籍,我手邊有而且覺得不錯的:
以上這些書籍所介紹的 design patterns,大都屬於比較通用的、一般目的的 patterns。另外還有一些書籍是針對特定問題領域或架構方面的 patterns,像是:
這幾本書我自己並沒有全看過,只能說稍微瞄過一些。PoEAA 的作者是 Martin Fowler,其中收錄了許多企業級應用程式經常用到的 patterns。之所以列出 POSA 這三冊,是有一次我在隨便翻看時,才發現原來自己寫的一套軟體中用來解決某個複雜問題的方法,就是 POSA Volumn 1 中介紹的 Blackboard pattern;此外,《物件導向分析設計與應用》也有一個 case study 用到此模式。Volumn 2、3 的 patterns 可能就稍嫌冷門了些。

目前大概先想到這幾本,若有想到其他值得推薦的書籍再補上來。

客製化 BugTracker.NET

4/08/2009
BugTracker.NET 除了安裝簡單,也有考慮到一些客製化的需求。例如,web.config 裡面有下列參數:
    <add key="AppTitle" value="BugTracker.NET"/>
    <add key="SingularBugLabel" value="bug"/>
    <add key="PluralBugLabel" value="bugs"/>
AppTitle 是顯示於瀏覽器視窗標題的文字,SingularBugLabel 和 PluralBugLabel 則可以讓你用別的名稱取代 bug/bugs。如果覺得 bug 這個名詞太侷限,或太敏感,我們可以改用其他名稱,像這樣:
    <add key="AppTitle" value="問題管理系統"/>
    <add key="SingularBugLabel" value="issue"/>
    <add key="PluralBugLabel" value="issues"/>
注意如果有用到中文字,在顯示時可能會變成亂碼,因此得將 web.config 改以 UTF-8 編碼的格式儲存。結果如下圖:

其他可客製化的頁面都放在 custom 目錄下,你可以藉由修改 custom_logo.html 把左上角的 Logo 換掉,或者修改 custom_header.html,以便在每頁的上方顯示公司或單位名稱,例如:

同樣地,這些檔案也都要存成 UTF-8 編碼,否則網頁無法正確顯示中文字。此外,上述修改動作都必須重啟 IIS 才會生效。

除了頁面文字的客製化,BugTracker.NET 還提供了 custom fields,讓你可以增加問題登錄頁面的輸入欄位。在 3.1.6 版,每個專案最多可有三個自訂欄位,而且都是下拉清單(drop-down list),如果這三個下拉清單欄位仍不夠用,還可以增加全域的自訂欄位。參考下面這張登錄新問題的畫面截圖:

圖中的「預定完成日」是全域自訂欄位,「子系統」則為該專案 'project 1' 的自訂欄位(若選擇其他專案,則不會出現此欄位)。在建立全域自訂欄位時,可指定輸入方式是否為下拉清單,並可指定資料型態;如果資料型態是 datetime,還有日曆元件可用。

有個小地方可能要注意一下:管理員可以隨時刪除全域自訂欄位。也就是說,如果你已經有登錄 issues,後來又把某個自訂欄位刪除,那麼之前登錄的 issues 資料就不會有那個自訂欄位了。實際上,在建立全域自訂欄位時,程式會真的在資料表 bugs 中動態建立新欄位,而刪除全域自訂欄位時,也會真的刪除實體欄位。至於專案的自訂欄位,則只是預先在資料表中預留三個欄位而已。

BugNET 比起來,BugTracker.NET 的程式寫法顯得「樸實」許多,因為它主要是以 .aspx 和 html 構成,完全沒有 code-behind 類別,但整個設計給我的感覺是非常簡單、易用,也很彈性,有需要架設問題管理系統的人不妨試試。

相關文章:BugTracker.NET 簡介

Subversion 版本控制系統的基礎觀念

4/06/2009
本文介紹 Subversion 版本控制系統的基礎觀念和術語,以及導入版本控制系統時應考慮的事項。

P.S. 這篇文章原本寫於 2004 年 6 月,之前將部落格搬到 blogger.com 時沒有整理進來,現在補上,順便重新編排、修剪。

ImeLib v0.1 測試報告 (二):Vista

4/05/2009
延續上一篇測試報告,這次測試的作業平台是 Windows Vista 繁體中文版,已內建微軟新注音 10.0。結果出乎意料,ImeLib 的範例程式竟無法取得注音和拼音字根,只有日文平假名/片假名的功能可正常運作。用 RegEdit 搜尋關鍵字 MSIME,只能找到 MSIME.Japan 的註冊資訊。
這情形跟我在測試 Windows Server 2008 的情況一樣,但這次的解決方法並不是安裝 Office 2007 。

ImeLib v0.1 測試報告,以及與 IMM32.dll 比較

4/04/2009
這裡記錄一下自己測試 ImeLib v0.1 的結果,並簡單比較 IFELanguage 與 IMM32.dll。

ImeLib v0.1 發布於 CodePlex

4/01/2009
剛剛把這兩天寫的一個取注音字根的元件放到 CodePlex 上面,取名為 ImeLib,以 LGPL 授權方式發布,附完整 C# 原始碼和一個範例程式。目前的版本是 0.1,還缺很多東西,但基本上我自己需要的部份(取得中文注音字根)已經大致完成且測試 OK。以下是範例程式的畫面:

A Better DataReader for C# 2.0

3/25/2009

在使用 ADO.NET 的 DataReader 來讀取欄位資料時,常常要寫很多判斷欄位值是否為 DBNull 的程式碼,例如:

SqlConnection cn = new SqlConnection("連線字串");
SqlCommand cmd = new SqlCommand("SELECT * FROM ...", cn);
SqlDataReader rdr = cmd.ExecuteReader();
while (rdr.Read())
{
if (rdr.IsDBNull(rdr.GetOrdinal("BIRTHDAY")))
{
Response.Write("");
}
else
{
Response.Write(Convert.ToDateTime(rdr["BIRTHDAY"], "yyyy-MM-dd"));
}
}
若不先判斷欄位值是否為 DBNull,程式執行時就會出現資料轉換失敗的 exception。如果能這樣寫就方便多了:
 SqlConnection cn = new SqlConnection("連線字串");
SqlCommand cmd = new SqlCommand("SELECT * FROM ...", cn);
BetterDataReader rdr = new BetterDataReader(cmd.ExecuteReader());
while (rdr.Read())
{
Response.Write(rdr.GetDateTimeStr("BIRTHDAY"));
}
這裡的 BetterDataReader 是修改自 Steve Michelotti 的 Nullable Data Readers,它本身雖然也實作了 IDataReader 介面,但大部分的實作方法都是直接呼叫外界傳入的 DataReader 物件的既有方法,同時再增加我們需要的方法。換句話說,BetterDataReader 只是一個簡單的 DataReader 轉換器(adapter)而已,它使用 wrapper(而非繼承)的方式來補強既有類別不足的地方,主要原因是既有的 DataReader 類別並不允許繼承。這是 C# 2.0 的解法,如果是 C# 3.0,就可以用 extension methods,這樣在撰寫程式時就更直覺了。

在剛才的範例程式中,GetDateTimeStr 方法會將你指定的日期欄位值轉換成字串傳回,若欄位值為 DBNull,則傳回空字串。這個方法便可以省掉每次判斷 DBNull 的瑣碎工作。

以下是 BetterDataReader 的部分原始碼:

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using Huanlin.Helper;

namespace Huanlin.Data
{
/// <summary>
/// 此類別是 DataReader 物件的簡單包裝,主要在解決欄位值為 DBNull 的問題,並增加一些方便的取值方法。
/// </summary>
public class BetterDataReader : IDataReader
{
#region Private Fields

IDataReader reader;

/// <summary>
/// Delegate to be used for anonymous method delegate inference
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
private delegate T Conversion<T>(int ordinal);

#endregion

#region Private Methods

/// <summary>
/// This generic method will be call by every interface method in the class.
/// The generic method will offer significantly less code, with type-safety.
/// Additionally, the methods can you delegate inference to pass the
/// appropriate delegate to be executed in this method.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="ordinal">Column index.</param>
/// <param name="convert">Delegate to invoke if the value is not DBNull</param>
/// <returns></returns>
private Nullable<T> GetNullable<T>(int ordinal, Conversion<T> convert) where T : struct
{
Nullable<T> nullable;
if (reader.IsDBNull(ordinal))
{
nullable = null;
}
else
{
nullable = convert(ordinal);
}
return nullable;
}

#endregion

#region Constructors

/// <summary>
/// 建構函式。
/// </summary>
/// <param name="dataReader"></param>
public BetterDataReader(IDataReader dataReader)
{
reader = dataReader;
}

#endregion

#region IDataReader Members
// (略)
#endregion

#region IDisposable Members
// (略)
#endregion


#region IDataRecord Members

public DateTime GetDateTime(int i)
{
return reader.GetDateTime(i);
}

public DateTime GetDateTime(string name)
{
return this.reader.GetDateTime(reader.GetOrdinal(name));
}

/// <summary>
/// 取得可為 NULL 的 DateTime 物件。
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Nullable<DateTime> GetNullableDateTime(int index)
{
return GetNullable<DateTime>(index, GetDateTime);
}

/// <summary>
/// 取得可為 NULL 的 DateTime 物件。
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public Nullable<DateTime> GetNullableDateTime(string name)
{
return GetNullableDateTime(reader.GetOrdinal(name));
}
/// <summary>
/// 傳回格式化的日期時間字串。若欄位值為 NULL,則傳回空字串。
/// </summary>
/// <param name="name">欄位名稱。</param>
/// <param name="format">格式化字串。</param>
/// <returns>日期時間字串。</returns>
public string GetDateTimeStr(string name, string format)
{
Nullable<DateTime> dt = GetNullableDateTime(name);
if (dt.HasValue)
{
return dt.Value.ToString(format);
}
return "";
}

/// <summary>
/// 傳回格式化的日期時間字串。
/// </summary>
/// <param name="name">欄位名稱。</param>
/// <returns>日期時間字串。</returns>
public string GetDateTimeStr(string name)
{
return GetDateTimeStr(name, DateTimeHelper.DateTimeFormat);
}

/// <summary>
/// 傳回格式化的日期字串。若欄位值為 NULL,則傳回空字串。
/// </summary>
/// <param name="name">欄位名稱。</param>
/// <returns>日期時間字串。</returns>
public string GetDateStr(string name)
{
return GetDateTimeStr(name, DateTimeHelper.DateFormat);
}

public int GetInt32(int i)
{
if (reader.IsDBNull(i))
return 0;
return reader.GetInt32(i);
}

public int GetInt32(string name)
{
return this.GetInt32(reader.GetOrdinal(name));
}

/// <summary>
/// 若欄位值為 NULL,則傳回預設值。
/// </summary>
/// <param name="name">欄位名稱。</param>
/// <param name="defaultValue">預設值。</param>
/// <returns>欄位值。</returns>
public int GetInt32(string name, int defaultValue)
{
int index = reader.GetOrdinal(name);
if (reader.IsDBNull(index))
{
return defaultValue;
}
return reader.GetInt32(index);
}

public string GetString(int i)
{
if (reader.IsDBNull(i))
return "";
return reader.GetString(i);
}

public string GetString(string name)
{
return this.GetString(reader.GetOrdinal(name));
}

/// <summary>
/// 若欄位值為 NULL 或空字串(包含 Tab、換行字元),則傳回預設值。
/// </summary>
/// <param name="name">欄位名稱。</param>
/// <param name="defaultValue">預設值。</param>
/// <returns>欄位值。</returns>
public string GetString(string name, string defaultValue)
{
string value = this.GetString(name);
if (StrHelper.IsEmpty(value))
{
return defaultValue;
}
return value;
}
#endregion
}
}

林以亮<翻譯的理論與實踐>

3/22/2009
在《林以亮論翻譯》的第一篇<翻譯的理論與實踐>中,他把林語堂的翻譯三原則略加修正為:

一個翻譯者所應有的條件應該是:(一)對原作的把握;(二)對本國文字的操縱能力;(三)經驗加上豐富的想像力。 (p.10)

《物件導向分析設計與應用》未付印的譯序

3/13/2009
物件導向分析設計與應用》沒有譯序,而這篇,應可算是未付印的譯序吧。
在翻譯過程中就開始寫譯序,似乎成了我的習慣。每當有一些想法時就寫一點、改一點,等到整本書譯完,譯序也差不多完成了。但這次我並沒有把譯序交給出版社,一方面,本書作者已是大師級人物,讀者可能沒興趣再看一些錦上添花的推薦文,或翻譯甘苦談之類的碎碎念。另一方面,貼在部落格上比較方便修改,隨時反映新的想法。

一點提醒

雖然作者的名氣響亮,但每個人的需求和口味不同,建議您先看看試讀章節(包含目錄、序、和第一章),考慮一下這本書是不是「你的菜」。比如說,有些人可能會覺得這本書的參考文獻太「豐富」、學術味道太濃、UML 語法不夠完整詳盡、案例跟自己碰到的專案類型相差太遠(而無法直接依樣畫葫蘆)、譯筆太差、當枕頭太硬......等等。

其實光看厚度也知道,這本書不是兩三下就能消化完的,而且文字風格也絕對不會像 Head First 系列書籍那樣輕鬆詼諧。願意嘗試「啃」這本書的人,我想應該是對物件導向技術有相當的興趣與學習熱誠吧。那麼,我也樂意在這裡野人獻曝,提供一點個人的小小心得。

本書梗概

本書的架構主要分成三個部分,即概念篇、方法篇、和應用篇。以下簡單說一下各篇的內容概要。

開發過軟體專案的人應該都會同意,看似簡單的需求,最後都可能演變成複雜的大系統(希望不是大災難)。因此,為了解決許多複雜的軟體設計問題,開發人員必須化繁為簡,讓使用者能夠以簡馭繁;而達成此目標的一種有效策略,便是分而治之、各個擊破。那麼,分而治之的「分」指的是什麼呢?作者在書中便從「演算法分解」與「物件導向分解」兩種方法的比較作為切入點。瞭解物件導向分解的原理和優點之後,再來便是分類的哲學與物件模型的介紹。以上這些,大概就是第一篇所要闡述的重點。當然,分得好,還須合得妙;若只是將系統拆解成各自分散的個體,還是成不了一個完整運作的系統。至於怎麼合,又涉及了 UML 各種圖形以及模式(patterns)的搭配運用,是另一層次的議題了(本書的第三篇其實已經有運用一些 design patterns)。既名為概念篇,其內容自然是以物件導向的基本理論、觀念、原則為主。就這個部分來說,讀者可能容易覺得枯燥,或者學術味道比較濃。作者大概也有想到這點吧,所以在書中適時加入一些與主題有關的卡通插畫,讓嚴肅的主題增添一點趣味。

第二篇(「方法」篇)包含 UML 圖形表示法(第 5 章)、開發流程(第 6 章)、以及一些實務作法(第 7 章)。UML 表示法的部分比較像參考手冊,已熟悉 UML 2 的讀者大可先跳過這個部分,等到需要時再回頭查閱無妨。如果你是方法論專家,或者是開發塑模工具的設計師,可能會覺得書中的 UML 表示法不夠詳盡,因為這裡介紹的 UML 表示法主要是針對一般的 SA/SD/PG 來寫,而這些內容對於一般軟體系統的分析設計來說已經相當夠用了。一般比較常用的,大概也就是所謂的 UML 三劍客(使用案例圖、類別圖、循序圖)。如果需要塑模事務流程,自然得再加上活動圖--姑且稱它們為 UML 四君子吧 :)

第三篇(「應用」)全是物件導向分析設計(OOAD)的個案探討,分為五章,每一章都是一個不同的案例。

在學習一項技術時,如果有搭配範例說明,通常會比較有感覺,而如果範例正好跟自己碰到的問題類似,那更好,說不定還可以透過複製再修改的方式直接套用。因此,範例對個人的幫助有多大,就牽涉到範例的大小與其涉及的問題領域了。簡單如自動櫃員機(ATM)的案例,雖然容易理解,可是在實際開發 OOAD 專案時,可能會發現原來還會碰到那麼多大大小小的問題、那麼多設計決策要取捨,因而不知所措。此時你可能會希望找到更貼近自己手邊問題的範例,以及一些更明確的開發指引和建議,例如:剛開始進行一個新專案時,軟體的架構要怎麼規劃、子系統和套件應如何切割、使用案例描述該怎麼寫、如何抓類別等等。本書第三篇的用意即在透過實際的案例展示 OOAD 的開發流程,並提供一些實務面的分析設計技巧。

要提醒的是,這些案例的專案規模都不小,如衛星導航、鐵路交通管理、氣象監測等,若要把這些專案的設計理念和開發細節全交代清楚,恐怕每個個案都可以寫成一本書了。所以在看這些範例時,最好把焦點放在如何從這些去蕪存菁的個案探討當中體會作者想要表達的東西,同時思考作者為何要這麼設計(優缺點在哪裡,有沒有更好的設計方法),以及找到對我們實際開發有用的部分。

大概就這樣吧,軟體設計的世界裡沒有絕對正確、一體適用的答案,希望這本書對您有幫助!

WCF 入門練習

3/13/2009
開發 WCF 應用程式基本上有三項工作:
  • 撰寫 WCF 服務,這包括定義服務介面,以及撰寫實作服務介面的類別。
  • 部署 WCF 服務。
  • 撰寫 WCF 用戶端程式。

Logging Application Block 概念圖

3/11/2009

補一張 EL Logging Application Block 的簡易概念圖:




MSDN 網站上也有一張比較詳細的類別圖可參考:Design of the Logging Application Block

Logging Application Block (三):撰寫自訂 Log 監聽器

3/10/2009
儘管 EL4 的 Logging Application Block(以下簡稱 LAB)已經提供很多種 trace listeners,但有時候還是無法完全符合我們的需要,例如上一篇提到的,Email Trace Listener 並未支援 SMTP 伺服器身份驗證的功能。此時就得自己寫一個 log 監聽器(sink)了。


Logging Application Block (二):透過 E-mail 寄送 log 訊息

3/10/2009
本文將示範如何在 ASP.NET 網站中透過 Logging Application Block 記錄 log 訊息,包括:
  • 使用 Filter 和 Severity 屬性的設定來篩選想要記錄的訊息。
  • 將 ASP.NET 網站未處理的 exception 輸出至 log。
  • 輸出的 log 訊息將透過 STMP 寄送至指定的 e-mail 信箱。

Logging Application Block (一):入門教學

3/09/2009
摘要:本文說明 Enterprise Library 的 Logging Application Block 的基本用法,其中包含兩個 step-by-step 練習,分別示範將 log 訊息輸出至 Windows 事件檢視器以及可循環使用的純文字 log 檔案。


BugTracker.NET 簡介

3/06/2009
原本個人常用的 issue tracking system 是 BugNET,因為安裝簡單、免費、功能也不差,像是:切分專案與子系統、問題的嚴重度、問題分類、統計圖表(稍嫌陽春)、E-mail 通知(這個真的很必要)等都有。這兩天又試了另一款工具:BugTracker.NET
同為 open source 軟體,我覺得二者的功能差不多,很難說哪一個一定比較好用。BugTracker.NET 還提供了自訂欄位,以及 E-mail 轉成 bug entry 的功能,此功能可以將 user 透過 e-mail 反映的問題轉入系統。

此外,BugTracker.NET 比較吸引我的部分,是它還能夠與 Subversion 整合。我還沒試這項功能,不過依官方文件的描述,它有提供 Subversion 的 commit hook,如此一來,每當程式設計師送交檔案時,如果有在 checkin comment 中填入 bug ID,那個 hook 程式就會去更新 BugTracker.NET 資料庫中對應的 bug entry。這樣的整合可以免去開發人員手動修改問題狀態的手續,讓整個問題處理的流程更順暢、更省力。

安裝步驟很簡單:
  1. 下載 BugTracker.NET(我用的是 v3.1.3),把壓縮包解開到某個資料夾,再進入 IIS 管理員建立一個虛擬目錄指向該資料夾,身分驗證方式設定為啟用匿名存取。
  2. 假設你建立的虛擬目錄名稱是 btnet,就用 IE 瀏覽網址: http://localhost/btnet/install.aspx 。這個網頁會告訴你怎麼安裝,如下圖所示:


此安裝頁面還有提供建立資料庫的功能,不過我是這麼做的:先自己手動建立資料庫(名稱隨你訂,例如:btnet_db),接著依畫面上的步驟 2~4 進行就完成安裝設定了。

當然,你的機器上必須有安裝 SQL Server(Express 版也行)、IIS、和 .NET Framework 2.0 或更新的版本。

安裝完成後,接下來的步驟基本上和 BugNET 差不多:建立專案、建立使用者帳戶、設定權限等等,這些動作都挺直覺,就不細說了。這裡有示範網站可以參考:http://ifdefined.com/btnet/bugs.aspx

《物件導向分析設計與應用 第三版》書籍相關資訊

2/26/2009

書名:物件導向分析設計與應用 第三版
   (OOAD with Applications 3ed)
作者:Grady Booch et al
譯者:蔡煥麟
出版:碁峰,2009 年 2 月 18 日
頁數:712(頁頁對譯)
相關資料
  - 未付印的譯序
  - 試讀章節(.pdf 格式)
  - 勘誤
  - 第 1 章投影片(.ppt 格式)




與本書有關的資訊,包括文章、勘誤、檔案下載等,都會陸續在這裡更新。

如對本書有任何建議或指正,也歡迎在此留言 :)

相關文章
博客來看看...

WCF 學習資源

2/24/2009

Unicode 內碼在網頁上顯示的問題

2/24/2009
2-25 Update經網友 Laneser 提醒,其實這篇提到的問題用 HtmlDecode 就簡單解決掉啦! 裡面提到的函式大概就只剩下 FindUnicodeEntities 可能還有用處吧! 其他一長串的東西就不用浪費時間看啦!


使用 Sandcastle Help File Builder 製作類別庫文件

2/20/2009

簡介

CodePlex 網站上有一個 Sandcastle 專案,這是用來製作類別庫文件的工具,如果你在撰寫 .NET 程式時,有輸入 XML 註解,這個工具可以幫你抓出來,並產生類似 MSDN help 那樣的說明文件(你的專案的 Build 選項中的 Output > XML documentation file 選項必須勾選)。

可是 Sandcastle 是個命令列工具,它沒有 GUI,所以用起來不是那麼方便。這裡要介紹的就是搭配 Sandcastle 的一套 GUI 工具:Sandcastle Help File Builder

Sandcastle 能夠產生的說明文件格式包括:
  • HtmlHelp 1.x(.CHM)
  • HtmlHelp 2.x(.HxS)
  • 網頁格式(.html)
你需要安裝的東西有:
要特別注意的是,Visual Studio SDK 裡面也有附 Sandcastle,但是版本比較舊。你必須在安裝完上述軟體後,檢查系統的環境變數:
  1. 查看「使用者變數」,若 PATH 裡面有 Sandcastle,就把它刪掉。
  2. 查看「系統變數」,確認 DXROOT 變數是指向 "C:\Program Files\Sandcastle\"(這是 Sandcastle 的預設安裝路徑)。

使用

安裝程序稍嫌繁瑣,使用起來倒很簡單。首先,從程式集點選 Sandcastle Help File Builder > Sandcastle Help File Builder GUI(以下簡稱 SHFB),進入 SHFB 之後,點 File > New Project,然後在 Project Explorer 視窗裡面展開專案節點,在 Documentation Sources 上點右鍵,選 Add Documentation Source,就可以將你要產生說明文件的 .NET 組件加進專案。

你會需要設定一些專案屬性,像是:輸出的文件類型(.CHM、.HxS、還是網頁,可複選)。從主選單點 Window > Project properties 或按 F4 即可開啟專案屬性設定視窗。參考下圖:



圖中的 HelpFileFormat 就是輸出的文件格式。除此之外,通常至少還要修改以下屬性:
  • HtmlFileName - 檔案名稱,預設是 Document。
  • HelpTitle - 說明文件的視窗標題。
都設定好之後,點擊主選單的 Document > Build Project 就可以產生說明文件了。很簡單吧?!

產生的檔案是放在專案屬性 OutputPath 所指定的目錄下,這裡貼兩張由此工具產生出來的 HtmlHelp 1.x(.CHM)說明文件的畫面:





語法的部分還分別列出 C#、Visual Basic、和 Visual C++ 三種語法,看起來真是挺專業地呀 ^_^

WCF 概念圖

2/10/2009
Windows Communication Foundation (WCF) 概念圖 v1.3:

UISpy.exe 跑哪兒去了?

2/08/2009

本來要找 Spy++,結果找到 Microsoft Windows SDK Blog 的一篇文章:Where is UISpy.exe?

原來 2008 年 2 月發布的 Windows SDK for Windows Server 2008 and .NET Framework 3.5 裡面並沒有附這支工具程式(漏掉了?),微軟會在下一個版本把它放回去。如果現在要用的話,可以下載 Windows SDK for Vista Update,在安裝時,只勾選「.NET Framework Tools」就行了。安裝好之後,UISpy.exe 可以在 \Program Files\Microsoft SDKs\Windows\v6.0\Bin 目錄下找到。

但是那篇文章也有提出警告,此更新版會安裝在 \Program Files\Microsoft SDKs\Windows\v6.0\ 目錄下,如果你的電腦原本就已經有安裝 Visual Studio 2008 和 Windows SDK for Windows Server 2008 and .NET Framework 3.5,則分別會有 v6.0Av6.1 這兩個版本。可是 VS2008 總是會使用最新安裝的 SDK,因此如果你選擇只安裝部分元件,在使用 VS2008 時可能就會出問題。

解決辦法在那篇文章裡面已經說得很清楚,就不贅述了。

C# 4.0 動態型別應用例:動態載入 DLL 模組

2/06/2009

小引

八年前(2001 年),我曾寫過一篇標題為「DLL 應用 - 設計可抽換的模組」的文章,當時的範例是以 Delphi 實作,之後經過一些修改,也成為自己開發 Windows 應用程式的主要框架。後來轉到 .NET 平台,又將此範例分別改寫成 Delphi.NET 和 C# 版本,並於 .NET Magazine 上發表類似的文章,標題是「設計動態載入的 Plug-in 應用程式」,這時候已經是 2005 年了。如今又過了四年,因為 C# 4.0 的 dynamic 型別,便想把這個範例拿出來改一下,看看有甚麼不一樣的地方。


C# 4.0 新功能:動態繫結

2/02/2009
官方網站 C# Future 上面有一份 2009 年 2 月份剛出爐的 C# 4.0 技術文件:New Features in C# 4.0。雖然 C# 4.0 規格尚未正式發布,但從這份文件已經可以看出一些端倪。例如文中提到,C# 4.0 的新功能主要可分成四大塊:



C# 筆記:從 Lambda 表示式到 LINQ

1/30/2009

C# 筆記:擴充方法

1/29/2009
摘要:本文將簡單介紹 C# 3.0 的新語法:擴充方法(extension methods)。

C# 筆記:重訪委派-從 C# 1.0 到 2.0 到 3.0

1/28/2009
這篇文章主要是複習一下 C# 委派(delegate)的基本觀念,同時也示範從 C# 1.0、2.0、到 3.0 的委派寫法。 我們會看到更直覺的建立委派物件的語法、匿名方法、以及 Lambda 表示式。

C# 筆記:自動實作屬性與物件初始器

1/27/2009
C# 3.0 增加了自動實作屬性(auto-implementation property)與物件初始器(object initializer)的語法。這兩種語法都提供了某種程度的方便性,Scott Guthrie 很早就寫了一篇文章介紹它們,這裡就不寫太多重複的東西了。貼個簡單的範例上來:

C# 筆記:匿名型別

1/27/2009
時候,例如在某個函式裡面,我們會臨時需要一個簡單的類別來儲存一些簡單資料,但又不想為了這個簡單的需求另外定義一個類別,此時便可使用 C# 的匿名型別(anonymous type)。


C# 筆記:使用 var 宣告隱含型別

1/27/2009
C# 3.0 增加了 var 關鍵字,你可以用它來宣告隱含型別,例如:

int i = 10;
var j = 10;


ASP.NET 網站呼叫 Web service 時出現 SocketException (0x274c)

1/20/2009
這個問題不太好找,值得記錄一下。

問題描述

在 ASP.NET 2.0 網站中呼叫另一個網站的 Web service 時,「有些」用戶端瀏覽器會出現以下錯誤:

連線嘗試失敗,因為連線對象有一段時間並未正確回應,或是連線建立失敗,因為連線的主機無法回應。 111.xxx.xxx.xxx:80
[SocketException (0x274c): 連線嘗試失敗,因為連線對象有一段時間並未正確回應,或是連線建立失敗,因為連線的主機無法回應。 111.xxx.xxx.xxx:80]

問題難解的原因有兩個。首先,是這個問題只有在少數用戶端環境才會發生,開發團隊不易模擬出問題環境。其次,整個網站的運作架構有點複雜(load-balance、proxy、switch、firewall...blah blah blah),以至於碰到 80 port 被擋住時,花了很多時間在懷疑網路繞徑和防火牆等問題。畢竟,採用 web service 的原因之一,就是因為 80 port 一般都能通行無阻嘛!

發生原因

基本規則:當你在 .NET 程式中利用 Web service proxy 類別來呼叫 Web services,而且該 Web service 是位在 proxy/NAT router 背後,此呼叫動作可能就會失敗。

可是,我們碰到的狀況是只有少數用戶端會出錯,這點倒是令我想不透。

根據 Rick Strahl 的文章「Slow Http client calls from ASP.NET 2.0? Make sure you check your Proxy Settings!」, 裡頭說 .NET 2.0 的 HTTP client 類別預設會去抓取機器上的 IE proxy 設定,然而 ASP.NET 應用程式的執行帳戶(IIS 6 為 NETWORK SERVICE)權限很低,無法讀取那些儲存在 registry 裡面的 IE 設定。這條線索也挺有幫助。

解決方法

首先,開發團隊總算模擬出可以讓問題重現的環境和操作程序。經過測試發現,只要用戶端瀏覽器有指定透過公司內部的 proxy 伺服器連接 Internet,程式就能順利執行。

但是公司內部的 proxy 伺服器僅供企業內網的用戶端使用,故外網的使用者仍會有問題。因此,便從 Strahl 的文章得到這個靈感:何不試試為 Web service 物件指定 proxy 伺服器?

於是,為了讓 ASP.NET 程式在呼叫 Web service 時透過我們指定的 proxy 去連接,我在 web.config 裡面加了以下設定:

<system.net>
  <defaultProxy>
    <proxy
      bypassonlocal="true"
      usesystemdefault="false"
      proxyaddress="http://proxy.xxx.com:3128" />
  </defaultProxy>
</system.net>

如此一來,無論是內網還是外網的使用者,便都能夠順利執行程式了。

不過這種修改 web.config 的方式也會影響 .NET FtpWebRequest 類別,導致應用程式原本順利執行的 FTP 功能發生錯誤:The Requested FTP Command Is Not Supported When Using HTTP Proxy。此時便可以改用程式指定 proxy 的方式解決,參考以下範例:

MyService.Foo ws = new MyService.Foo();
WebProxy proxyObj = new System.Net.WebProxy("http://proxy.xxx.com:3128", True);
ws.Proxy = proxyObj;
ws.DoSomeThing();


這樣就不用去動 web.config 了。

當然,你也可以修改 FTP 傳輸處理的程式碼,將 FtpWebRequest 的 Proxy 設為 null。要用哪一種方式,就看實際情況而定了。

小結

在撰寫 Web service 應用程式時,須了解:
  • .NET 從 2.0 起,利用 wsdl.exe 或 Visual Studio 的 Add Web Reference 功能產生的 Web service proxy 類別預設會使用機器上的網際網路 proxy 連線設定來存取 Web service。此內定行為對某些網路環境會造成問題,須自行指定透過 proxy 連線的方式存取 Web service。
  • 透過 proxy 連線的方式有兩種:(1) 以程式建立 WebProxy 物件,然後指定給 Web service proxy 物件的 Proxy 屬性;(2) 在應用程式組態檔中設定 proxy 元素。
  • 應用程式組態檔的 proxy 元素可以控制 .NET 網路傳輸相關類別要不要使用 IE 的 proxy 設定,亦可明白指定 proxy server 的位址。

《OOAD with Applications 3rd edition》的翻譯進度(2009/1/16 更新)

1/16/2009

2009-02-24 更新

書籍已出版,詳情請看《物件導向分析設計與應用 第三版》書籍相關資訊


《Object-Oriented Analysis and Design with Applications 3rd edition》是由物件導向三巨頭之一的 Grady Booch 以及其他作者所共同撰寫。

中文版書名目前暫定為:《物件導向分析設計與應用 第三版》

這本書將近 700 頁,雖然附錄和參考文獻就占了 139 頁,但是對我來說還是屬於重量級的挑戰(對讀者來說恐怕也是吧)。

目前的翻譯進度:

第 12 章
> 附錄 > 索引 > 校稿 > 等出版


2009/1/16 更新:

今天(2009/1/16)的版本應該是最後的修訂了,最後一次校稿結果是由出版社的編輯做最終修訂,我自己也沒有看到最後的排版結果。不過,碁峰出版社的編輯 Novia 小姐相當細心,還抓到幾條二校的「漏網之魚」。相信最終的成品會有不錯的品質。

這帖不會再更新了,本文的回應也就此鎖住。有關本書的出版訊息、簡介、試讀章節等參考資源會另起新文。

相關文章:

複雜的資料查詢:手工打造 SQL 指令 vs. 撰寫程式碼

1/11/2009
對於複雜的資料查詢,我們是該用手工打造的 SQL 指令呢,還是該寫程式碼來處理?在甚麼情況下該選擇甚麼方法呢?

我的看法是應該盡量以撰寫程式的方式來處理。

在解釋原因之前,要先聲明一件事:我不是 SQL 高手,所以在一開始打算寫這篇文章時,我就在想我的結論可能過於偏頗。不過,若能拋磚引玉,得到其他前輩、高手的意見,因而修正自己的想法,倒也是好事一樁。

其實在多年以前,我便覺得撰寫 SQL 對我來說是一件「寫時容易讀時難」的工作。我不是說兩三兩個資料表 join 這類簡單的 SQL 查詢,我說的是包含七八層、甚至更多層子查詢的 SQL。因此在我自己寫的程式裡面,我都盡量不寫太複雜的 SQL,而我的 SQL 功力自然也就一直停留在小學生的階段(好吧,也許是幼稚園階段)。

兩種解法

會想把這個議題寫下來,其中一個原因是不久前又碰到了複雜 SQL 導致嚴重效能問題的情形。另一個原因,是正好一個朋友(T 先生)最近 msn 給我,提到他們公司裡面有一位 SA 寫了一個超級 SQL。這個超級 SQL 指令多達 8463 bytes,據他描述,若以每一列都填滿 80 個字元來計算,也有一百多列。

對一個 SQL 功力只有小學程度的我來說,這道一百多列的 SQL 指令自然讓我覺得不可思議,便半開玩笑的回他:「快請他回火星吧!」然而他說這個 SQL 不僅解救了他們的難題,執行速度還很快哩。

我比較擔心的,是萬一將來 business rule 有變,原作者又離開團隊了,當初也沒寫文件,那麼這些 SQL 誰有辦法改?朋友回答的大意是:在時程壓力下,能解決問題的就是好方法。言下之意是顧不了那麼多了。我不禁想到《軟體工程與 VSTS》裡面提到的「時程膽小雞」(schedule chicken)。

架構問題效能 vs. 可維護性

對我來說,要用手寫的複雜 SQL,還是撰寫程式來解決,是屬於架構面的議題。在決定採用何種解法時,必須同時考慮系統的整體效能和往後的維護問題。我不喜歡為了執行效能而捨棄可維護性,更何況我覺得兩者並非魚與熊掌。很多時候,程式開發人員並沒有仔細分析問題,寫出來的程式跑得慢,就想用 SQL 來一次解決。其實加快程式執行效能的技巧何其多(例如:資料快取、預先產生暫時資料、建立索引等),如果未經深思熟慮和多方測試,便認定哪一種解法就一定有更佳的執行效能,對此結論我會持相當保留的態度。

工作方式與習慣問題

有句諺語說,只會用鐵槌的木匠,看到甚麼東西都覺得是釘子,會想敲它一下。的確,每個人都會盡量用自己熟悉的工具來解決問題(我也是)。這便產生了另一個問題。

以我自己碰到的情況來說,開發團隊經過效能調校和檢視 log 之後,發現有一位 SA 負責的子系統都跑得特別慢,只要有使用者上線執行他負責的子系統,整個系統效能就 down 下來,變成龜速,甚至導致應用程式伺服器完全無法回應。進一步查看其程式碼,發現該子系統的程式很多都是超過 10 個 subquery 的 SQL,也就是說,那位 SA 開的規格書大都如此,所有資料查詢的處理都完全用複雜 SQL 指令一次解決,而未曾想過有些情況可能用程式碼處理比較適當。更令人驚訝的是,有些複雜 SQL 就只是取一兩個 count 值而已(select count(*) from many, many, many subqueries),而且還用到了許多會造成 full table scan 的條件判斷式。不禁納悶:這當中的 subquery 難道是必要的嗎?會不會有在迴圈裡面反覆執行此 SQL 的情況(這會是多大的效能負擔與資源浪費啊!)?

我並不是對 SQL 專家有意見,也並非全然反對寫複雜 SQL。為了提升效能,且實在別無他法時,自然還是得用。但前提是必須把 SQL 指令的 business rule 寫成文件,以便日後維護時可對照著 SQL 拆解、調整。這是為了避免 SA/SD 直接在規格書裡面寫好 SQL,程式設計師不明就裡直接照著 coding 而埋下的炸彈。少了說明文件,日後他人接手維護程式時,有多少人有把握完全理解那些複雜 SQL 的用意並修改它呢?我想這是我對超級 SQL 退避三舍的最主要原因。

小結

或許有人會認為,那我一定不敢用 regular expression,因為用它寫出來的東西更像天書,比 SQL 還難懂。恰好相反,我喜歡用 regular expression。但除非必要,我都盡量找別人寫好的字串樣板來用,而不自己動手寫。因為網路上已經有許多現成的了,而且這種東西不像 SQL 指令那樣對系統的整體效能和資料的正確性有那麼大的影響,如果發現目前使用的字串樣板有缺點,大不了再找一個,或者自己想辦法稍微改一下就行了。

最後,容我耍點引用他人論述來支持自己觀點的小伎倆。在《Release It! Design and Deploy Production Ready Software》這本書裡,作者進一步建議應盡量使用 O-R mapping(物件-關聯對應)框架,而不要手工打造 SQL,其理由與前面談的問題有點關係。且摘錄部分內容如下:

The problem is that developer-crafted SQL tends to be so idiomatic and unpredictable. All the database tuning for the rest of the application won’t help for these beastly one-offs.
開發人員手工打造的 SQL 往往帶有濃厚的個人色彩,而且無法預測。就算對應用程式的其餘部分進行資料庫調校,也無法減輕這類怪獸級 SQL 帶來的傷害。

What makes these handcrafted SQL queries so bad? They usually suffer from a handful of common mistakes. First, they often join on nonindexed columns. Second, they usually join too many tables.Third, developers .... will issue some query that joins five tables to select exactly the one row they're looking for, and then they'll issue that query 100 more times to find other individual rows.
手工打造的 SQL 容易犯幾個毛病:第一,它們常常 join 非索引欄位;第二,它們通常都 join 太多資料表了;第三,開發人員可能會單單為了取得一筆資料列而 join 五個資料表,而且可能還重複執行該 SQL 指令一百次以上,以便取得其他特定的資料列。

《I. M. Wright's "Hard Code"》第二章閱讀札記

1/08/2009
延續上一篇,這裡寫一點第 2 章<流程改善,沒有魔法>的筆記。

精實(Lean)製程定義了七種會破壞客戶價值流的浪費:
  • 過度生產(overproduction):開發太多不重要、甚至無用的功能。
  • 傳遞成本(transportation):專案建置(build)、版本分支(branch)、和團隊成員之間透過 e-mail 溝通等傳遞成本。
  • 多餘動作(motion):花在找資料、找解法、搞清楚要做甚麼等動作的時間太多。
  • 等待時間(waiting):系統功能的優先順序沒訂好、開發流程不當、品質不良等因素,導致各開發小組之間彼此等待。
  • 過度處理(overprocessing):把軟體功能設計得太複雜、對已經跑夠快的程式不斷做效能最佳化的調整、增加不必要的擴充等等。
  • 庫存過剩(inventory):矇著頭開發了一堆功能,後來發現有很多都白做了,在系統上線前就被拿掉,所以客戶也看不到這些功能--未實現的價值是種浪費。
  • 產品瑕疵(defects):太多 bugs 將令客戶失去信心,也會造成重工(rework)。
深度 vs. 廣度

作者認為,花太多時間專注在開發底層架構和元件等「基礎設施」上,可能會讓客戶久候,苦等不到他們想要的東西。為了經常獲得客戶的回饋意見,快速反覆(iteration)有個基本前提:開發應該「深度優先,而非廣度優先(depth first, not breadth first)。

廣度優先意味著包羅萬象,要求納入所有能想到的功能,並對每一項功能進行設計,設計階段完成後再全部一起實作,然後一起測試。深度優先則著重於把單一功能做好,完成了某項功能的設計、實作、測試之後,才繼續處理下一個功能。兩種極端都不好,但是對大部分的開發團隊來說,最好是先做一次高階的廣度設計,之後就立刻改以深度優先的方式進行底層的設計與實作。

的確,我覺得偏好深度優先的另一層意義是對品質的堅持,而不是以量取勝、上(線)了再說的心態。因為開發人力和時間都很寶貴,如果想要全都顧到,很可能結果就是每項功能的品質都差了點。一次交付預定的那些功能,專案進度表自然是交代得過去,可是當客戶開始使用時,可能會發現怎麼跟當初期望的不太一樣:好像每一項功能都做了,但又沒幾個讓人覺得滿意的。開發團隊並沒有真正實現當初對品質的承諾。

如果專案經理光是盯著進度表,只求達到表面上的實現承諾,卻從不認真思考軟體品質與維護成本的問題,我覺得是相當短視、而且不負責任的作法。這種領導風格可能一開始會讓客戶和老闆很高興,但是胡亂承諾、扮好人的結果,往往會讓後面維護的人疲於奔命:接客戶的抱怨電話、除 bug、改規格、加需求;每做一個專案就在團隊腳上綁一塊大石,到最後整個團隊精疲力竭,只能選擇抽身離去。

喔,又忍不住寫太多感想了,就此打住。

舊文重發:Windows 表單與多執行緒

1/01/2009

本文原刊載於 .NET Magazine 中文版 2005 年 12 月號,現在將此舊文重發,只是為了方便參考。

摘要

在撰寫多執行緒的 Windows 表單應用程式時,有一項必須特別注意的規則,就是不可以在工作執行緒(worker thread)當中修改表單或控制項的屬性與方法。本文說明這項規則的由來,以及違反此規則將造成的後果,同時示範錯誤的以及正確的程式撰寫方式。

在撰寫多執行緒的 Windows 表單應用程式時,有一項必須特別注意的規則:只有在建立該表單(或控制項)的執行緒中,才能存取、修改表單(或控制項)的內容。如果讀者到MSDN Library查看Control.BeginInvoke方法的說明,也可以得到類似的訊息(雖然不是很明顯),其中有一段備註是這麼說的:「在任何執行緒裡面可以安全呼叫的函式只有四個:Invoke、BeginInvoke、EndInvoke、和 CreateGraphics。」進一步的解釋這句話,它的意思就是:除了上述四個方法,您在任何執行緒當中存取或修改 Control 類別的屬性或方法時,都有可能導致程式發生不預期的錯誤,例如:畫面沒有正常更新、程式當掉等等。而由於Form類別也是繼承自Control類別,因此,就如本文一開始所說的,如果您想要存取表單或控制項的屬性或方法,這些程式碼一定要執行於建立該表單或控制項的執行緒當中,通常這個執行緒就是應用程式預設的執行緒,又稱為主執行緒。

為什麼會有這項規則?簡單的說,是因為.NET控制項在設計時就是執行緒相依的(thread affnity),因此您只有在建立該表單或控制項的執行緒當中,才能安全地存取其屬性。但這樣的說明仍不足以讓我們了解背後真正的原因,在本文中,筆者將會詳細說明這項規則的來龍去脈,如果不遵守這個規則會造成什麼後果,並且示範正確的程式撰寫方式。讀者如果對執行緒的基本觀念還不熟悉,建議您參考 2002 年九月號由胡百敬老師所撰寫的文章。

Windows 表單與 Win32 視窗的關係

首先必須先了解的是 Windows 表單(即 Form 類別)與執行緒之間有什麼關係,以及它們的運作方式,由於 Windows 表單實際上是基於傳統的 Win32 視窗技術以及訊息傳遞的機制,因此這個小節筆者就先從表單與 Win32 視窗的關係談起,請讀者注意在本節當中的表單(form)跟視窗(window)這兩個名詞所代表的是不同的東西。

當您建立一個新的表單(form)時,只不過是建立了一個.NET物件,其實這時候並沒有建立任何視窗,這個表單物件的建構函式所執行的工作只是初始化物件的內部資料(欄位成員、事件處理常式),如此而已。我們所看到的視窗(Win32視窗)則是在表單第一次顯示時才會真正建立起來,整個過程是這樣的:在設定表單的 Visible 屬性為 True 時,該屬性的 set 方法會呼叫 SetVisibleCore 方法,此方法會讀取表單的 Handle 屬性,而 Handle 屬性的 get 方法會檢查視窗是否已經建立,若已建立,就傳回視窗handle,否則便呼叫 CreateHandle 方法,視窗便是在這個時候建立起來的。程式碼列表1是筆者利用 Reflector 工具反組譯 .NET Framework 的結果,讀者可以對照剛才的描述,應該能更清楚了解整個建立視窗的過程。

 程式碼列表 1:Windows 表單應用程式的 Main 方法
   1:  // Form 類別的 SetVisiblCore 方法.
   2:  public class Form : ContainerControl
   3:  {
   4:    protected override void SetVisibleCore(bool value)
   5:    {
   6:      // ....省略其他不相關的程式碼
   7:    
   8:      UnsafeNativeMethods.SendMessage(new HandleRef(this, base.Handle), 0x18, value ? 1 : 0, 0);
   9:      // 上面這行在讀取 base.Handle 時,就會呼叫 Control 類別的 Handle 屬性的 get 方法(見下方)。
  10:    }
  11:  }   
  12:  
  13:  // Control 類別的 Handle 屬性.
  14:  public class Control : Component
  15:  {
  16:    public IntPtr Handle
  17:    {
  18:      get
  19:      {
  20:        if (this.window.Handle == IntPtr.Zero)  // 若視窗 handle 尚未建立
  21:        {
  22:          this.CreateHandle();                // 建立視窗 handle
  23:        }
  24:        return this.window.Handle;
  25:      }
  26:    }
  27:    //...省略其他成員
  28:  }

因此,建立表單與建立Win32視窗的過程,在內部的運作上其實是分成兩個階段:(1) 建立表單物件;(2) 建立視窗,而建立視窗的動作則是經由設定表單的Visible屬性為True所引發。這種運作方式意味著表單的建構函式裡面不可以存取跟視窗handle有關的變數(因為當時視窗根本還沒建立起來),而且不只是表單而已,所有具有視窗handle的控制項都適用此規則。這對開發人員有什麼影響?當您在設計自訂的控制項時,可能要特別留意,那些跟視窗有關的資料必須以另外的資料結構(例如:陣列)儲存起來。以ListView控制項為例,當您要加入項目時,是透過Items屬性的Add方法,此方法會把加入項目的工作委派給ListView.InsertItem方法,InsertItem則會先檢查看看這個ListView控制項的視窗(注意:不是表單)是否已經建立了,若已建立,就送出LVM_INSERTITEM訊息給控制項,告訴它要加入一個新的LVITEM;若控制項尚未建立,那麼這些項目就會先儲存在一個ArrayList裡面,等到控制項的視窗建立時,便會觸發HandleCreated事件,在此事件中會把事先儲存的ArrayList內容全部加入到控制項裡面。當您在設計自己的控制項時,也很可能會需要用到這種技巧,即先把控制項所需的資料暫存到別處,等到控制項的視窗建立時,再把事先儲存的資料塞給控制項。聽起來挺麻煩的,不是嗎?還好我們通常不會需要從無到有設計一個新的控制項。

從以上的說明可以知道,表單的視窗只有在第一次設定Visible屬性為True時才會建立,可是我們平常在撰寫Windows表單應用程式時,並沒有去設定Form的Visible屬性,那這個屬性到底是什麼時候、在哪裡設定的?記得Windows表單應用程式的Main方法裡面都會類似程式碼列表2的程式碼。

 程式碼列表2:Windows表單應用程式的Main方法
  static void Main()
{
Application.Run(new Form1());
}
所有建立視窗的動作就是從程式碼列表1裡面的Application.Run() 開始。Application.Run() 會啟動一個訊息迴圈(message pump),在這個訊息迴圈裡面,會呼叫Win32 API的GetMessage函式,以便從執行緒的訊息佇列裡面取出下一個視窗訊息,接著再呼叫DispatchMessage函式將訊息傳送給目標視窗。簡言之,由主執行緒所建立的訊息迴圈會不斷從主執行緒的訊息佇列提取視窗訊息,並發送至應用程式中的各個視窗(或控制項)。請注意「執行緒的訊息佇列」這句話,如果視窗建立在另一條執行緒,那麼它的訊息就會被送到該執行緒的訊息佇列裡。Windows表單應用程式的視窗訊息處理的過程如圖1所示,當使用者按下「測試」按鈕時,系統會將這個按鈕事件轉換成一個滑鼠點擊的訊息存放到執行緒的訊息佇列中,應用程式則藉由訊息迴圈從訊息佇列中取出訊息,並且分派到對應的控制項(此例的「測試」按鈕),以便進行對應的處理(例如:將按鈕畫成凹陷的樣子)。


兩種訊息傳送機制

現在我們知道,當控制項的狀態改變時,系統會將它轉換成視窗訊息,放到訊息佇列裡面,再由程式取出並處理。而這個將訊息放到訊息佇列的動作,又有兩種不同的作法,一個是透過SendMessage函式,另一個是PostMessage函式。這兩種送訊息的方式有個主要的差異:SendMessage會令呼叫端停住,直到該訊息被處理完畢,程式的控制權才會回到呼叫端,而 PostMessage則是將訊息丟到訊息佇列後就立刻返回呼叫端。因此,應用程式實際上會有兩個訊息佇列,分別存放SendMessage和PostMessage送出的訊息,而訊息迴圈會優先處理SendMessage的訊息佇列,等這些訊息都處理完了,才會去處理PostMessage的訊息佇列。那麼,當 .NET控制項的屬性變更時,是採用哪一種傳送訊息的方法呢?
在.NET裡面,所有表單或控制項的屬性變更──不管此變更是由使用者操作時所觸動的,還是由程式去更改控制項的屬性或呼叫其方法──都會轉換成對應的視窗訊息,並且利用SendMessage送出該訊息。而文章前面也提到,在呼叫SendMessage時,必須等到送出的訊息被處理完畢,才會返回呼叫端繼續往下執行,這就是為什麼在撰寫多執行緒的Windows表單應用程式時,只能在主執行緒(也就是建立表單與控制項的執行緒)當中修改控制項屬性的主要原因。
舉個例子來說,假設現在應用程式有兩條執行緒,一條是主執行緒(main thread),另一條稱它為工作執行緒(worker thread),如果主執行緒正在等工作執行緒完成,而您又在工作執行緒裡面嘗試更新某個控制項,也就是呼叫了SendMessage來更新某個控制項的狀態,於是SendMessage會令工作執行緒停住(blocked),因為它必須等待主執行緒把SendMessage送出的訊息處理掉,可是這時候主執行緒卻因為在等待工作執行緒執行完畢,而沒有去處理訊息佇列裡面的訊息,結果便形成了兩條執行緒相互等待的情形,程式就當掉了。

範例實作

接著我們來撰寫一些簡單的測試程式,看看如果不按照之前建議的規則撰寫程式,會發生什麼情況。先在 Visual Studio .NET 2003 中建立一個新的 Windows 應用程式專案,然後在 Form1 上面各放一個 Button 和 Label 控制項,將按鈕名稱改為 btnTestThread。當按下按鈕時,建立一條工作執行緒,並且等待該執行緒執行完畢,而工作執行緒在執行完畢時會呼叫UpdateUI函式,此函式的用途是更新控制項的狀態,這裡筆者只是簡單的把執行該程式碼所在的執行緒名稱用 Label 控制項顯示出來,請參考程式碼列表3。

 程式碼列表3:範例一
   1:  using System.Threading;
   2:  //....
   3:  
   4:  private void btnTestThread_Click(object sender, System.EventArgs e)
   5:  {
   6:    // 為表單所在的執行緒取個名字,方便判斷程式執行於哪個執行緒。
   7:    Thread.CurrentThread.Name = "主執行緒";
   8:  
   9:    // 建立並且啟動工作執行緒。
  10:    Thread workerThread = new Thread(new ThreadStart(ThreadMethod));
  11:    workerThread.Name = "工作執行緒";
  12:    workerThread.Start();
  13:  
  14:    // 等待 workerThread 執行完畢。
  15:    workerThread.Join();
  16:  }
  17:  
  18:  private void ThreadMethod()
  19:  {
  20:    // 延遲一下,假裝執行了一項費時的工作。
  21:    Thread.Sleep(1000); 
  22:  
  23:    // 更新 UI。
  24:    UpdateUI();
  25:  }
  26:  
  27:  private void UpdateUI()
  28:  {
  29:    label1.Text = Thread.CurrentThread.Name + ": Work is done!" ;
  30:  }

在這個範例當中,筆者分別為主執行緒和工作執行緒設定了Name屬性,這可以讓我們很容易辨識某段程式碼是執行於哪個執行緒當中(以往撰寫Win32程式時,必須利用執行緒識別碼的一組數字來辨別,比較不方便),執行的結果如圖2所示。



從圖2當中可以看到,UpdateUI 函式是執行於工作執行緒的,而且,若讀者實際執行這個範例程式,會發現程式並沒有發生什麼怪異的現象,Label的Text有正常更新顯示,程式也沒有當掉。可是,前面不是說:不可以在其它執行緒當中修改控制項的屬性嗎?為什麼這樣寫程式還能運作無誤?這也是撰寫多執行緒應用程式的一個陷阱,如果一開始用簡單的程式去測試,發現違反這項規則其實沒有什麼問題,就這樣繼續寫下去的話,當程式發展到某種規模時,可能才會出現問題,到時候要除錯或修改程式就更麻煩了。因此,建議讀者在撰寫程式時還是應該遵守這項規則。

為了讓讀者實際體會一下,這裡筆者再利用一點簡單的技巧,讓範例一的程式發生兩條執行緒相互等待而導致當機的情形,只要將btnTestThread_Click函式的最後一行,也就是呼叫workerThread.Join() 的敘述改成如程式碼列表4那樣就行了,這是因為Thread的Join方法雖然會讓主執行緒等待工作執行緒執行完畢,但主執行緒仍然能夠處理訊息佇列中的訊息,而修改後的程式碼用一個空的迴圈等待工作執行緒結束,主執行緒就只能等待,而無法處理訊息佇列了。

 程式碼列表4:修改自程式碼列表3,會讓兩條執行緒相互等待造成當機
   1:  private void btnTestThread_Click(object sender, System.EventArgs e)
   2:  {
   3:    // 為表單所在的執行緒取個名字,方便判斷程式執行於哪個執行緒。
   4:    Thread.CurrentThread.Name = "主執行緒";
   5:  
   6:    Thread workerThread = new Thread(new ThreadStart(ThreadMethod));
   7:    workerThread.Name = "工作執行緒";
   8:    workerThread.Start();
   9:  
  10:    // 讓程式空轉,等待 workerThread 執行完畢。
  11:    while (workerThread.ThreadState != ThreadState.Stopped)
  12:    { }
  13:  }

程式碼照列表4修改之後,執行時按下按鈕就會讓程式當掉,原因前面已經解釋過了,讀者也可以試著把UpdateUI函式裡面的修改label1.Text屬性的程式碼註解掉,再執行看看,程式便不會當掉,這也可以證明如果在工作執行緒裡面沒有修改表單或控制項的屬性,是不會造成當機的。

最後,我們再把前面的範例改成正確的寫法,請參考程式碼列表5,跟前面的程式碼不一樣的地方,主要是ThreadMethod函式。

 程式碼列表5:正確的寫法
   1:  private void btnTestThread_Click(object sender, System.EventArgs e)
   2:  {
   3:    // 為表單所在的執行緒取個名字,方便判斷程式執行於哪個執行緒。
   4:    Thread.CurrentThread.Name = "UI thread";
   5:  
   6:    Thread workerThread = new Thread(new ThreadStart(ThreadMethod));
   7:    workerThread.Name = "工作執行緒";
   8:    workerThread.Start();
   9:  
  10:    while (workerThread.ThreadState != ThreadState.Stopped)
  11:    {
  12:      // 空轉等待 workerThread 執行完畢
  13:    }
  14:  }
  15:  
  16:  private void ThreadMethod()
  17:  {
  18:    // 延遲一下,假裝執行了一項費時的工作。
  19:    Thread.Sleep(1000); 
  20:  
  21:    // 安全地更新 UI
  22:    if (this.InvokeRequired) 
  23:    {
  24:      MethodInvoker mi = new MethodInvoker(this.UpdateUI);
  25:      this.BeginInvoke(mi, null);
  26:    }
  27:    else 
  28:    {
  29:      UpdateUI();
  30:    }
  31:  }
  32:  
  33:  private void UpdateUI()
  34:  {
  35:    label1.Text = Thread.CurrentThread.Name + ": Work is done!" ;
  36:  }

在程式碼列表4當中,關鍵的敘述在於ThreadMethod函式裡的 this.BeginInvoke() 敘述,這個BeginInvoke其實就是本文一開始引述MSDN Library文件時提到的,在任何執行緒當中都能安全地呼叫的那四個方法中的其中一個。這裡的this指的是Form物件,由於Form繼承了Control類別,因此也有BeginInvoke方法,此方法的作用是:將傳入此方法的委派方法執行於主執行緒中,也就是說,雖然ThreadMethod方法執行於工作執行緒,但是透過Control.BeginInvoke() 執行的委派方法,會切換到主執行緒當中執行。由於這裡的UpdateUI函式沒有傳回值也沒有傳入參數,因此就直接使用.NET 提供的MethodInvoker委派型別,讀者也可以視需要使用自訂的委派型別。


另外值得注意的是,在呼叫 this.BeginInvoke() 之前,程式先檢查了this.InvokeRequired屬性,當此屬性為true時,表示目前所在的執行緒不是主執行緒,因此才需要多費一番功夫去呼叫this.BeginInvoke();若InvokeRequired為false,就可以安全地更新控制項,而不用切換到主執行緒了。執行結果如圖3所示,讀者可以跟圖2比較一下,這回label1的訊息顯示的是「主執行緒」,可以看出更新控制項的動作確實被切換到主執行緒當中執行了。


結語

撰寫多執行緒的應用程式是一項挑戰,在寫程式時必須比撰寫單一執行緒的應用程式更加小心,要處理的問題包括資源爭用、同步化、以及本文所介紹的,Windows表單應用程式需要特別遵守的規則:不可以在主執行緒之外的執行緒當中修改控制項的屬性。筆者在本文中說明了這項規則背後的原因,同時也說明了表單與Win32視窗之間的關係以及訊息傳遞的機制,最後以實際的範例展示正確的程式撰寫方式,希望本文對於要開發多執行緒的Windows表單應用程式的讀者有一些幫助。
技術提供:Blogger.
回頂端⬆️