編寫讓別人可以讀懂的代碼

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

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

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

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

咱們所寫的代碼主要是爲了閱讀,其次纔是被機器執行。因此咱們要寫:函數式編程

  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.若是你的團隊代碼寫的醜陋不堪,命名也不規範,註釋必需要有,註釋再不許確也比讀代碼讀着強些。

相關文章
相關標籤/搜索