2013年10月31日 星期四

Round 8: Exception - fail fast

Backlog中仍有SP2與SP7,這一 round 我們選SP7:建立vector與matrix超限功能與測試。

U步驟:

超限測試是vector::operator [] 與 matrix::at極為重要的面向。以vector::operator []為例,由於當初我選擇以operator [] 取用向量的分量,一開始向量v的第一分量為v[0],後來認錯修正為v[1],這樣的歷程即便是你自己都有可能記錯。於是,你有可能仍用v[0]表達向量v的第一分量 -- 超限了。

簡單的說起來,我們需要在取用向量的分量之前檢查operator []的傳入參數是否超限。如是,則應避免傳值回去。這種情況類似於先前計算innerProduct的函數,


bool innerProduct(double &product, const vector &u, const vector &v);

所以,一種解法是仿照innerProduct的函數的做法,傳回bool並引進一個參數作為回傳結果。

問題來了,由於operator []是C++ 內建運算子,只能接受一個輸入參數,且返回型態在這裡必須是 double:


double & operator [](int i) const;

所以無法套用innerProduct的函數的做法!我們得另想辦法。

幸運的是C++及其他較新的程式語言如Java、Python、Scala等,均提供表達與處理例外狀況的機制(exception handling mechanism)如果將超限視為一種例外狀況,則 operator []可檢查(detect)是否超限,如是則拋出例外(throw an exception),表示超限已發生, 並終止正常函數執行:

double & vector::operator [](int i) const {
if (i <=0 || i > _dim )
throw std::string("Index to vector out of bound!");
return _v[i-1];
}

C++ 提供一個exception class,用來表達例外。我們可以利用這個型態,但是代價為較為複雜些。幸好,C++容許拋出任意型態的例外,例如int、const char* (C string)、string等。這裡,vector::operator[]可拋出的例外型態為string。

呼叫operator []的敘述,則可放置在 try 敘述 (try statement)try block之中,並由緊跟在後的catch block加以捕捉:

try {
double component = u[0];
}
catch (string s) {
CHECK(string("Index to vector out of bound!") == s);
}

注意 catch敘述是以型態比對的方式捕捉例外。

如超限例外未被捕捉(例如,呼叫operator []的敘述未被放置於try statement或無對應的catch block加以捕捉),則該例外將繼續上傳至前一個做出呼叫的的函數,依此類推,直到到達main。如main未加以捕捉,則C++ runtime將會終結程式。

D步驟:

我們將以string作為operator []超限例外。工作列出如下

T31:寫vector::operator []超限測試。
T32:vector::operator []超限檢查,及拋出例外
T33:決定如何處理vector::operator []超限例外,做必要的程式修改
T34:寫matrix:at超限測試。
T35:matrix:at超限檢查,及拋出例外
T36:決定如何處理matrix:at超限例外,做必要的程式修改

C步驟:

T31:operator []超限測試。

增加兩個超限測試,期望超限發生時,拋出的例外物件為string("Index to vector out of bound!")。

TEST (index_outofbound_low, vector){
vector u(2);
try {
double ccomponent = u[0];
}
catch (string s) {
CHECK(string("Index to vector out of bound!") == s);
}
}

TEST (index_outofbound_high, vector){
vector u(2);
try {
double ccomponent = u[3];
}
catch (string s) {
CHECK(string("Index to vector out of bound!") == s);
}

}


T32:operator []超限檢查,及拋出例外

double & vector::operator [](int i) const {
if (i <=0 || i > _dim )
throw std::string("Index to vector out of bound!");
return _v[i-1];

}

注意到 throw std::string(...)了嗎?如果只寫 throw string("...")則發生compilation error,原因是vector有一個同名的函數 string(),故以std::string(...)加以區別。另一種做法是將vector::string()改名,例如改為vector::str()。

T33:決定如何處理超限例外,做必要的程式修改
operator []變更,影響innerProduct.exe 與 linearTransform.exe。我們需要決定當vector index超限發生時,該如何處理?

首先,我們先問,使用者會不會引發超限例外?如果會,則超限例外處理的方式,需要顯考慮使用者。

以innerProduct.exe為例,程式並不會讓使用者有直接引發這個例外的機會

