1. 程式人生 > >現在開始關注細節,明天方能成就卓越程式碼

現在開始關注細節,明天方能成就卓越程式碼

開篇

  • 我們總是很容易就能寫出滿足某個特定功能的程式碼,卻很難寫出優雅程式碼。又最欣賞那些優雅的程式碼,因為優雅程式碼更能體現一個開發者的積累。
  • 就像寫一篇散文,有的就像初學者不得其門而入,遣詞造句都非常困難,然後糾糾結結,最終不了了之。或者囉哩吧嗦,看起來說了一堆,其實就像是村婦閒聊,毫無重點,不過是口水文而已。
  • 好程式碼應該是這樣的,如涓涓細流、如同一首詩,一篇優美的故事,將作者編寫程式碼時的情感慢慢鋪墊開來,或是高潮迭起,此起彼伏,或是平鋪直述,卻蘊含道理。我始終相信優秀的程式碼是有靈魂的,程式碼的靈魂就是作者的邏輯思維。
  • 編寫整潔程式碼 or 非整潔程式碼,就像平時生活中是否注意愛護環境的一點點小習慣,一旦壞味道程式碼沒有及時處理,就會成為破窗效應,然後逐漸的程式碼越寫越爛,最終這些程式碼要麼以重構收場,要麼就被拋棄。
  • 我們見過太多沒有毫無質量可言的程式碼,許多時候開發者們由於能力原因、或者時間有限,寫了許多能夠滿足當前工作的程式碼,然後就棄置高閣,不再理會。於是,程式碼寫之前的只有自己和上帝能理解程式碼的意思,而寫完了之後,只有上帝能懂了;還有一些開發者說:我只會寫程式碼,不會優化程式碼,他們彷彿特別勤奮,每天都會比其他人都熱衷於熬工時,但是寫出的程式碼,實際上是一個個難以維護的技術債。而且許多程式碼的作者總喜歡找各種藉口來抵賴,例如喜歡說程式碼出了問題都是底層框架太垃圾了、或者別人的程式碼封裝得太差。他們總是抱怨這抱怨那,但是即便有優秀的框架、技術,就一定能寫出優秀的程式碼麼?
  • 在這裡筆者列舉了平時看到過一些自認為不太整潔的程式碼,以及與《程式碼整潔之道》(Clean Code · A Handbook of Agile Software Craftsmanship)一書中相對應的範例,歡迎大家一起來拍磚。
  • (經驗有限,時間倉促,請輕噴。)

    一些栗子

  • 1、命名規則
    • 1.1 變數命名和方法命名

在我們剛剛開始學習寫程式碼的古老時代,或許會有下面這種習慣。

/// <summary>
/// author:zhangsan
/// </summary>
class ZhangsanTest
{
    private void TestGetData()
    {
        int a, b, c;
    }
    private int ZhangsanGet(int s1, int s2)
    {
        int s3 = s1 + s2;
        return s3;
    } 
    private List<string> GetData()
    {
        return null;
    }
}

這是一個喜歡用自己的姓名來命名類和方法的作者,在他的程式碼中,經常可以看到這樣奇怪的物件定義,而且他還喜歡用a,b,c,d,e,f或者s1,s2這樣的命名,彷彿他的程式碼自帶混淆特效。這樣的程式碼嗅起來會不會覺得充斥著奇怪的味道?
另外,有沒有發現有許多開發者喜歡用 GetData() 來定義獲取資料的方法?然後這個方法就成為一個萬金油的方法,不管是爬蟲採集、或者資料繫結,無論是 C# 寫的後端或者 Java 寫的後端程式碼,或者用 vue 寫的前端程式碼,彷彿在任何場景、任何資料應用都可以看到這樣的方法。
如果一個專案中,有十幾個地方都出現了這個** GetData() **方法,那種感覺一定非常難受。

  • 1.2 Model、Dto 傻傻分不清楚

