編寫讓別人能夠讀懂的代碼

隨着軟件行業的不斷髮展,歷史遺留的程序越來越多,代碼的維護成本越來越大,甚至大於開發成本。而新功能的開發又常常依賴於舊代碼,閱讀舊代碼所花費的時間幾乎要大於寫新功能的時間。

我前幾天看了一本書,書中有這麼一句話:

“複雜的代碼往往都是新手所寫,只有經驗老道的高手才能寫出簡單,富有表現力的代碼”

此話雖然說的有點誇張,可是也說明了經驗和智慧的的重要性。

我們所寫的代碼主要是爲了閱讀,其次纔是被機器執行。所以我們要寫:

  1. 讓別人能讀懂的代碼
  2. 可擴展的代碼
  3. 可測試的代碼(代碼應該具備可測試性,對沒有可測試性的代碼寫測試,是浪費生命的表現)

其中2,3點更多強調的是面向對象的設計原則。而本文則更多關注於局部的代碼問題,本文通過舉例的方式,總結平時常犯的錯誤和優化方式。

本文的例子基於兩個指導原則:

一.DRY(Don't repeat yourself)

此原則如此重要,簡單來說是因爲:

  • 代碼越少,Bug也越少
  • 沒有重複邏輯的代碼更易於維護,當你修復了一個bug,如果相同的邏輯還出現在另外一個地方,而你沒意識到,你有沒有覺得自己很冤?

二.TED原則

  • 簡潔(Terse)
  • 具有表達力(Expressive)
  • 只做一件事(Do one thing)

三.舉例說明

1.拒絕註釋,用代碼來闡述註釋

反例:

         /// <summary>
        /// !@#$%^&^&*((!@#$%^&^&*((!@#$%^&^&*((!@#$%^&^&*((
        /// </summary>
        /// <returns></returns>
         public decimal GetCash()
         {
             //!@#$%^&^&*((!@#$%^&^&*((
             var a = new List<int>() { 2, 3, 10 };
             var b = 2m;
             var c = 0m;
             //!@#$%^&^&*((!@#$%^&^&*((!@#$%^&^&*((
             foreach (var p in a)
             {
                 c += p*b;
             }
             return c;
         }

重構後:

        public decimal CalculateTotalCash()
        {
            var itemCounts=new List<int>(){2,3,10};
            var price = 2m;
            return itemCounts.Sum(p => p*price );
        }

良好的代碼命名完全可以替代註釋的作用,如果你正在試圖寫一段註釋,從某種角度來看,你正在試圖寫一段別人無法理解的代碼。

當你無法爲你的方法起一個準確的名稱時,很可能你的方法不止做了一件事,違反了(Do one thing)。特別是你想在方法名中加入:And,Or,If等詞時

2. 爲布爾變量賦值

反例:

        public bool IsAdult(int age)
        {
            bool isAdult;
            if (age > 18)
            {
                isAdult = true;
            }
            else
            {
                isAdult = false;
            }
            return isAdult;
        }

重構後:

        public bool IsAdult(int age)
        {
            var isAdult = age > 18;
            return isAdult;
        }

3.雙重否定的條件判斷

反例:

 if (!isNotRemeberMe)
{

 }

重構後:

if (isRemeberMe)
{

}

不管你有沒有見過這樣的條件,反正我見過。見到這樣的條件判斷,我頓時就暈了。

4.拒絕HardCode,拒絕挖坑

反例:

if (carName == "Nissan")
 {

}

重構後:

if (car == Car.Nissan)
{

 }

既然咱們玩的是強類型語言,咱就用上編譯器的功能,讓錯誤發生在編譯階段

5.拒絕魔數,拒絕挖坑

反例:

if (age > 18)
{

}

重構後:

const int adultAge = 18;
 if (age > adultAge)
{

}

所謂魔數(Magic number)就是一個魔法數字,讀者完全弄不明白你這個數字是什麼,這樣的代碼平時見的多了

6.複雜的條件判斷

反例:

            if (job.JobState == JobState.New
                || job.JobState == JobState.Submitted
                || job.JobState == JobState.Expired
                || job.JobTitle.IsNullOrWhiteSpace())
            {
                //....
            }

重構後:

            if (CanBeDeleted(job))
            {
                //
            }        

        private bool CanBeDeleted(Job job)
        {
            var invalidJobState = job.JobState == JobState.New
                                  || job.JobState == JobState.Submitted
                                  || job.JobState == JobState.Expired;
            var invalidJob = string.IsNullOrEmpty(job.JobTitle);

            return invalidJobState || invalidJob;
        }

有沒有豁然開朗的趕腳?

7.嵌套判斷

反例:

            var isValid = false;
            if (!string.IsNullOrEmpty(user.UserName))
            {
                if (!string.IsNullOrEmpty(user.Password))
                {
                    if (!string.IsNullOrEmpty(user.Email))
                    {
                        isValid = true;
                    }
                }
            }
            return isValid;

重構後:

if (string.IsNullOrEmpty(user.UserName)) return false;
if (string.IsNullOrEmpty(user.Password)) return false;
if (string.IsNullOrEmpty(user.Email)) return false;
 return true;

第一種代碼是受到早期的某些思想:使用一個變量來存儲返回結果。事實證明,你一旦知道了結果就應該儘早返回。

8.使用前置條件

反例:

            if (!string.IsNullOrEmpty(userName))
            {
                if (!string.IsNullOrEmpty(password))
                {
                    //register
                }
                else
                {
                    throw new ArgumentException("user password can not be empty");
                }
            }
            else
            {
                throw new ArgumentException("user name can not be empty");
            }

重構後:

 if (string.IsNullOrEmpty(userName)) throw new ArgumentException("user name can not be empty");
 if (string.IsNullOrEmpty(password)) throw new ArgumentException("user password can not be empty");
 //register

重構後的風格更接近契約編程,首先要滿足前置條件,否則免談。

9.參數過多,超過3個

反例:

        public void RegisterUser(string userName, string password, string email, string phone)
        {

        }

重構後:

        public void RegisterUser(User user)
        {

        }

過多的參數讓讀者難以抓住代碼的意圖,同時過多的參數將會影響方法的穩定性。另外也預示着參數應該聚合爲一個Model

10.方法簽名中含有布爾參數

反例:

       public void RegisterUser(User user, bool sendEmail)
        {

        }

重構後:

        public void RegisterUser(User user)
        {

        }

        public void SendEmail(User user)
        {

        }

布爾參數在告訴方法不止做一件事,違反了Do one thing

10.寫具有表達力的代碼

反例:

        private string CombineTechnicalBookNameOfAuthor(List<Book> books, string author)
        {
            var filterBooks = new List<Book>();

            foreach (var book in books)
            {
                if (book.Category == BookCategory.Technical && book.Author == author)
                {
                    filterBooks.Add(book);
                }
            }
            var name = "";
            foreach (var book in filterBooks)
            {
                name += book.Name + "|";
            }
            return name;
        }

重構後:

       private string CombineTechnicalBookNameOfAuthor(List<Book> books, string author)
        {
            var combinedName = books.Where(b => b.Category == BookCategory.Technical)
                .Where(b => b.Author == author)
                .Select(b => b.Name)
                .Aggregate((a, b) => a + "|" + b);

            return combinedName;
        }

相對於命令式代碼,聲明性代碼更加具有表達力,也更簡潔。這也是函數式編程爲什麼越來越火的原因之一。

四.關於DRY

平時大家重構代碼,一個重要的思想就是DRY。我要分享一個DRY的反例:

項目在架構過程中會有各種各樣的MODEL層,例如:DomainModel,ViewModel,DTO。很多時候這幾個Model裏的字段大部分是相同的,於是有人就會想到DRY原則,乾脆直接用一種類型,省得粘貼複製,來回轉換。

這個反例失敗的根本原因在於:這幾種Model職責各不相同,雖然大部分情況下內容會有重複,但是他們擔當着各種不同的角色。

考慮這種場景: DomainModel有一個字段DateTime Birthday{get;set;},ViewModel同樣具有DateTime Birthday{get;set;}。需求升級:要求界面不再顯示生日,只需要顯示是否成年。我們只需要在ViewModel中添加一個Bool IsAdult{get{return ....}}即可,DomainModel完全不用變化。

五.利用先進的生產工具

以vs插件中的Reshaper爲例,本文列舉的大部分反例,Reshaprer均能給予不同程度的提示。經過一段時間的練習,當Reshaper對你的代碼給予不了任何提示的時候,你的代碼會有一個明顯的提高。

截圖說明Reshaper的提示功能:

光標移動在波浪線處,然後Alt+Enter,Resharper 會自動對代碼進行優化

如果你能夠避免本文總結的反例,你的代碼就已經具備了優秀代碼應有的基因。當然高質量的代碼還需要良好的設計和遵循面向對象編程的原則。 如果想了解更多相關內容,請閱讀《代碼大全》,《代碼整潔之道》,《重構 改善既有代碼的設計》,《敏捷軟件開發 原則、模式與實踐》

 

 

追加:既然大家對拒絕註釋這個建議覺得不可行,我拿出來詳細說說: 1.如果你的團隊代碼命名不錯,代碼寫的還算不錯,沒有一些奇怪的實現方式,不妨可以嘗試去掉註釋試試效果。因爲寫註釋需要成本,並且重構代碼的時候往往會忽略註釋的修改,時間長了註釋也不夠準確,反而成了一種累贅。 2.如果你的團隊代碼寫的醜陋不堪,命名也不規範,註釋必須要有,註釋再不準確也比讀代碼讀着強些。

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