如何一本正經的寫出別人無法維護的代碼

編寫除了自己沒人能看懂的代碼,是一種怎樣的體驗?

下面由作爲資深挖坑程序員的我,手把手教大家這是怎麼做到的?如果各位可以在接下來的時間多加練習,所謂青出於藍勝於藍,相信各位不但可以寫出別人無法維護的代碼,還可能在有朝一日,甚至能技藝爐火純青地寫出自己都維護不了的代碼。

編寫無法維護的代碼說難其實並不難,核心要點就是和編碼規範反其道而行之,如果在此基礎上再添加一些自己琢磨出的心得的話那就更加完美了。

掌握了這個要點還不夠,還要注意一個原則:不要讓我們的代碼一眼看上去就無法維護,格式之類的還是要注意些的,我們要追求的不是這種膚淺的表面上的無法維護,我們要的是實質是無法維護的。

要是別人一眼就能看出你的代碼無法維護,那你的代碼就存在需要重寫或者重構的風險了,那不成了前功盡棄親者痛,仇者快的事情了嘛。

瞭解清常規編程的思維方式再下手!

《孫子兵法》有云“知己知彼,百戰不殆”,假如我們要想從心理上徹底擊敗後續的代碼維護人員,我們必須明白常規編程中的一些思維方式。

各位先想下,如果接手程序的是我們自己,而且代碼量比較大,一般我們是沒有時間去從頭到尾一行一行地讀一遍的,更不要說能理解代碼了。

爲了能儘快地上線交差,程序員常見的做法是根據需求,先快速找到代碼中需要改動的那一部分邏輯,然後對這部分的代碼進行修改、測試。這種修改方式一次只能看到代碼的一小部分,管中窺豹。

**所以我們要做的是確保讓代碼維護人員永遠看不到我們寫的代碼的全貌,要儘量保證代碼維護人員找不到他想要找到的那部分代碼。**這還不是最關鍵的,最關鍵的是要讓修改者知道自己沒有忽略任何的東西。

每一個我們精心設計的這些小陷阱都會迫使代碼維護者像用放大鏡似的,仔細地閱讀我們的每一行代碼。

有些同學可能覺得這很簡單,認爲只要按照上文中提到的反編程規範原則來進行即可。但是實際操作起來並沒有這麼簡單,還需要配合我們的精心誤用纔可。下面我們就對常用的一些核心技能娓娓道來。

**第一招:**一本正經地亂用註釋

這一部分我們先了解下注釋的正常用途:註釋是用來幫助開發者理解程序的,尤其是對於後來的開發者,通過註釋可以更快的瞭解代碼的實際作用。

正常情況下代碼註釋的原則一般是隻在需要註釋的地方進行註釋。這是一句很正確的廢話,解釋起來就是很明顯就能看懂的代碼就不要去註釋的了,畢竟看註釋也是需要花費時間的。

另外一個原則就是在註釋中註明代碼的作用需要和代碼的實際作用是一致。

在實際工作中,在對代碼進行修改後一定要連同代碼的註釋也一起進行修改。關於註釋的其他的一些作用我們在此不再多說,光是這些就已經足夠我們用的了。

如何利用代碼註釋寫出讓人無法理解的代碼呢?

一、多整沒用的

這塊我分了兩種情況來描述,兩種情況對應兩種處理方式,實用性比較強。

  • 明顯型註釋

讓維護者浪費時間看顯而易見的註釋。

這部分的原則是維護者看完註釋後覺得“代碼比註釋容易讀多了”,目的就是誤導讀代碼的人。維護者在看代碼時,上眼一看代碼很清晰,但又一看竟然還有註釋

此時讀代碼的人心裏肯定是要嘀咕下:看來這代碼沒我想的這麼簡單。

然後我們的註釋要寫的長一些,最後是要閱讀者看不懂,改的時候猶豫不決。

如果有餘力的話可以在註釋中教維護者怎麼編程,這種一般殺傷力要比上面寫的會高一些,程序員最反感的可能就是你要教他怎麼編程了,尤其是教他這麼簡單的編程,殺傷力加倍。

下面看個例子:


