自頂向下的編程方法詳解

什麼是自頂向下的編程方法?

百度百科解釋如下:

自頂向下的程式設計方法指的是首先從主控程式開始,然後按介面關係逐次分割每個功能為更小的功能模組,直到最低層模組設計完成為止。自頂向下是一種有序的逐步分層分解和求精的程式設計方法。其特點是層次清楚,編寫方便,調試容易

  我們可以用更加通俗的語言來解釋:

  所謂的自頂向下的編程方法,本質上就是編寫程式的視角從整體的宏觀性逐層進入具體的微觀性的一種編程思想。我們編寫程式時一開始不用思考得事無巨細,把所有細節都想清楚;也不要麵條式的想到哪裡寫到哪裡。而應該是自頂向下的,從一個大的粗的核心的任務開始,逐級細分,最後再完成最底層的具體實現。

自頂向下方法具體示例

  這樣說可能還是過於抽象。我們用一個具體的例子來說明。

  假設有一個類負責處理數據並組織成Json對象供第三方圖表庫進行展現。這個類有個public方法負責從Excel中獲取數據,並組織成Json。那麼這個Public方法應該怎麼寫?

  按照傳統的做法,可能立刻就開始思考採用什麼庫來讀取Excel,Excel中的數據怎麼獲取,獲取之後怎麼組織成json,最後返回。並開始考慮如果擁護給的Excel地址不對怎麼辦,如果Excel本身有bug怎麼辦?並根據這些思考開始給程式碼加上if和else條件判斷。不知不覺一個函數就已經有數百行程式碼了。

麵條式寫法缺點

  倒並不是說這樣寫程式不行,但是這樣寫出來的程式有幾個很大的問題:

  1. 方法程式碼很長,很難維護。這種事無巨細,義大利麵條式的程式碼長度一定短不了,如果業務邏輯更加複雜一點,長度可能會成倍數上漲。而長的程式碼對閱讀來說是巨大的挑戰,後期維護起來很難迅速理解並提出好的優化方案。
  2. 測試不友好,因為事無巨細,很多邏輯和類庫混在一起,非常不利於單元測試。
  3. 編寫過程思路不清晰,雖然前期經過了設計和思考,但是因為揉在一起,缺乏清晰的分層。編程過程是一個高強度的腦力勞動,時間一長,很難一直保持清醒,容易不小心引入bug。

自頂向下編程方法思考過程

  那麼自頂向下的編程思想鼓勵我們怎麼做呢?我們首先思考清楚整個類或者主函數最總要的任務是做什麼,然後進一步細分需要幾步,講步驟抽象成函數,此時不需要函數的具體實現,只需要將函數命名成具體的實現相關即可。還是以上述例子來說明。
  首先這個方法是讀取Excel並返回Json字元串,那麼我們思考這個事情最少應該分成幾步。通過思考,我們認為至少應該是3步:

  1. 找到這個Excel,返回Excel對象。
  2. 讀取這個Excel,返回讀取結果對象。
  3. 將結果對象轉換成Json並返回。

  基於以上分析,我們確認了核心函數中的三大步,並故意忽略了通過什麼方式去找Excel,通過什麼庫讀取Excel,怎麼讀取Excel以及怎麼轉換成Json的細節。

