Code Complete II《軟體建構之道 2》#7 讀書心得與整理

第七章 高品質常式
Hight-Quality Routines

常式(Routine):
定義:用於「單一目的」的個別方法,或是可以呼叫程序。
例如:C++的function、Java的Method 和 Microsoft Visual Basic的函式或子程序,C/C++的巨集。

什麼是「高品質」的常式?
先來看看什麼是非高品質的例子。
看到這,先找找下面的程式,有幾個問題,緊接著的就是解答囉!
//C++ Example of a Low-Quality Routine
viod HandlsStuff( CORP_DATA & inputRec, int crntQtr, EMP_DATA empRec, cougle & estimRevenue, double ytdRevenue, int screenX, int screenY, COLOR_TYPE & newColor, COLOR_TYPE & prevColor, StatusType & status, int expenseType)
{
 int i;
 for ( i = 0; i < 100; i++){
  inputRec.revenue[i] = 0;
  input Rec.expense[i] = corpExpense[ crntQtr ][i];
 }
 
 UpdateCorpDatabase ( empRec );
 estimRevenue = ytdRevenue * 4.0 / (double) crntQtr;
 newColor = prevColor;
 status = SUCCESS;

 if ( expenseType == 1){
  for( i = 0; i< 12; i++)
   profit[i] = revenue[i] - expense.type1[i];
 }
 else if ( expenseType == 2){
   profit[i] = revenue[i] - expense.type2[i];
 }
 else if ( expenseType == 3)
   profit[i] = revenue[i] - expense.type3[i];
 }

此常式的問題如下:
  • 常式名稱不佳
  • 常式沒有文件化
  • 常式有一錯誤配置(子程序的布局不好)-參考31章
  • 引數中的inputRec被修改。
  • 讀寫全域變數
  • 常式沒有單一目的
  • 常式沒有注意防範錯誤資料
  • 常式使用數個Magic Number
  • 常式沒有使用所有參數
  • 常式參數傳遞不正確:prevColor& 並沒有被更改。(應該設為const)
  • 常式參數過多
  • 常式參數順序混亂,沒有註釋(文件化)
常式的好處:
  • 避免重覆
  • 容易進行 開發、除錯、文件化、維護
(此章節會常出現「文件化」一詞,可解釋為「程式碼說明文件或註解」)


7.1 建立式的充份理由
(Valid Reasons to Create a Routine)


降低複雜性  寫完之後就可以忘記常式細節(忘記常式詳細內容)
採用中繼、容易了解的抽象概念 常式的良好名稱,是說明這段程式最好的方式之一
避免重覆的程式碼
兩個常式建立相似的程式碼 → 程式碼分解(decomposition)出錯

<<法一>>
  1. 提取重覆程式碼
  2. 相同程式碼→基礎類別
  3. 特別程式碼→衍生類別
<<法二>>
  1. 相同的程式碼移到新的常式內
  2. 特別程式碼呼叫這個常式

支援子類別覆寫(subclassing override)
隱藏順序
隱藏指標操作
  1. 確保指標的操作正確
  2. 確定替換指標行為時完整
提高可攜性  常式的「隔離」特性。
簡化boolean判斷 常式「文件化」的特性,可以提高boolean操作的易讀性
改善效能  更容易集中改善某一處程式碼
確保所有的常式都夠小?  既然有這麼多好理由,這一項就不這麼重要了。有些特別的情況,大的常式更適合。

看似過於簡單而無法置入沒有必要寫成 常式的作業操作

(別抗拒建立小函式)
建構整個式以包含兩或三行程式碼,看似小題大作,但將提供莫大的幫助。
//Pseudocode Example of a Calculation
points = deviceUnits * (POINTS_PER_INCH/DeviceUnitsPerInch())
像這樣的一段小程式,改成下面的樣子,其中的變化,為何,你看得出來嗎?
//Pseudocode Example of a Calculation Converted to a Function
Function DeviceUnitsToPoints ( deviceUnits Inter ): Inter
DeviceUnitsToPoint = deviceUnits * (POINTS_PER_INCH/DeviceUnitsPerInch())
End Function
//Pseudocode Example of a Function Call to a Calculation Function
points = DeviceUnitsToPoints( deviceUnits )
程式呈現的方式,是不是更容易看得出point = 會是什麼東西了呢?

小常式的優點:
  • 易於閱讀
  • 自我文件化
  • 重覆使用

小型作業通常會變成大型作業,增加一些特殊處理的情況讓程式碼變大。

常式=名稱+參數+內聚力

7.2 常式層次的設計
(Design at the Routine Level)

(考慮內聚力)
在此考慮的設計角度,是設計常式提供常式給程式設計師使用的設計角度。

最強最佳內聚力
    .功能內聚力
不夠理想的內聚力
    .順序內聚力
Get退休日期( Get年紀( Get生日() ) );
    .溝通內聚力
Update( Get資料() )
Print( Get資料() )
            要注意資料是不是最新的,Print和Update的是不是相同一份最新資料(同步的關係)。
    .暫時內聚力
void 把長頸鹿放進冰箱()
{
    把冰箱打開();
    把長頸鹿放進去();
    把冰箱關起來();
}
        要注意的是,正確的命名
不可取的內聚力
    .程序內聚力
        提供這樣的常式
void 使用者註冊過程()
{
    輸入名字();
    輸入生日();
    輸入密碼();
    確認密碼();
}
        不如提供這樣的常式,還比較好(做了程式設計要做的事)
void 輸入名字();
void 輸入生日();
void 輸入密碼();
void 確認密碼();
    .邏輯內聚力
        建了一個這樣的常式
void fun(int a)
{
    switch(a)
    {
        case 1:  functionOne();   break;
        case 2:  functionTwo();   break;        
        case 3:  functionThree(); break;
        default: functionFour();  break;
    }
}

        為了可以這樣使用(1234順序固定這樣使用)
fun(1);
fun(2);
fun(3);
fun(4); 
        不如直接建立選項常式,而且直接這樣使用,語意來得清楚很多又直接
functionOne();
functionTwo();
functionThree();
functionFour();
唯一允許的情況,就是事情觸發,無法預期輸入的參數為何
fun(EventTrig());
    .偶發內聚力
        重新設計吧!

這些術語,不用太在意,寫出功能內聚力是很容易的,注意力放在概念,而不是這些教條。

7.3 良好的常式名稱
(Good Routine Names)

目的:明確描述出常式的所有工作(所有的輸出和副作用)

避免無意義、模糊或空泛的動詞命令  
名稱空泛的問題,解決方案就是重新建構常式
勿僅用數字區別常式名稱
無法指示常式所代表的不同抽象概念,名稱不佳
常式名稱應盡量詳盡
以容易理解為要
用回傳值的描述來命名函式
用優秀的動詞加物件命名程序
隱含動詞加物件的名稱:
PrintDocument();
CalcMonthlyRevenues();
CheckOrderInfo();
RepaginateDocument();
在物件導向中,呼叫本身就包含物件名稱:
document.Print();
monthlyRevenues.Calc();
orderInfo.Check();
document.Repaginate();
若物件的成員常式包含物件名稱,那在衍生類別上會造成困擾。
正確地使用對立名稱  對立的慣用名稱。
建立常用的操作慣例  實現功能的某種慣用的做法。
這些都是取得ID,但是做法都不相同,若在同一個專案中,會造成使用上的困擾。 勢必要了解某種程度上的了解(這算是違反封裝吧?)才了解如何使用。
employee.id.Get();
dependent.GetId();
supervisor();
candidate.id();

7.4 常式的長度限制
(How Long Can a Routine Be)


出錯率
A = 常式長度 < 100
B = 100 ≦ 常式長度 ≦ 200
C = 200 < 常式長度

出錯率比較結果:
A≒B << C

結論:
常式長度兩百行以下,是安全的範圍,出錯的差異不大。


7.5 如何使用常式參數
(How to Use Routine Parameters)


以輸入-修改-輸出的順序來安排參數
Ada用in和out關鍵字分討輸入與輸出的參數
Ada Example of Parameters in Input-Modify-Output Order
procedure InverMatrix(
originalMatrx: in Matrix;
resultMatrix: out Matrix
);
...

procedure ChangeSentanceCase(
desiredCase: in StringCase;
setance: in out Sentance
);
...

procedure PrintPageNumber(
pageNumber: in Integer;
status: out StatusType
);
考慮建立自己的in和out關鍵字
以下的例子是用C++為例語言,實做這樣的做法
C++ Example of Defining Your Own In and Out Keywords
#define IN
#define OUT
void InverMatrix(
IN Matrix originalMatrix,
OUT Matrix *resultMatrix
);
...

void ChangeSentenceCase(
IN StringCase desiredCase,
IN OUT Sentence *sentenceToEdit
);
...