接著我們問,使用vector::operator []的programmer會不會所引發超限例外?注意,這裡programmer可能是你,或者其他使用你寫的vector class的programmer。同時,這個超限例外極容易發生:由於後來從1開始的使用方法顯然與operator [] 的從0開始慣用法不合,發生超限的機率就更大了。

Programmer引發這個超限例外時,我們可以說這個例外使因為程式的bug造成的。有bug的程式不該給客戶用。所以,這個決定很容易做:提示programmer這個情形後終結程式,讓programmer修復造成超限的code。

決定了超限例外處理方針之後,下一個決定是由哪一個函數來提示programmer這個情形後終結程式被賦予責任處理這個例外的函數,它的code將因增加try statement而變複雜。根據前面關於例外處理的描述,最簡單的作法是不要做任何事:
超限例外發生時,任由C++ runtime 異常終止 main函數。

以下在main中使用者輸入第一個向量之後,故意讓超限發生,程式執行不正常終止的畫面:




我寫的程式提供你參考。畫面中告訴我們程式在拋出 'std::string' 之後異常終結,並請我們連繫開發者。不論對使用者或programmer,以此方式處置一個bug,應屬恰當。

T34、T35、T36的工作請你試試看。

L步驟:

1. vector::operator [] index 由1而非0開始,有違慣例,對programmer而言,vector::operator []的使用已成bug的溫床。請將vector::operator []改名,例如vector::component(int i)或vector::at(int i)。請你試試看。

2. 前述超限例外,如果我們讓使用者輸入特定維度,為它取得向量在該維度分量,則使用者將有直接引發這個例外的機會,如果這種情形發生,程式應適度提示使用者,使他有機會更正此項錯誤,並繼續正常執行。同理,innerProduct程式有哪些地方會有此類使用者引發的錯誤?這些地方都可以改以例外的方式處理。 

© Y C Cheng, 2013. All rights reserved.

2013年10月27日 星期日

Round 7: Testing improves design

U步驟:

在測試議題上,根據Round 6回顧,列出問題如下:
SP6:建立vector與matrix輸出的單元測試。
SP7:建立vector與matrix超限功能與測試。

讓我們先解決SP6。vector物件的輸出函數在實作上讀取u的分量,適當的符號裝飾,直接使用C++ 輸出物件 cout 進行輸出:


void outputVector(vector u){
cout << "[";
for (int i=1; i<u.dim(); ++i)
cout << u[i] << ",";
cout << u[u.dim()] << "]" << endl;
return;
}

我們的問題是如何測試?前次L步驟說不該直接讓cout輸出「污染」測試輸出畫面,所以不能直接在測試中呼叫 outputVector。

結論:outputVector的可測試性(testability)不佳。SP6的目標就是改善outputVector與outputMatrix的可測試性。

D步驟:

如何改善outputVector的testability?解決方法之一,是以間接的方式測試:先讓vector u轉成一個可測試且容易輸出到cout的中間表示型式(intermediate form),再將該型式輸出到cout。假設後者均能以C++內建的方式做到,則能測試vector u轉出的中間表示型式,即能測試outputVector!

有了這個解決方案,接下來就是決定vector u轉出的中間表示型式。由於outputVector輸出是給使用者看的,直覺上字串(string)就是一個適用的中間表示型式。

列出待辦工作如下:

T23 測試 vector轉成字串。
T24 撰寫vector轉成字串的函數。
T25 修改 outputVector。
T26 迴歸測試。
T27 測試 matrix轉成字串。
T28 撰寫 matrix轉成字串的函數。
T29 修改 outputMatrix
T30 迴歸測試。

相信你已經知道如何列出這些工作了,同時這些工作已經難不倒你了!

C步驟:

完成T23、T24、T25的相關程式片段如下。
(學習課題:string, ostringstream)

T23:
TEST (outputVector, vector){

double a[3]={1,2,3};

vector u(3,a);

CHECK(string("[1,2,3]")==u.string()); }

T24:in vector.h

class vector {

...

public:
...
std::string string() const;
};

in vector.cpp

string vector::string() const {

ostringstream oss;

oss << "[";
for (int i=1; i<= dim()-1; ++i)
oss << (*this)[i] << ",";
oss << (*this)[dim()] << "]";
return oss.str();
}

T25:
void outputVector(vector u){
cout << u.string() << endl;
return;
}


T25得到的 outputVector 對當前程式功能似乎夠了,但稍加思考,不難發現它有些限制,例如輸出兩個向量

vector u(...);
vector v(...);
outputVector(u);
outputVector(v);

後二行code看起來有些小題大作。為什麼不寫成

cout << u << "\n" << v << endl; // u v 間有換行
或者
cout << u << " " << v << endl; // u v 間無換行

這看起來是個好想法,而且並不難做到,我只要 overload operator << 即可:

ostream & operator << (ostream & os, const vector & u){
return os << u.string();
}

同意嗎?matrix相關工作請自己試試看,可參考我寫的程式

L步驟:

此round我們出發時,面對的是一個測試的問題。不容易測試的函數,通常隱含著其他的問題。工作T24完成後,outputVector函數變得可測;T25完成後,你可以將 vector透過標準的operator << 輸出到 cout以外的輸出串流(output stream),如檔案(使用ofstream)、字串(使用ostringstream)等。此時outputVector即無存在的必要!故有本篇的title: 

Testing improves design.

此外,我可以說

Design to enable testing. 

© Y C Cheng, 2013. All rights reserved.

2013年10月11日 星期五

Round 6 continued: Member function or ordinary function?

接下來實作T20,線性轉換的unit test。我們需要決定線性轉換函數為matrix的member function

matrix m(...);
vector v(...);
vector u = m.transform(v);

或者一般函數

matrix m(...);
vector v(...);
vector u = transform(m, v);

功能上二者完全一樣,何者較佳?

  • 如採member function,則matrix 認得(knows) vector :vector v被當成參數傳入transform,且transform需傳回一型態為vector的值。當vector有變動時,vector test專案與matrix test專案均需被編譯、執行。
  • 如採一般函數,則matrix與vector維持獨立(independent)當vector有變動時,matrix test專案不需被編譯、執行。

下圖顯示這兩種相依關係(dependency)。左手邊的作法增加matrix對vector的耦合度(coupling),右邊則無。因此,我們決定transform將以一般函數型式存在。套用物件導向設計的說法,左手邊的作法將transform的責任(responsibility)指派(assign)給matrix,右邊則無。由此可知,

     是否增加class耦合度乃為責任指派的重要考慮因素。




接下來的問題是將transform函數放置在哪一個檔案?如與matrix同置(collocate),需在matrix.h 引入vector.h

#include "vector.h"

經過前一項不增加耦合度的設計決定後,這看起來不是一個好的安排。所以:將transform函數安置放在另二個新的檔案 linearAlgebra.h 與 linearAlgebra.cpp,建立 linearAlgebra test專案。這個專案在vector或matrix有變動時,需被編譯、執行以確保正確。

接著實作T19,含transform與rowVector兩個一般函數。transform的演算法如下(設輸入矩陣為M向量為v,輸出向量為u):

for i in 1 to M.row()
  get ith row vector of M
  u[i] = inner product(ith row vector,  v)

這是根據線性代數教科書的所寫的算法。設M為3x2矩陣,則它有三個列向量:第1、2、3列向量。u的第1個分量為第1列向量與v的內積,etc.

你發現問題了嗎?先前vector u 的第一個分量為u[0]而非u[1],或許當時受了使用index operator []的影響,並不覺得是一個問題,現在做線性轉換,卻有些出入。如何解決?有兩個方法:
  • 實作transform的演算法如下
    for i in 0 to M.row()-1
      get (i+1)th row vector of M
      u[i] = inner product((i+1)th row vector,  v)
  • 承認過去的設計有誤,修改vector::operator [],使第一分量為u[1],第二分量為u[2], 依此類推。
選擇第一個,則演算法不易被看懂(特別是對使用你的程式的工程師們),選擇第二個,則對現有使用到vector::operator[]的code有直接的衝擊。如何選擇?

我選擇承認過去的設計有誤,修改vector::operator [],雖然衝擊較大,但更正了一項意外的錯誤,並使得transform演算法的實作易懂。T19可細分為下列工作:

1. 修改vector test中直接關於operator []的測試。修改後,這些測試應會fail。
2. 修改operator [],直到operator []的測試通過。
3. 修改其他使用到operator []的測試。
4. 修改被測函數、成員函數,直至所有測試通過。
5. 測試、修正innerProduct專案,直至通過。
6. 實作transform與rowVector函數,直至linearAlgebra test通過。