隨著技能的增長,或許我們會學到一些新的程式碼概念,例如,Model、DTO 是經常容易弄混淆的一種概念,但是在某些程式碼中,出現了下面的命名方式就有點令人窒息了。

public class XXXModelDto
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Alias { get; set; }
    }

這是大概是一位對概念嚴重消化不良的資深開發者,居然同時把 Model 和 DTO 複用在一個物件上,
(當然,一個開發者定義變數的背後一定有他的動機)。
他到底是想要的是用來在 MVC 模式解決資料傳輸和物件繫結的模型物件?還是用於傳輸資料的 DTO 呢?
--其實他定義這個物件,是為了定義儲存資料物件的實體( Entity )。

  • 1.3特殊情況術語和欄位對照表非常重要

近年來開發者素質越來越高,所以許多優秀開發者會傾向於使用翻譯軟體來翻譯變數名,然後用英語來命名,但是即便如此,許多政務專案總是能嗅出一些奇怪的味道。
例如前不久看到一條這樣的簡訊:(原圖已經消失)

xxx公積金中心提醒您:您於{TQSJ}日進行了{TQCZ}操作,賬上剩餘金額為{SYJE}元。

這是個bug將xxx公積金中心的某些祕密透露在大家面前。作為一個嚴謹的專案,居然使用中文首字母大寫命名法,這讓習慣於大駝峰、小駝峰的我看了之後尷尬癌犯了,很不舒服。但是這也是許多政務資訊化專案的中欄位命名的規範,而且在這種情況下,往往會輸出一份非常規範的資料庫欄位對照表,確保中文和首字母的語義不讓人產生歧義。
所以特定語境下,變數和方法本身沒有嚴格的規定,但是一定要使用恰當的語境概念,對於這樣的特定場景,儘量維護一份實時更新的術語表吧。

  • 2、狀態碼返回值
    • 2.1業務邏輯狀態碼

似乎在對外提供介面時,使用下列介面狀態碼是一種比較常見的慣例。提供統一格式的 code 狀態碼以及返回的訊息和成功返回結果時的填充資料,能夠讓開發者高效的完成介面對接,無需關心http狀態碼背後的含義。

{"code":"100101","message":"success","data":{},"count":""}
  • 2.2用 http 狀態碼為什麼不夠?

上面這是一種經典的流派,還有一種流派則會使用http狀態碼來返回指定的資料,事實上 http 協議本身已經提供了許多狀態碼,例如下面的這些大家都非常熟悉的狀態碼。
但是這些狀態碼為啥不夠?主要是為了減少前後端、服務上下游之間介面對接的難度,也是一種提高效率的方式。 但是 http 狀態碼是一種通用的格式,應儘量使用這種方式,而不應該通過解析正常響應後的 json 來判斷是否正確操作。

    200 :正常響應 標準成功程式碼和預設選項。  
    201 :建立物件。 適用於儲存行為。 
    204 :沒有內容。 當一個動作成功執行,但沒有任何內容可以返回。   
    206 :部分內容。 當您必須返回分頁的資源列表時很有用。   
    400 :請求不正確 無法通過驗證的請求的標準選項。  
    401 :未經授權 使用者需要進行身份驗證。   
    403 :禁止 使用者已通過身份驗證,但沒有執行操作的許可權。   
    404 :找不到資源自動返回。 
    500 :內部伺服器錯誤。 理想情況下,您不會明確地返回此訊息,但是如果發生意外中斷,這是您的使用者將會收到的。 
    503 :服務不可用 相當自我解釋,還有一個不會被應用程式顯式返回的程式碼。
  • 3、switch 語句與判斷語句
    • 3.1 面向過程式或面向物件式

我曾經跟小組中一位大佬交流他的一段程式碼,他的這段程式碼大概是這樣的。

/// <summary>
/// 流程處理
/// </summary>
public void FlowProcess(int auditType)
{
  switch (auditType)
  {                
      case 1://通過
          //此處省略通過場景下的50行程式碼
          break;
      case 2://不通過
          //此處省略不通過場景下的50行程式碼
          break;
      case 3://再審通過
          //此處省略再審通過場景下的50行程式碼
          break;
      case 4://再審不通過
          //此處省略再審不通過場景下的50行程式碼
          break;
  }
}

