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.

沒有留言:

張貼留言