最後完成T21, 建立 linear transformation project,實作主程式。

現在我們用了 vectorTest、matrixTest、linearAlgebraTest、inner product與 linearTransform等五個專案。這五個專案在DEV C++各自存在,但共用一些檔案。例如在專案vectorTest下改變了vector.cpp,則需將其他使用vector.cpp的專案,分別重新編譯、執行;這將會造成一定程度的不便,例如執行迴歸測試。我們需要的是在vector.cpp有所改變時,所有使用vector.cpp的程式自動被重建。

著眼於減少工作上的不便,追加一個工作:

T22:將五個合併專案為一個專案,建立makefile,讓Dev C++使用這個makefile而不每次重新產生 makefile。

我的做法是使用在C/C++程式開發中最常用的建置工具make。 make 讓你將程式建置的指令以目標(target)與相依關係(dependency)編寫在於一個建置腳本(build script),按照慣例,這腳本放在一個名稱為makefile的檔案中。

事實上,之前的五個專案,Dev C++根據你提供的專案資訊自動為你產生一個檔名為 Makefile.win的建置腳本。我提供makefile 即是參考這個檔案修改所得。

有了makefile以後,建立一個新專案 LinearAlgebra,加入所有延伸檔名為 .h 與 .cpp的所有檔案,並在Project options-> Makefile選項下勾選 "use custom makefile"並設定建置腳本檔名(即 makefile)即可。

你可以試著修改vector.cpp檔案。按下Dev C++ compile 鍵,所有依賴vector.cpp的建置目標vectorTest.exe、linearAlgebraTest.exe、innerProduct.exe與linearTransform.exe將會自動被重新產生。相對的,matrixTest.exe則與vector.cpp無關而不會被重新產生。

最後,記得移除原先的五個專案檔 (即延伸檔名為.dev的案),T22即告完成。

(學習課題:make)

L步驟:雖然功能不多,但程式越來越複雜(專案多、檔案多)。我們努力的控制dependency,讓變動發生時受影響的範圍盡量縮小。我們也順利地重複使用vector及inner product函數。儘管如此,我們仍需要認真地檢視有無可改善之處。目前,我們的程式具備容易測的架構,讓我們來檢視單元測試的議題。

 1. 單元測試須獨立運作。但我們測試vectorToFile 時,使用了vectorFromFile。雖然vectorFromFile被測過,但並不能保證它完全正確。E W Dijkstra的關於測試的名言:

Program testing can be used to show the presence of bugs, but never to show their absence!

測試只能用來證明bug的存在,而不能證明bug的不存在!

2. 函數outputVector與outputMatrix(m)仍未被測試,事關輸出,如果真的在console輸出,我們的單元測試 Silent unless broken的特性豈不被破壞了?

3. vector::operator []() 與 matrix::at()都未有超限 (out of bound)測試,而其實作亦未偵測。

© Y C Cheng, 2013. All rights reserved.

2013年10月10日 星期四

Round 6: Enters matrix

目前為止,我的例子只有一個class,vector。接下來,我將讓例子裡有兩個class。我仍取材自簡單的線性代數運算。



問題P2:寫一個程式,提示使用者輸入兩個分別存放一個矩陣M與一個向量v的檔案的名稱,其中矩陣M代表一個線性轉換。請計算對向量v施以該轉換所得之向量u。

u= Mv

並將u輸出到第三個使用者指定名稱的檔案。提示使用者可重複執行或結束程式。

限制:你必須儘量使用為inner product所開發的vector類別及相關函數,並在必要時增加相關函數。

U步驟:

問題P2要求的檔案輸入輸出均有C++ library可供使用,且牽涉的計算不複雜,所以我暫時將不分解成數個更小的問題。當然,如同前5輪U-D-C-L,我們後續仍有可能提出改善的sub-problem。

你可能要問我,為什麼不在一開始(round 1)的時候就把問題P2一併提出來?你的疑問很合理,我的確可以在一開始就一併提出向量內積與線性轉換等二個問題。即便如此,我並不需要一次面對個不同的問題;因為這個問題彼此間的邊界(boundary)相當清楚,分開來解是合理的做法。同時,因為解線性轉換問題時須使用向量內積,故這個解題順序仍屬合理。

