成就卓越代碼,從關注細節開始

開篇

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

    一些栗子

  • 一、命名規則
    • 1.1 變量命名和方法命名

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

/// <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 是常常容易弄混淆的一種概念,可是在某些代碼中,出現了下面的命名方式就有點使人窒息了。web

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特殊狀況術語和字段對照表很是重要

近年來開發者素質愈來愈高,因此許多優秀開發者會傾向於使用翻譯軟件來翻譯變量名,而後用英語來命名,可是即使如此,許多政務項目老是能嗅出一些奇怪的味道。
例如前不久看到一條這樣的短信:(原圖已經消失)json

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

這是個bug將xxx公積金中心的某些祕密透露在你們面前。做爲一個嚴謹的項目,竟然使用中文首字母大寫命名法,這讓習慣於大駝峯、小駝峯的我看了以後尷尬癌犯了,很不舒服。可是這也是許多政務信息化項目的中字段命名的規範,並且在這種狀況下,每每會輸出一份很是規範的數據庫字段對照表,確保中文和首字母的語義不讓人產生歧義。
因此特定語境下,變量和方法自己沒有嚴格的規定,可是必定要使用恰當的語境概念,對於這樣的特定場景,儘可能維護一份實時更新的術語表吧。後端

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

彷佛在對外提供接口時,使用下列接口狀態碼是一種比較常見的慣例。提供統一格式的 code 狀態碼以及返回的消息和成功返回結果時的填充數據,可以讓開發者高效的完成接口對接,無需關心http狀態碼背後的含義。api

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

上面這是一種經典的流派,還有一種流派則會使用http狀態碼來返回指定的數據,事實上 http 協議自己已經提供了許多狀態碼,例以下面的這些你們都很是熟悉的狀態碼。
可是這些狀態碼爲啥不夠?主要是爲了減小先後端、服務上下游之間接口對接的難度,也是一種提升效率的方式。 可是 http 狀態碼是一種通用的格式,應儘可能使用這種方式,而不該該經過解析正常響應後的 json 來判斷是否正確操做。服務器

200 :正常響應 標準成功代碼和默認選項。  
    201 :建立對象。 適用於存儲行爲。 
    204 :沒有內容。 當一個動做成功執行,但沒有任何內容能夠返回。   
    206 :部份內容。 當您必須返回分頁的資源列表時頗有用。   
    400 :請求不正確 沒法經過驗證的請求的標準選項。  
    401 :未經受權 用戶須要進行身份驗證。   
    403 :禁止 用戶已經過身份驗證,但沒有執行操做的權限。   
    404 :找不到資源自動返回。 
    500 :內部服務器錯誤。 理想狀況下,您不會明確地返回此消息,可是若是發生意外中斷,這是您的用戶將會收到的。 
    503 :服務不可用 至關自我解釋,還有一個不會被應用程序顯式返回的代碼。
  • 三、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.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)說話,而不跟孫子說話。

  • 五、圈複雜度

在軟件測試的概念裏,圈複雜度用來衡量一個模塊斷定結構的複雜程度,數量上表現爲線性無關的路徑條數,即合理的預防錯誤所需測試的最少路徑條數。圈複雜度大說明程序代碼可能質量低且難於測試和維護,根據經驗,程序的可能錯誤和高的圈複雜度有着很大關係。
聽說在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 "...";
}

這是一個圈複雜度很是複雜的方法,我想任何一個讀到這樣代碼的開發者都會對本身的人生充滿了積極而樂觀的判斷,那就是「活着比一切都好」。
對於這樣的代碼,咱們應該儘量的下降代碼的圈複雜度,讓程序知足基本可讀的需求。

  • 六、註釋
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; }
  • 七、霰彈式修改

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";
    }
}

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

  • 八、異常

有時候可能會遇到這樣的代碼,在方法中定義一些文本的狀態碼,而後調用方法時,再去判斷這個狀態碼的內容,當返回錯誤碼時,要求調用者當即處理錯誤。

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.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 開發者,你都在努力用代碼實現本身的夢想。如同韓磊老師在譯做《代碼整理之道》封面上總結全書,寫下的那句詩

「細節之中自有天地,整潔成就卓越代碼」。

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

本文來自: 溪源 | 長沙.NET技術社區

相關文章
相關標籤/搜索