(讀者卒。)
且不說這位大佬的程式碼是寫得好或者不好,僅僅就這200多行程式碼的4個大switch讀起來大概會讓人便祕難受吧。於是在我讀完這段程式碼之後,我冒死向他請教這麼寫程式碼的原因,他說我這個流程處理就是一個簡單的用例場景,哪裡還有什麼可以優化的餘地?
我跟他介紹了20分鐘程式碼封裝的必要性,於是,他把程式碼寫成了這樣。

/// <summary>
/// 流程處理
/// </summary>
public void FlowProcess(int auditType)
{
    switch (auditType)
    {                
        case 1://通過
            AuditOK();
            break;
        case 2://不通過
            AuditNotOK();
            break;
        case 3://再審通過
            ReAuditOK();
            break;
        case 4://再審不通過
            ReAuditNotOK();
            break;
    }
}
public void AuditOK()
{
    //此處省略通過場景下的50行程式碼
}
public void AuditNotOK()
{
    //此處省略不通過場景下的50行程式碼
}
public void ReAuditOK()
{
    //此處省略再審通過場景下的50行程式碼
}
public void ReAuditNotOK()
{
    //此處省略再審不通過場景下的50行程式碼
}

這酸爽令人簡直難以置信。(事實上這個新鮮出爐的遺留應用,正是這樣一點點堆積了許多總程式碼行超過千行的類檔案)
《程式碼整潔之道》書上有一個類似的例子,大概與上文類似,Robert 大叔給出了這樣的建議:

對於switch 語句,我的規矩是如果只出現一次,用於建立多型物件,而且隱藏在某個整合關係中,在系統中其他部分看不到,就還能容忍。當然也要就事論事,有時我也會部分或全部違反這條規矩。

上文我給出的示例,有點像面向過程的程式碼風格,而 Robert 大叔在他的書中寫下的示例是這樣的(抽象工廠模式的示例)。

這清爽的感覺,讓人很舒服啊。

  • 3.2 孰優孰劣?

當然,原示例是一個流程處理的例子,似乎大家的流程處理程式碼都習慣於使用這種面向過程風格的寫法,反正要加判定條件,就加一個 case 就可以了。
而在某些特定情況下,甚至用 if / else 來寫邏輯判斷更簡單,於是我們經常在某些銷量很好的快速開發平臺中,看到這樣的例子。

這些典型的面向過程風格的程式碼,確實讀起來似乎更加簡單、而且也易於實現。

Robert 大叔是這樣說的:過程式程式碼(使用資料結構的程式碼)便於在不改動既有資料結構的前提下新增新函式,面向物件程式碼便於在不改動既有函式的前提下新增新類。
反過來講也說得通:過程式程式碼難以新增新資料結構,因為必須修改所有函式,面向物件程式碼難以新增新函式,因為必須修改所有類。

所以究竟是使用面向過程式程式碼,還是面向物件式程式碼?沒有萬試萬靈的靈丹妙藥。

  • 4、奧卡姆剃刀定律、得墨忒耳律
    • 4.1“如非必要,勿增實體”

一旦開始初步掌握面向物件開發的基本原則,於是我們就會新建許多各種不同的模型物件。尤其是在webapi介面開發過程中,更是如此。

切勿浪費較多東西去做,用較少的東西,同樣可以做好的事情。

  • 4.2 得墨忒耳律

假設有一段程式碼是這樣的。

public  class GrandParent
{
    public Father Son { get; set; }
    public string Name { get; set; }
    public Father GetSon()
    {
        return Son;
    }
}
public class Father
{
    public Me Son { get; set; }
    public string Name { get; set; }
    public Father GetSon()
    {
        return Son;
    }
}
public class Me
{
    public Son Son { get; set; }
    public string Name { get; set; }
    public Son GetSon()
    {
        return Son;
    }
}
public class Son
{
    public GrandSon GrandSon { get; set; }
    public string Name { get; set; }
    public GrandSon GetSon()
    {
        return GrandSon;
    }
}
public class GrandSon
{
   public string Name { get; set; }
    public string GetSon()
    {
        return Name;
    }
}