除此之外,現實世界中,客戶追加需求是相當常見的情形。追加的需求,當然需要站在既有的基礎上,正像前述問題限制我們必須儘量重複使用(reuse)為inner product所開發的vector類別及相關函數。問題P2中,線性轉換u = Mv的算法,u的分量為M相同位置的列向量(row vector)與v的內積。


D步驟:


我們需要針對matrix運算與測試、線性轉換主程式建立相關專案。

由於問題要求重複使用且我們也可能需要修改或新增inner product所開發的code,在專案的安排上,我們需要繼續讓
project inner product、 project vector test維持在開發中的狀態。雖然問題P2並未要求與 inner product有關係,由於vector可能有所變動,所以project inner product需要連帶被編譯、執行以確保其正確性。

列出工作如下:


T13 設計 vector在檔案中的儲存格式,編寫讀/寫檔案函數。
T14 測試vector讀/寫檔案函數。
T15 設計matrix在檔案中的儲存格式,編寫讀/寫檔案函數。
T16 測試matrix讀/寫檔案函數。
T17 編寫 matrix類別,含data members 與 member functions。
T18 測試matrix類別。
T19 編寫線性轉換函數。
T20 測試線性轉換函數。
T21 編寫main函數。

C步驟:
先做哪個task?基於round 5結果,我們有vector test專案,因此可以先顯選擇範圍限制在處理T13(先寫 vector讀寫檔函數)或T14(先寫 vector讀寫檔函數單元測試)。 

我採取 測試先行(test first)策略,先選擇T14。好處有二:

  • 當unit test code完成時,已初步完成vector讀寫檔函數的函數原型設計。
  • unit test執行無誤時,你知道已初步完成vector讀寫檔函數的函數的實作。

T14與T13完成後,編譯、執行inner product project,確認無誤。這個動作為inner product project進行迴歸測試(Regression testing)原有的inner product 執行已達預期狀況,但它使用的class vector與函數變動了,我們需重新加以確認。

(學習課題:ifstream, ofstream, fstream)

接著,完成matrix相關測試、類別與函數,仍採測試先行,依序執行T18、T17、T16、T15。在此之前,我們仿照先前vector的做法,建立matrix test專案。

下篇繼續。

© Y C Cheng, 2013. All rights reserved.

2013年10月3日 星期四

Round 5: Unify when you can; separate when you must

U步驟:第四輪結束時,所有class與function均存放在同一個file main.cpp,如下:

main函數是一個劇烈變動點,經常透過編輯code的方式主功能(production code)測試(test code)間切換。將此問題具體敘述如下:

SP5:分割程式碼,建立測試專案,與production專案分立。

D步驟:要解決SP5,必須要使test code與product code不共用 main function。具體的做法是自innerProduct專案拆出另一個測試專案,暫時稱為 vectorTest專案,並設法讓兩個專案共用 vector, inputVector, outputVector 與 innerProduct,示意如下:


建立待辦工作如下:
T11 分割test code、vector與相關函數
T12 建立test project, 修訂inner product project。

C步驟:

T11 分割test code、vector與相關函數

1. 將main.cc中vector與相關的函數搬到二個檔案,vector.h與vector.cpp,在main.cc中引用 vector.h。編譯,修正錯誤。

2. main.cc中test code搬移至另一個檔案,vectorTest.cpp
vectorTest.cpp中引用 vector.h,編譯,修正錯誤。

(學習課題:為class與函數分別建立 header file(宣告)與 implementation file(定義),亦即 interface in .h file, implementation in .cpp file)
(學習課題:#ifndef, #define, #endif的使用)

T12 建立test project。

1. 建立 test project:vectorTest.cpp code併入 main function所在的mainTest.cpp;設定CppUnitLite 路徑;在test project中加入 vector.h、 vector.cpp;編譯,修正錯誤。

2. 移除原 innerProduct專案之CppUnitLite 路徑設定,移除並刪除vectorTest.cpp。

L步驟:現在,你已擁有 a production project and a test project各一個!接下來將能更容易的為vector及相關函數做更多測試。再度檢視待辦工作,我們發現只剩SP2了。解或不解?請你自己決定。inner product問題,將暫告一個段落。





© Y C Cheng, 2013. All rights reserved.