public class Program
{
    static void Main(string[] args)
    {
        /* This is a for loop that prints the
         * words "I Rule!" to the console screen
         * 1 million times, each on its own line. It
         * accomplishes this by starting at 0 and
         * incrementing by 1. If the value of the
         * counter equals 1 million the for loop
         * stops executing.*/
        for (int i = 0; i < 1000000; i++)
        {
            Console.WriteLine("I Rule!");
        }
    }
}
  • 廢棄代碼註釋

字面意思已經很清楚了,正常情況下代碼中不用的部分我們一般會註釋掉或者直接刪除掉,即使這段代碼將來會使用到也不影響,可以從版本控制工具中再找回來。

針對性的做法就是給刪掉的代碼加個長長的註釋,寫明這段代碼爲什麼會被註釋起來,也向維護者傳達了一個信息,即這段代碼不是被”廢棄”的,而是”臨時”先不用。

這樣做的殺傷點就在,如果只註釋了代碼沒加註釋說明,根據實際經驗大家多數會直接略過被註釋的代碼,而給代碼加了註釋後看代碼的人可能就要看看這個註釋了,不然會漏掉什麼關鍵信息,畢竟代碼不是他寫的。

樣板代碼:


public class Program
{
    static void Main(string[] args)
    {
        /* This block of code is no longer needed
         * because we found out that Y2K was a hoax
         * and our systems did not roll over to 1/1/1900 */
        //DateTime today = DateTime.Today;
        //if (today == new DateTime(1900, 1, 1))
        //{
        //    today = today.AddYears(100);
        //    string message = "The date has been fixed for Y2K.";
        //    Console.WriteLine(message);
        //}
    }
}

二、這個地方將來會修改

這種註釋就是我們經常提到的“TODO”型註釋。正常情況下TODO註釋並非一無是處,比如在初始化項目的時候TODO註釋還是非常有用的,到項目release 時一般是建議去掉的,如果必須要留着一般需要寫明在具體什麼日期會處理掉。一般是不推薦TODO型註釋長期存在於項目的代碼中,正常的處理邏輯一般是遵循有Bug儘快Fix,無Bug則去掉註釋。

通過上面的描述相信大家已經知道這塊具體要怎麼應對了。個人建議是對於有待修改的多**寫點TODO註釋,且不註明更改的原因以及計劃更改的時間,**這樣後面的維護人員在看的時候可能連這塊到底是不是已經改過了都搞不清楚,所以殺傷效果也是有一些的。

樣板代碼:

public class Program
{
    static void Main(string[] args)
    {
       //TODO: I need to fix this someday – 07/24/1995 Bob
       /* I know this error message is hard coded and
        * I am relying on a Contains function, but
        * someday I will make this code print a
        * meaningful error message and exit gracefully.
        * I just don’t have the time right now.
       */
       string message = "An error has occurred";
       if(message.Contains("error"))
       {
           throw new Exception(message);
       }
    }
}

三、錯誤註釋信息

這部分的意思是造成代碼和註釋的不匹配,也就是註釋的信息不正確。

我們要做的就是改完代碼後不改註釋就行了,此種方式比較省事,額外工作一點也不用多做,但是稍微有些代價,需要注意的是最好是在此類註釋中加個特殊的標記,防止自己後續看的時候把自己也繞進去。

樣板實例這塊就不用加了吧,場景太多了,大家在自己的一畝三分地上耕作時臨場發揮即可。

四、講故事

簡單說來就是寫明這段代碼爲什麼要這樣寫,當然肯定不是單純的原因。除了原因一般建議在註釋中寫上當時的情況,比如某年某月和某人在某地討論了這個問題,某人說這個問題應該怎樣處理,你說這個問題不該這樣處理應該那樣處理,後來某某人又加入了討論,某某人對倆的討論做了某某的評價,最後決定要用現在的代碼去實現這塊的功能。

總之,原則就是把事情的細節描述清楚,越細越好。有些同學可能會建議將當天的天氣情況也寫上,還有討論中那個氣死人的S名字也要帶上,我個人認爲天氣可以酌情添加,但寫上S名字是不太鼓勵的,畢竟同事一場,要相互愛護的,大家按照自己公司的實際情況來選擇具體的處理方式吧。