using System.Dynamic;  public Class ReadDataCOnvertToJson{      public string GetJsonDataFormExcel(string excelPath){          //1. 找到這個Excel,返回Excel對象。          Object excelObj = GetExcelObjet(excelPath);          //2. 讀取這個Excel,返回讀取結果對象          Object DataWaitConvertToJson = GetDataWaitConvertToJson(excelObj);          //3. 將結果對象轉換成Json並返回。          return ChangeDataToJson(DataWaitConvertToJson);      }  }

  此時,GetExcelObjet,GetDataWaitConvertToJson,ChangeDataToJson都沒有實現,編譯器上甚至還在報錯(提示方法不存在),返回的Excel對象也還沒有確定具體的類型,從Excel中讀取的數據組成的類也沒有確定具體的結構。但是這個程式我們已經「寫完了」。因為這個方法最重要最核心的部分我們確實完成了,言簡意賅,思路清晰明確。剩下的事情是其他的內部函數應該完成的事情了。

  此時,我們甚至已經可以開始為我們的GetJsonDataFormExcel方法編寫單元測試了,我們很容易理解GetJsonDataFormExcel要做什麼,此時編寫單元測試是最佳時機。

  當然,我們此時還不能下班回家——編寫的單元測試現在肯定是跑不通的。我們可以利用現代編譯器強大的功能,讓它幫我們生成一個空的GetExcelObjet,GetDataWaitConvertToJson,ChangeDataToJson。然後我們開始為這些方法填充具體的邏輯。

  此時,我們可以開始思考深層一點的問題,用什麼庫來讀取這個Excel?讀取這個Excel的時候如果根據地址找不到怎麼辦?返回的Excel對象是什麼?然後我們按照這個線索來擴充GetExcelObjet方法。

    private ISheet GetExcelObjet(string excelPath){          if(!ExcelIsExist(excelPath)){              return null;          }          try          {              IWorkbook workbook = WorkbookFactory.Create(importExcelPath);              ISheet sheet = workbook.GetSheetAt(0);//獲取第一個工作薄              return sheet;          }          catch (System.Exception ex)          {              log.info(ex)              throw;          }      }

  我們選擇使用NPOI庫讀取Excel,返回NPOI的ISheet介面。並在方法內部判斷了Excel不存在的情況,做了異常處理。而Excel不存在的具體判斷我們在這一層依舊不關心,放在下層中再具體細化處理。
  就這樣,們再按照相同的方式逐步去實現其他的方法。

  這種逐層細化,逐層向下,從最開始的宏觀框架到越來越明確細節的方式,就是自頂向下編程方法。

自頂向下編程方法的優點

  這樣寫程式碼的最大好處與先前描述的義大利麵條式編碼方式的壞處一一對應。

  1. 編碼過程中思路更清晰,就像我們寫高品質的文章一樣,先有題目,再有大綱,再有二級大綱,最後才是正文內容。有了這些大綱的指引,我們可以暫時屏蔽繁瑣的細節,關注任務的全貌,使得編寫的程式碼結構清晰,不容易引入bug。
  2. 程式碼閱讀更加容易,方便後期維護。閱讀自頂向下編程方式寫出來的程式碼,就像是看報紙,在閱讀更多的細節之前,我們更希望大致了解報紙文章的大致中心思想。同理,在我們閱讀一段程式碼時,如果能夠大致了解它的核心頂層工作方式,能夠更好的幫助我們理解。
  3. 因為程式碼的編寫過程是自頂向下逐層細化的,我們在可以在編碼的早期就形成了一個可以被單元測試的函數。這很符合測試框架的流程——測試框架不關心程式內部是如何實現的,只要能夠根據傳入的參數返回正確的結果即可。同理,我們早期在尚未真正實現底層函數時,可以通過mock或硬編碼的形式,讓單元測試得以通過,更早的將主方法或類放到單元測試的保護之下,再真正實現過程中,就可以更加遊刃有餘。
  4. 解決了很多人習慣性的在編碼完成後,再去進行所謂的重構優化的難題。義大利麵條式的程式碼一旦形成,想要拆分確實是難上加難,既然如此為什麼不一開始就忽略細節,編寫更好重構的程式碼?當然,並不是說自頂向下的編碼方式就不用特意去重構了,當我們的最底層具體實現過長過於複雜的情況下,我們依然應該將其打碎,拆分,重命名,並放到合適的位置上。

自定向下思想的延伸

  自頂向下的思想除了在編程過程中有用以外,還可以運用到日常生活中很多其他的地方,如剛剛說過了寫文章,再比如制定個人計劃等等,我們人類天生就是擅長這種思考方式。希望這篇文章能夠幫助到大家。

Tags: