本系列文將會以最淺顯好懂的方式說明相關議題,已經了解的朋友或許也能從中知悉一些脈絡細節,希望這些文章能讓讀者們有所收穫。
Copy and paste is a design error.
-------------------------------------------------
大師David Parnas說“Copy and paste is a design error.”,前些日子trace了一些之前人寫的code,突然覺得好適合拿出來做說明。
今天你剛開始寫程式,你寫了一隻API(註1),你Google、你拼拼湊湊,你成功的讓程式碼動起來了,你的API會接收搜尋參數並回傳結果,恭喜你,你踏入了程序猿的第一步,差不多等於LV1吧。
//標準Search API
public string Search(string DataBaseName, string TableName, string SearchWords, Dictionary<string, double> QueryFields, List<string> ReturnFields, List<string> HighlightFields, int PageNumber, int PageSize, string Is_Published, Dictionary<string, DateTime[]> QueryDate, Dictionary<int, Order> SortDic, List<string> ExactKeyword, Dictionary<string, string> RenameFields)
{
const string Command = "QUERY";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query
#回傳結果
}
這時候你的主管說:"你的API怎麼驗證權限呢!",你回去之後在你的API上面補上去了,現在只有當UserName是你主管的時候可以使用此API了。
//標準Search API
public string Search(string UserName, string DataBaseName, string TableName, string SearchWords, Dictionary<string, double> QueryFields, List<string> ReturnFields, List<string> HighlightFields, int PageNumber, int PageSize, string Is_Published, Dictionary<string, DateTime[]> QueryDate, Dictionary<int, Order> SortDic, List<string> ExactKeyword, Dictionary<string, string> RenameFields)
{
//權限驗證 等等
if (UserName!="你主管")
{
strJsonResult = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
return strJsonResult;
}
const string Command = "QUERY";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query
#回傳結果
}
你的主管又說:"去寫Count API,記得也要驗證權限!",你心中一喜:"哈,我已經寫過驗證啦,複製貼上就搞定囉"
//Get Table的資料筆數
public string GetTableDataCount( string DataBaseName, string TableName, string SearchWords)
{
//權限驗證 等等
if (UserName!="你主管")
{
strJsonResult = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
return strJsonResult;
}
const string Command = "QUERY COUNT";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query COUNT
#回傳結果
}
你就這麼的一路剪剪貼貼,忙盲茫的度過了半個月,其中大概寫了87隻API,這時候你主管說:"權限要開發給其他人用!",你就得修改87隻API
你可能找不到其中7隻API;你可能忘了其中8隻API在幹嘛;你可能改一改發現有9隻API壞掉了,但你不知道原因。
//Get Table的資料筆數
public string GetTableDataCount( string DataBaseName, string TableName, string SearchWords)
{
//權限驗證 等等
//因為你是87 這行要寫UserName!="你主管" && UserName!="其他87" 才對
if (UserName!="你主管" || UserName!="其他87" )
{
strJsonResult = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
return strJsonResult;
}
const string Command = "QUERY COUNT";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query COUNT
#回傳結果
}
三天三夜後你改完了,你以為可以告一個段落了,這時候你的主管說:"Count可以給其他87用,但Search只有我能用!",你慶幸你對於Search還算熟悉,它還沒有從你的記憶中遺失,你很快的完成了這件事,主管又說:"Count要只能給18歲以上的87用!",所以這時候你有程式碼如下,請記得這可能在87隻API之中
//標準Search API
public string Search(string UserName, string DataBaseName, string TableName, string SearchWords, Dictionary<string, double> QueryFields, List<string> ReturnFields, List<string> HighlightFields, int PageNumber, int PageSize, string Is_Published, Dictionary<string, DateTime[]> QueryDate, Dictionary<int, Order> SortDic, List<string> ExactKeyword, Dictionary<string, string> RenameFields)
{
//權限驗證 等等
if (UserName!="你主管")
{
strJsonResult = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
return strJsonResult;
}
const string Command = "QUERY";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query
#回傳結果
}
//Get Table的資料筆數
public string GetTableDataCount( string DataBaseName, string TableName, string SearchWords)
{
//權限驗證 等等
//不要問這邊這麼會有AGE,工程師會去冰箱裡找
if (UserName!="你主管" && !(UserName=="其他87" && AGE>18) )
{
strJsonResult = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
return strJsonResult;
}
const string Command = "QUERY COUNT";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query COUNT
#回傳結果
}
又過了半個月,你主管換了名字,他說:"去讓我的新名字可以用!",但因為你這半個月出了車禍你失憶了,你不記得這87隻API中有一隻API要開放給其他87用,於是你把程式改成了:
//標準Search API
public string Search(string UserName, string DataBaseName, string TableName, string SearchWords, Dictionary<string, double> QueryFields, List<string> ReturnFields, List<string> HighlightFields, int PageNumber, int PageSize, string Is_Published, Dictionary<string, DateTime[]> QueryDate, Dictionary<int, Order> SortDic, List<string> ExactKeyword, Dictionary<string, string> RenameFields)
{
//權限驗證 等等
if (UserName!="你主管的新名字")
{
strJsonResult = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
return strJsonResult;
}
const string Command = "QUERY";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query
#回傳結果
}
//Get Table的資料筆數
public string GetTableDataCount( string DataBaseName, string TableName, string SearchWords)
{
//權限驗證 等等
//不要問這邊這麼會有AGE,工程師會去冰箱裡找
if (UserName!="你主管的新名字")
{
strJsonResult = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
return strJsonResult;
}
const string Command = "QUERY COUNT";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query COUNT
#回傳結果
}
而這次你又改了三天三夜後換來的是主管的雷霆震怒:"為什麼Count其他87不能用了! 為什麼還有50個API只有舊名字能用? !為什麼有50個API能用的87沒有年齡限制?!"
你累了嗎?
當你有不同的程式碼區塊,有使用到類似的寫法、功能時,請不要將程式碼複製貼上過去,請保持程式碼的重複使用率(Code reuse),今天當你發現你有第二段類似的程式的時候,你可以考慮使用父類別以及繼承的方式,以API來說有一個常用的慣例命名就是BaseAPI
//所有API的父類 用來做所有API都會做的事情
//權限驗證 等等
public string BaseAPI(string UserName){
if (UserName!="你主管的新名字")
{
結果= @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
回傳結果
}
}
//標準Search API
public string Search:BaseAPI(string UserName, string DataBaseName, string TableName, string SearchWords, Dictionary<string, double> QueryFields, List<string> ReturnFields, List<string> HighlightFields, int PageNumber, int PageSize, string Is_Published, Dictionary<string, DateTime[]> QueryDate, Dictionary<int, Order> SortDic, List<string> ExactKeyword, Dictionary<string, string> RenameFields)
{
const string Command = "QUERY";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query
#回傳結果
}
//Get Table的資料筆數
public string GetTableDataCount:BaseAPI( string DataBaseName, string TableName, string SearchWords)
{
const string Command = "QUERY COUNT";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query COUNT
#回傳結果
}
如果你的兩段程式碼做的事很像,但又有點不一樣,你可以考慮把不一樣的地方做在外面,你也可以直接在父類裡面再做判斷
//所有API的父類 用來做所有API都會做的事情
//權限驗證 等等
public string BaseAPI(string UserName){
// 這隻api要給87用
if(APIName== "GetTableDataCount"){
if (UserName!="你主管的新名字" && !(UserName=="其他87" && AGE>18) )
{
結果 = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
回傳結果
}
}
//其他不用
else if (UserName!="你主管的新名字" )
{
結果 = @"{""Status"":""8000"",""Message"":""AccessCode invalid!!""}";
回傳結果
}
}
//標準Search API
public string Search:BaseAPI(string UserName, string DataBaseName, string TableName, string SearchWords, Dictionary<string, double> QueryFields, List<string> ReturnFields, List<string> HighlightFields, int PageNumber, int PageSize, string Is_Published, Dictionary<string, DateTime[]> QueryDate, Dictionary<int, Order> SortDic, List<string> ExactKeyword, Dictionary<string, string> RenameFields)
{
const string Command = "QUERY";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query
#回傳結果
}
//Get Table的資料筆數
public string GetTableDataCount:BaseAPI( string DataBaseName, string TableName, string SearchWords)
{
const string Command = "QUERY COUNT";
#region宣告一堆變數
#region Check 必填參數
#region 處理 選擇性參數
#region 組成Query COUNT
#回傳結果
}
爾後有類似的變化,你只需要去注意父類,而如果是涉及各個不同API才有的問題(比如Count的數量不對),你也可以很明確的知道,跟驗證權限部會有任何關連,所以你也不需要是確認那邊的程式碼。
---------
補充一個更簡單的例子,不需提到API
單純一般流程會用到的IF ELSE或是SWITCH也一樣
當你在複製貼上的時候,你可能會覺得很明顯阿
因為你複製的那一行也是你寫的
但對於下一個要看的人(或是3個月後的你自己),這其實是一種很花時間又容易眼殘的事情
就是所謂不好維護的程式碼
寫成這樣不就能夠很快地看懂這段流程
調整起來既快又方便
-----------------------------------------------------------
你可能會說,我一開始哪知道?!
對,今天有經驗者的開發者,可能會意識到有些事情是所有API都必然要做的,那它可能在寫第一隻的時候就會寫一個BaseAPI來繼承,沒有關係,你第一隻API把這些內容寫在裡面,我覺得合理,因為的確可能沒有第二隻API,但當今天有第二隻API的時候,你就應該想到這件事,把這些程式抽離出去,而不是複製貼上,軟體開發不是會動就好,程式維護的人力成本更高,當你寫出不好維護的程式碼時候,想想下一個需要改寫這個程式碼的人可能是三個月後的自己,而你要記得,三個月後的你什麼都不會記得,你只會想痛打上一個寫這段程式的人。
只有一種情況我覺得可以不要這麼做,就是當你老闆跟你說,你一定要在半小時內改完程式碼上到正式環境,而你打算明天領到年終之後才要把辭職信甩到他臉上,那你就複製貼上吧。
Bob的無暇的程式碼中的一段話
註1:科班出生的人可能會記得計算機概論一直有一個說法:"所謂資訊系統者,有輸入、處理後、會有輸出的一個黑盒子",API其實也差不多,強調點可能是透過網路供其他系統使用的黑盒子。
------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------
它:「我愛寫程式ㄟ!」
我:「你...愛寫程式?」
它:「是阿!」
我:「你...愛用程式解決問題?愛寫出被大家所使用的程式?愛能夠用程式改變世界的力量?」
它:「沒錯!你說得太...」
我:「你愛日以繼夜焚膏繼晷的寫程式?你愛無法離座坐到屁股長痔瘡的寫程式?你愛寫程式愛到奮不顧身?」
它:「你...」
我:「你愛想破頭想到拿頭去撞牆想到掉光頭髮的寫程式?你愛被時程、需求、主管、客戶追著跑的寫程式?你愛寫程式即使程式不愛妳?」
我:「你愛你明明知道這樣不是好的寫法卻仍然得這樣寫?你愛維護別人寫出來的爛程式?你愛聽不懂的主管胡說八道該怎麼寫?」
它:「你在說什麼東西啦!你也沒有這樣子阿!」
我:「是阿,所以我不愛寫程式。」
它:「你懂不懂寫程式阿?」
我:「:)」