自顶向下的编程方法详解

什么是自顶向下的编程方法?

百度百科解释如下:

自顶向下的程序设计方法指的是首先从主控程序开始,然后按接口关系逐次分割每个功能为更小的功能模块,直到最低层模块设计完成为止。自顶向下是一种有序的逐步分层分解和求精的程序设计方法。其特点是层次清楚,编写方便,调试容易

  我们可以用更加通俗的语言来解释:

  所谓的自顶向下的编程方法,本质上就是编写程序的视角从整体的宏观性逐层进入具体的微观性的一种编程思想。我们编写程序时一开始不用思考得事无巨细,把所有细节都想清楚;也不要面条式的想到哪里写到哪里。而应该是自顶向下的,从一个大的粗的核心的任务开始,逐级细分,最后再完成最底层的具体实现。

自顶向下方法具体示例

  这样说可能还是过于抽象。我们用一个具体的例子来说明。

  假设有一个类负责处理数据并组织成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. 解决了很多人习惯性的在编码完成后,再去进行所谓的重构优化的难题。意大利面条式的代码一旦形成,想要拆分确实是难上加难,既然如此为什么不一开始就忽略细节,编写更好重构的代码?当然,并不是说自顶向下的编码方式就不用特意去重构了,当我们的最底层具体实现过长过于复杂的情况下,我们依然应该将其打碎,拆分,重命名,并放到合适的位置上。

自定向下思想的延伸

  自顶向下的思想除了在编程过程中有用以外,还可以运用到日常生活中很多其他的地方,如刚刚说过了写文章,再比如制定个人计划等等,我们人类天生就是擅长这种思考方式。希望这篇文章能够帮助到大家。