會不會為了獲得某些資料,而寫出這樣的程式碼呢?

return new GrandParent().GetSon().GetSon().GetSon().Name;

這樣就是典型的對得墨忒耳律的違背。這個原則指出:

模組不應瞭解它所操作物件的內部情形。
更準確的說,得墨忒耳律認為,類C的方法f只應該呼叫以下物件的方法:
C(本身)
由方法f建立的物件。
作為引數傳遞給f的物件;
由C的實體變數持有的物件。
物件不應呼叫由任何函式返回的物件的方法。換言之,只跟朋友說話,不與陌生人說話。

在上文中我舉的例子,祖父只跟自己的親兒子(Father)說話,而不跟孫子說話。

  • 5、圈複雜度

在軟體測試的概念裡,圈複雜度用來衡量一個模組判定結構的複雜程度,數量上表現為線性無關的路徑條數,即合理的預防錯誤所需測試的最少路徑條數。圈複雜度大說明程式程式碼可能質量低且難於測試和維護,根據經驗,程式的可能錯誤和高的圈複雜度有著很大關係。
據說在Oracle資料庫中有一些屎山程式碼,是通過一堆標識量來判斷某些特定邏輯的,大概是這樣的。
(示例僅供參考,由於資源限制,未能考證,還請大佬指正一二。)

/// <summary>
/// 一個高複雜度的方法
/// </summary>
public string HighCCMethod()
{
    int flag = 1;
    int flag1 = 2;
    int flag2 = 3;
    int flag3 = 4;
    int flag4 = 5;
    if (flag == 1)
    {
        //do something
        if (flag1 == 2)
        {
            //dosomething
            if (flag2 == 3)
            {
                //dosomething
                if (flag3 == 4 && flag4 == 5)
                {
                    return "編譯器 die";
                }
            }
        } 
    }
    return "...";
}

這是一個圈複雜度非常複雜的方法,我想任何一個讀到這樣程式碼的開發者都會對自己的人生充滿了積極而樂觀的判斷,那就是“活著比一切都好”。
對於這樣的程式碼,我們應該儘可能的降低程式碼的圈複雜度,讓程式滿足基本可讀的需求。

  • 6、註釋
public void UploadImg()
{
    int flag = 3;
    //標識量為3標識什麼意思我也不知道,我在網上看到的。
    if (flag == 3)
    {
        //dosomething
    }
    //uploadfile();
}

我曾經參加過一個使用objectc編寫的應用的,其中有一段程式碼是這樣的,這個flag大概是魔法值,作者未經考證直接就在程式碼中使用了。然後一直流傳下來,成為一段佳(gui)話(hua)。
還有這樣的註釋。傻傻分不清楚。

/// <summary>
/// 為true標識為真,為false標識為假
/// </summary>
public bool IsTrue { get; set; }
/// <summary>
/// 是否可見,為true標識可見,為false標識不可見
/// </summary>
public bool IsVisible { get; set; }

還有這樣的。

//do something
Thread.Sleep(3000); //專案經理說此處要暫停3000毫秒,以便作為下次效能改進的需求點

Robert大叔如是說:

什麼也比不上放置良好的註釋來得有用。什麼也比不會亂七八糟的註釋更有本事搞亂一個模組。什麼也不會比陳舊、提供錯誤資訊的註釋更有破壞性。

當然很多中國程式設計師自稱其變數命名是自注釋的,例如大概是這樣的。萬能的 Is 命名法,只要是判斷狀態皆可用。
(每個程式設計師能夠成功的生存下來都不容易,他一定有異於常人的本事。)

public bool IsShow { get; set; }
public bool IsGet { get; set; }
public bool IsUsed { get; set; }
  • 7、霰彈式修改

CRUD開發者或許經常會看到這樣的程式碼,例如,如果我要對某一個物件的狀態( Status)進行更改,可能會這麼做:

public class ShotGun1
{
    public void Method1()
    {
        DataStatus dataStatus = new DataStatus();
        dataStatus.Flag = 1 * 3;
        dataStatus.Status = "1234";
    }
}
public class ShotGun2
{
    public void Method2(int i, int status)
    {
        DataStatus dataStatus = new DataStatus();
        dataStatus.Flag = 1 * 3;
        dataStatus.Status = "1234";
    }
}

這種霰彈式程式碼中,一處程式碼規則的變化,可能會需要對許多處程式碼進行同步修改,使得我們的程式碼異常的難以維護。

  • 8、異常

有時候可能會遇到這樣的程式碼,在方法中定義一些文字的狀態碼,然後呼叫方法時,再去判斷這個狀態碼的內容,當返回錯誤碼時,要求呼叫者立即處理錯誤。

public class XXXApi
{
    public CurrentUser CurrentUser { get; }
    public string DoSomething()
    {
        if (GetCurrentUid() == "使用者為空")
        {
            //do something
        }
        else
        {
            //dosomething
        }
        return "";
    }
    public string GetCurrentUid()
    {
        if (CurrentUser == null)
        {
            return "使用者為空";
        }
        return "";
    }
}
public class CurrentUser
{
    public string Uid { get; set; }
}

不如直接丟擲異常,讓異常處理機制進行處理吧。

public string GetCurrentUid()
{
    if (CurrentUser == null)
        throw new NoLoginExecption("");
    return "";
}

9、邊界

9.1 模組間的邊界

即便是簡單的CRUD應用系統,優秀的開發者也能更好的處理應用程式模組間的邊界。某種意義上講,應用程式內部的邊界看起來或許沒有明確的界限之分,但是稍不留心就可能導致應用程式間關係過於紊亂,讓其他開發者捉摸不透。
例如,假設有一段程式碼是這樣的,在使用者操作類中,加入了一個獲取應用資料的方法,確實會讓人很費解吧。(而應用領域驅動設計的思維,或許是一種不錯的模式。)

public class UserService
{
    public string GetAppData(string config)
    {
        //do something
    }
}

9.2應用間的邊界

相對而言,或許應用間的邊界似乎能相對清晰的分析出來?並非如此。
在當今時代,我們很少開發完全與其他應用系統沒有任何關聯的獨立軟體,這意味著我們或許無時無刻都得與其他第三方應用進行介面行為或資料的交換。這讓我們必須確保採取措施讓外來程式碼乾淨利落地整合進自己的程式碼中。
假設有一段程式碼是這樣的:

public class UserService
{ 
    public string GetAppData(string config)
    {
        //do something
    }
    public string UploadOssData(string file)
    {
        OssConfig oss = new OssConfig();
        OssSdk ossSdk = OssSdk.CreateInstance();
        ossSdk.UploadFile(file);
    }
}

在《程式碼整潔之道》書中,Robert大叔推薦應該第三方介面進行隔離,通過Map那樣包裝或者使用介面卡模式將我們的介面轉換成第三方提供的介面。讓程式碼更好地與我們溝通,在邊界兩邊推動內部一致的用法,當第三方程式碼有改動時修改點也會更少。

總結

寫程式碼是開發者的基礎技能,無論你是.NET 開發者,或者 Java 開發者,你都在努力用程式碼實現自己的夢想。如同韓磊老師在譯作《程式碼整理之道》封面上總結全書,寫下的那句詩

“細節之中自有天地,整潔成就卓越程式碼”。

卓越程式碼從來不僅僅只是功能完善、程式碼齊全,做好細節,每個細節就是一方小天地。優雅的程式碼,不僅僅只是開發者的個人能力的體現,更是開發者的立足之本。努力改善壞習慣,提高程式碼質量,時刻消除異味,時刻提高自己,更有助於個人技能的全面發展