樣板代碼:

public class Program
{
    static void Main(string[] args)
    {
       /* I discussed with Jim from Sales over coffee
        * at the Starbucks on main street one day and he
        * told me that Sales Reps receive commission
        * based upon the following structure.
        * Friday: 25%
        * Wednesday: 15%
        * All Other Days: 5%
        * Did I mention that I ordered the Caramel Latte with
        * a double shot of Espresso?
       */
        double price = 5.00;
        double commissionRate;
        double commission;
        if (DateTime.Today.DayOfWeek == DayOfWeek.Friday)
        {
            commissionRate = .25;
        }
        else if (DateTime.Today.DayOfWeek == DayOfWeek.Wednesday)
        {
            commissionRate = .15;
        }
        else
        {
            commissionRate = .05;
        }
        commission = price * commissionRate;
    }
} 

五、不要寫原因

按照註釋的規範,註釋時不但要解釋程序的表述的意思,更重要的是寫明爲什麼寫,即代碼這麼寫的原因是什麼。

這樣應對之策也已經顯而易見了,對於複雜程序,比如一些特殊的邊界條件判斷,只寫下程序的字面意思,具體邊界值判斷爲什麼要這樣寫,爲什麼是這個值可以忽略掉,讓維護的人盡情去猜吧。

六、瑣碎

在這需要註明的是大部分程序註釋一般是用不到這種情況的,一般是推薦放在一些複雜算法的解釋上,越是複雜的算法越是推薦,原則就是把這部分應該寫到文檔中的內容寫到代碼中。

**一定要把算法的所有的詳細設計都寫上,註釋內容分段落,段落之間要分級,每個段落建議加上編號,這樣就基本可以保證代碼的註釋和文檔的內容保持一致。**後續的維護看到這樣的註釋的時候基本可以保證頭大一圈,如果此類註釋存在多處的話效果更佳。

鑑於樣板示例中註釋篇幅太長就不加示例了。

七、單位問題

單位這部分和具體的業務場景相關,比如時間相關的一般會有毫秒、秒、分鐘、小時、天、月、年等,涉及尺寸的場景如像素、英寸等,涉及文件大小的場景如字節、KB、MB、GB等。

這一類的代碼中我們的原則是不對單位進行註釋,只管使用,如果可以在代碼中各種單位混用,那自然是更加優秀。

比如在關於文件處理的場景中,KB、MB、GB多個單位混合使用,這樣後來的維護人員要想搞懂這部分代碼中單位的真正含義就要下一番功夫了。

按照我們的正常邏輯,後面的人要想改這部分的代碼的邏輯首先要先弄懂各個數據的單位,搞清楚之前肯定是不敢隨意修改的,一般這種情況只有一種解決辦法那就是一遍遍的調試、測試程序來推算各個數據實際的單位,花費的時間自然是相當的多。

八、恐嚇

這一招可以說是殺手鐗級別的註釋,可以在程序中加一部分可有可無的代碼,而且是很明顯可有可無的那種,然後給這段程序加個註釋,註釋中寫明“千萬不要註釋掉或者刪除這段代碼,否則程序會出現異常!!!”,需要注意的是不要解釋會出現什麼樣的異常。

這樣維護人員在看到這段代碼的時候肯定首先會聯想到自己以前看過的一些文章,並堅信這段“廢話代碼”肯定是不能刪除的。代碼中如果存在多處這種註釋的話效果更佳。

障眼法篇

一、入門

讓你的代碼和註釋交融在一起,算是入門級的代碼僞裝術,主要目的是噁心後來的維護者,假使看代碼的人剛好頭昏腦漲的話肯定會直接懵逼一會,懵逼完之後再一陣噁心。

如果代碼和註釋的邏輯剛好是一脈相承下來的那自然更好,具體操作可以參考下面的樣板實例。

樣板示例:

public class Program
{
    static void Main(string[] args)
    {

        for(j=0; j<array_len; j+ =8)
        {
        total += array[j+0 ];
        total += array[j+1 ];
        total += array[j+2 ]; /* Main body of
        total += array[j+3]; * loop is unrolled
        total += array[j+4]; * for greater speed.
        total += array[j+5]; */
        total += array[j+6 ];
        total += array[j+7 ];
        }
    }
}