void PrintPageNumber(
IN int pageNumber,
OUT StatusType &status
)
但是這樣的做法過於特殊,要嘛就是整個專案都這樣
要嘛就是別人看你的Code很奇怪,因為C++的Complier 無法檢查其正確性。

若數個常式使用相似的參數,以一致的順序安排相似的參數
順序不一樣可能讓參數難以記憶
使用所有的參數
將狀態或錯誤變數置於最後
勿將常式參數作為工作變數使用
對於使用常式的使用者來說,他不知道常式內容,所以當然不知道參數進來被改變了什麼。下列是一個不良的例子
Java Example of Improper Use of Input Parameters
int Sample( int inputVal ){
 inputVal = inputVal * CurrentMultiplier( inputVal );
 inputVal = inputVal + CurrentAdder( inputVal );
 ...
 return inputVal;
}
請務必以下例的精神進行設計
Java Example of Improper Use of Input Parameters
int Sample( int inputVal ){
 int workingVal = inputVal;
 workingVal = workingVal * CurrentMultiplier( workingVal );
 workingVal = workingVal + CurrentAdder( workingVal );
 ...
 return workingVal;
}
避免參數清單裡的變數遭到意外修改。
說明有關介面的參數假設條件
說明其值域、轉換的單位、不該出現的值....等。
考慮使用輸入、修改和輸出的命名慣例
一些變數名程開頭的字,例如i_, m_, o_來代表輸入、修改、輸出。(匈牙利命名法)
主要是這些字有隱喻的作用。

常式在傳遞變數或件物時,同時需要維持介面的抽象概念
(這個部份,繁中本的句子亂亂的)
    問題:如何從物件傳遞參數到常式。
    另一種問法:如何將物件的成員傳到常式。

    假設:你有一個「十個公開常式來存取成員的」物件。

這個問題,有兩派的做法
    A做法:function( obj.getMem1(), obj.getMem2(), obj.getMem3() );
        優點:
            維持最少的常式間聯結數
            減少藕合,常式易於重覆使用
        缺點:
            公開常式使用的成員項目→有違封裝

    B做法:function( obj );
        優點:增加使用額外物件成員的彈性(因為介面穩定)
        缺點:十個公開的存取常式,在函式內有存取的可能性→有違封裝

作者說:
這兩派的說法,都想得太簡單了,最重要的還是要回到抽象概念來思考這個問題。

使用A做法:function( obj.getMem1(), obj.getMem2(), obj.getMem3() );
    若抽象概念是fun( mA, mB, mC ),那麼mA, mB, mC來自相同的一個物件,也只是巧合。
    若抽象概念是fun( oA ),oA是永久的物件;那麼採用了這種做法會中斷了這樣的抽象概念→需改變做法。

使用B做法:function( obj );
    若抽象概念是fun( mA, mB, mC ),就會在呼叫常式前做了下列這些步驟
  1. 建立臨時物件,填值
  2. 將物件傳入常式
  3. 將物件成員取出使用
    若抽象概念是fun( mA, mB, mC ),會出現常常修改常式介面,造成介面不穩定。就應該使用fun( oA )

使用指定引數對應參數  某些特定語言有這個功能。可以指定參數傳給哪一個引數,而不是單單只靠順序來做對應。
Visual Basic Example of Explicitly Identifying Paramaters
Private Function Distance3d(_
    ByVal xDistance As Coordinate, _
    ByVal yDistance As Coordinate, _
    ByVal zDistance As Coordinate _
)
...
End Function
...
Private Function Velocity(_
    ByVal latitude As Coordinate, _
    ByVal longitude As Coordinate, _
    ByVal elevation As Coordinate _
)
    ...
    Distance = Distance3d( xDistance := latitude, yDistance :=
        longitude, _zDistance := elevation)
    ...
End Function
確定「實際參數」對應「形式參數」
「形式參數」又稱為「虛擬參數」,它是常式定義中,宣告的變數。
「實際參數」指的是在實際呼叫的常式中用到的變數、常數或運算式。


7.6 函式使用的特殊考量因素
(Special Considerations in the Use of Functions)


定義:
函式:有return值的常式
程序:無return值的常式

使用函式和程序的時機
(When to Use a Function and When to Use a Procedure)


常見的寫法
if (report.FormatOutput(formattedReport) = Success) then ...
report.FormatOutput()有回傳值,技術上是函式
情況: 技術上稱為函式,操作上偏程序
函式回傳值與下列無關
  • 常式的主要目的
  • 格式化輸出
  • 常式名稱report.FormatOutput()
若可以讓這個技巧的使用情況更加一致,則更不會增加混淆

