第七章 高品質常式
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)出錯
<<法一>>
- 提取重覆程式碼
- 相同程式碼→基礎類別
- 特別程式碼→衍生類別
- 相同的程式碼移到新的常式內
- 特別程式碼呼叫這個常式
支援子類別覆寫(subclassing override)
隱藏順序
隱藏指標操作
- 確保指標的操作正確
- 確定替換指標行為時完整
簡化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 ),就會在呼叫常式前做了下列這些步驟
- 建立臨時物件,填值
- 將物件傳入常式
- 將物件成員取出使用
使用指定引數對應參數 某些特定語言有這個功能。可以指定參數傳給哪一個引數,而不是單單只靠順序來做對應。
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更難管理。
沒有留言:
張貼留言
(什麼是留言欄訊息?)