二、同義詞

這種招式一般是建議用在C類程序的宏定義中,使用的原則也比較簡單,即宏名稱和具體的值雜糅使用即可,造成一種你中有我,我中有你的既視感。

樣板示例:

#define xxx global_var // in file std.h&nbsp;
#define xy_z xxx // in file ..\other\substd.h&nbsp;
#define local_var xy_z // in file ..\codestd\inst.h

三、命名不一致

這部分主要應用在前端開發中,舉個例子大家就清楚了,比如Web界面上郵政編碼顯示爲postal code,代碼中把變量名命名爲zipcode,我相信不論誰看到這種情況都不敢直接改代碼的,肯定要反覆確認一會postal code 對應的變量到底是不是zipcode。

四、宏定義隱藏

需要說明的是這裏的隱藏不是說將宏定義藏到找不到的地方,那肯定是不行的,說不定我們自己還要進行修改呢。

這裏說的宏定義隱藏是指將宏定義寫的不像宏定義,讓看代碼的人一眼看去覺得這不是一個宏,然後略過去。

話不多說,上樣板:

#define a=b  a=0-b

五、變量名換行

這一招我只能用猥瑣來形容了,因爲真的是猥瑣。產生的效果是即難閱讀也很難進行變量名的搜索。

樣板示例:

#define local_var xy\
_z // local_var OK

六、全局變量

這裏說的是隱藏全局變量,方法就是在函數裏面使用全局變量時不直接使用,而是以傳參的形式傳進來後進行使用,這樣很難分辨出這是一個全局變量。

七、函數重載

正常重載後的函數,其功能應該和被重載的函數應該是接近的,我們要做的就是讓重載後的函數和被重載函數的功能完全沒有關係

這個時候看代碼的人如果基礎不牢的話,可能需要去溫習下函數重載的知識,是不是自己以前理解錯了。

八、操作符重載

操作符重載是一種很變態的招式,因爲他會讓你的代碼變的非常的詭異,只要按照下面描述的方式進行使用基本可以把代碼的混亂程度直接拉昇到藝術的級別,藝術就是打破常規,所以一般只要不按照操作符重載推薦的使用方式去使用都能收到意想不到的效果。

樣板示例:類中重載 ! 操作符,重載後的功能不是取反而讓其返回一個整數,於是當使用!!時會先調用被重載後的函數,返回一個整數,然後再取反,最後返回個bool值,一臉懵逼。

九、混亂#define

這一招用上後我覺得看代碼的人如果不是穩如老狗的老司機應該會抱頭痛哭的。不信可以看下面的這段樣板代碼。

樣板示例:

#ifndef DONE
#ifdef TWICE
// put stuff here to declare 3rd time around
void g(char* str);
#define DONE
#else // TWICE
#ifdef ONCE
// put stuff here to declare 2nd time around<
void g(void* str);
#define TWICE
#else // ONCE
// put stuff here to declare 1st time around
void g(std::string str);
#define ONCE
#endif // ONCE
#endif // TWICE
#endif // DONE

變量命令篇

一、大小寫任意交替

這種基本上就是一種罵孃的命名方式,爲啥會這麼容易引起怒火,看個例子就知道了:gGEtpRoDucTnaME,有沒有腦裂的感覺?

二、單字母變量

名稱上毫無邏輯可言。

樣板代碼:

public class Program
{
    static void Main(string[] args)
    {
        private a = 0;
        private A = 0;
        private b = 0;
    }
}

三、字母+數字很配

如果字母a – z 不夠使用,可以考慮字母+數字的組合,這樣一般就足夠使用了,畢竟數字是無限的。

樣板示例:

public class Program
{
    static void Main(string[] args)
    {
        private a1 = 0;
        private a2 = 0;
        ...
        ...
        private a10 = 0;
        private A2 = 0;
        ...
        ...
        private A10 = 0;
        private A1 = 0;
        private b1= 0;
        private b2 = 0;
        ...
        ...
        private b10 = 0;
    }
}

四、故意拼錯