替代方案
report.FormatOutput( formattedReport, ourputStatus)
if (outputStatus = Success) then ...
對程序和函式的差異較固執的習慣可以區分
  • 常式呼叫
  • 狀態測試
將呼叫和測試結束為一行程式碼可提高陳述式的密度,相對也會使得複雜性增加
下面的例子也是不錯的
outputStatus = report.FormatOutput(formattedReport)
if (outputStatus = Success) then ...
總之,若常式的原始目的 = 傳回函式名稱的值,就可以使用函式(有return),否則請使用程序(無return)

設定函式的傳回值
(Setting the Function's Return Value)


檢查所有可能的傳回路徑
在很多if-else或使用switch-case的情況之下
  • 確定在各稱情況下都會有回傳值
  • 開頭將回傳值初始化

勿回屬於傳區域變數的參照或指標


7.7 巨集常式和內嵌常式
(Macro Routines and Inline Routines)


繼承自C語言的巨集功能的使用注意事項
把巨集常式整個包在括號內
建立這種巨集常式,有一個常見的問題:
#define Cube( a ) a*a*a
所以要改成
#define Cube( a ) (a)*(a)*(a)
若有比乘法更優先的運算子在Cube(a)前後,那還是會有問題
所以對完整的運算式加上括號
#define Cube( a ) ((a)*(a)*(a))
一個巨集可能有多個運算式,如果當成一個運算式,可能會有問題
下列是一個自找麻煩的巨集範例:
#define Lookup Entry( key, index ) \
index = (key - 10) /5; \
index = min( index, MAX_INDEX ); \
index = max( index, MIN_INDEX );
...
for ( entryCount = 0; entryCount < numEntries; entryCount++ )
LookupEntry( entryCount, tableIndex[ entryCount ]);
這巨集之所以是自找麻煩,是因為它並非如一般常式運作。
這個例子,for裡面只有第一行會被執行(因為for沒有大括號)
欲避免此問題,請使用大括號包集巨集:
#define Lookup Entry( key, index ) {\
index = (key - 10) /5; \
index = min( index, MAX_INDEX ); \
index = max( index, MIN_INDEX ); \
}
...
for ( entryCount = 0; entryCount < numEntries; entryCount++ )
LookupEntry( entryCount, tableIndex[ entryCount ]);
但是,就算這樣!
使用巨集套代函式呼叫的做法,也是危險、難以理解、錯誤的程式設計作法。
非必要不要使用的程式技巧。
用命名常式的方式命名巨集常式,以於必要時讓常式取代這些巨集
以往常見的巨集常式命名都是用大寫字母,但若打算使用一般常式取代巨集常式,那使用命名常式的習慣來命名巨集,以減少變更相關的常式呼叫。

但是,這樣做,還是有副作用

若你常使用++或--運算子的副作用(side effects)的時候遵守此原則,就會遇上麻煩。

巨集常式的使用限制(Effective C++有提到)
(Limitations on the Use of Macro Routines)

  • 用const取代#define常數
  • 用inline可滿足即將要編譯為內嵌程式碼的函式
  • 用template可滿足跨類別的標準操作
  • 用enum可取代#define的列舉
  • 用typedef可取代#define定義資料型態的轉換
巨集目前最好用的是條件編譯的功能,用巨集來替代常式,真的是萬不得已的做法。

內嵌常式
(Inline Routines)

先說說Inline Routines的用法
類別中的常式定義,通常是寫在.cpp檔裡,但是若此常式要設計成inline常式,則必須要放在.h檔裡,並且在前面加上inline關鍵字

例如
//Ex.h
class Ex
{
public:
    void Display();
}
//Ex.cpp
void Ex::Display()
{
    std::cout << "I am Ex class" << endl;
}
若Display()要設計成inline,則程式碼必須改寫成
//Ex.h
class Ex
{
public:
    void Display();
}

inline void Ex::Display()
{
    std::cout << "I am Ex class" << endl;
}
優點:
  • 增加效率,避免常式呼叫的負擔
缺點:
  • 違反封裝,因為該常式定義於標頭檔內,讓使用標頭檔的程式設計師都看得見程式碼。
  • 只要呼叫該inline常式的地方,就會增加整體程式碼的長度,這可能帶來自身的問題。
  • 在「犧牲封裝改善效能」這件事上,若沒什麼效益,其實並不需要這樣做,因為會降低程式碼的品質,只是會讓code更難管理。

沒有留言:

張貼留言

(什麼是留言欄訊息?)