[你阿罵也看得懂的軟體開發]不要複製貼上!

本系列文將會以最淺顯好懂的方式說明相關議題,已經了解的朋友或許也能從中知悉一些脈絡細節,希望這些文章能讓讀者們有所收穫。

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其實也差不多,強調點可能是透過網路供其他系統使用的黑盒子。

------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------

它:「我愛寫程式ㄟ!」

我:「你...愛寫程式?」

它:「是阿!」

我:「你...愛用程式解決問題?愛寫出被大家所使用的程式?愛能夠用程式改變世界的力量?」

它:「沒錯!你說得太...」

我:「你愛日以繼夜焚膏繼晷的寫程式?你愛無法離座坐到屁股長痔瘡的寫程式?你愛寫程式愛到奮不顧身?」

它:「你...」

我:「你愛想破頭想到拿頭去撞牆想到掉光頭髮的寫程式?你愛被時程、需求、主管、客戶追著跑的寫程式?你愛寫程式即使程式不愛妳?」

我:「你愛你明明知道這樣不是好的寫法卻仍然得這樣寫?你愛維護別人寫出來的爛程式?你愛聽不懂的主管胡說八道該怎麼寫?」

它:「你在說什麼東西啦!你也沒有這樣子阿!」

我:「是阿,所以我不愛寫程式。」

它:「你懂不懂寫程式阿?」

我:「:)」