變量名稱拼錯並不是隨意一個單詞就拼錯,此處指的是比較有創意的拼寫錯誤。

因爲隨意的拼寫錯誤是很容易被發現的,高級的拼寫錯誤由於很難看出來,所以在進行變量搜索的時候根本搜不出來。 比如:SetPintle、SetPintalClosing。

五、重複名稱

函數或者方法的內嵌結構中使用和函數或者方法層面中同名的變量名,變量名多的話可能會一陣眩暈。

進階篇

前面的註釋和變量命名可以說是本文的基礎篇,主要是較大家一些基本的編程技巧。這一篇作爲進階的一篇,我會給大家介紹一下常見的一些稍微高端的編程技巧,廢話不多說,一起看下:

一、void*

不管什麼類型的指針一律都用聲明定義爲void*,當實際用到時再轉換爲需要的類型。

二、條件表達式

條件表達式這塊可以發揮的空間就比較大了,從實際編碼情況來看,每個簡單的條件表達式都可以進行拆分,看個例子就明白了。

10 == num,拆分爲 num >= 99 && num <= 101

三、長代碼

什麼,一行最多80個字符? 不行,這才哪到哪,一定要跨行,而且要跨多行,不能因爲換行影響了我們寫代碼的那股激情。

原則就是越長越好,這樣後續閱讀代碼的人就需要來來回回地讀,想想都覺得累。

四、嵌套

一個優秀的程序員必須能夠在一行代碼中使用超過10個小括號(),如果覺得很難得話在一個函數裏面使用超過5層的大括號也是可以的,還不行的話把嵌套的條件語句if … else 轉爲[?:] 也是可以說明你是個優秀的程序員的。

五、不要break

不要在代碼的循環中使用break,更不要使用goto,這樣可以保證一行break可以處理的代碼最少要寫5層的if … else 來解決,一遇到break 就多出百十行代碼,想想都過癮,一天下來光看看新增的代碼行數就覺得充實。

六、儘量使用XML

XML 的強大是無人能及的,不是JSON、Yaml這些所能及的。項目中使用XML 可以幫助我們將原來只需要10行的代碼變爲100行(可能還不止)。XML是無所不能的,哪怕是自己封裝自己也是可以做到的,信XML 得永生,信XML 的自信!

樣板代碼:

<!-- ED: soap envelope omitted for readability -->
<string xmlns="urn:Initech.Global.Services">
  &lt;CompanyGetConnector&gt;
    &lt;xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
      &lt;xs:element name="InitechGetConnector"&gt;
        &lt;xs:complexType&gt;
          &lt;xs:choice maxOccurs="unbounded"&gt;
            &lt;xs:element name="employees"&gt;
              &lt;xs:complexType&gt;
                &lt;xs:sequence&gt;
                  &lt;xs:element name="EmployerName" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Employee" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Firstname" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Prefix" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Lastname" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Org._unit" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Function" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="E-mail_work" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Telephone_work" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Mobile_work" type="xs:string" minOccurs="0"/&gt;
                  &lt;xs:element name="Birthdate" type="xs:date" minOccurs="0"/&gt;
                  &lt;xs:element name="Hired_since__irt._yearsemployed_" type="xs:date" minOccurs="0"/&gt;
                  &lt;xs:element name="Image" type="xs:base64Binary" minOccurs="0"/&gt;
                &lt;/xs:sequence&gt;
              &lt;/xs:complexType&gt;
            &lt;/xs:element&gt;
          &lt;/xs:choice&gt;
        &lt;/xs:complexType&gt;
      &lt;/xs:element&gt;
    &lt;/xs:schema&gt;

    &lt;employees&gt;
      &lt;EmployerName&gt;
        My Client
      &lt;/EmployerName&gt;
      &lt;Employee&gt;
        100001
      &lt;/Employee&gt;
    &lt;/employees&gt;
  &lt;/CompanyGetConnector&gt;
</string>

七、測試

測試,不存在的。

一般建議不要測試,測試是一種懦夫的行爲,作爲一個優秀的程序員我們必須保持這種對自己代碼的自信,再者測試會影響你的生產力,直接影響你寫代碼的行數,所以測試這一步直接跳過就好啦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章