重构之美之一方法的长度
我曾经在一次演讲中,问过听众这样一个问题:“一个方法的理想行数最多不超过多少行?”如果问一千个人,或许会有一千条答案吧。
这是一个见仁见智的问题。在《软件开发沉思录》一书中,ThoughtWorks的技术负责人Jeff Bay认为:“一个常见的原则是将方法的行数控制在5行之内……”很多人对此感到不可思议。窃以为,关键不在于方法的最大行数,而是要合理理解方法的微粒度能为我们带来什么好处?
Jeff Bay提倡“利用IDE提供的‘提取方法’功能,不断地提取方法中的行为,直到它只有一级缩进为止。如果方法过长,不可能达到如此清晰的可读性。”Robert C. Martin则强调:“方法的第一规则是要短小。第二条规则还要更短小。”
短小的方法更容易理解,更容易重用。这一点毋庸置疑。不过,短小的方法会导致方法数量的急剧增加,这会否对阅读造成干扰?另一个顾虑则是我们在阅读方法时,短小的方法必然会导致我们需要在方法之间跳来跳去。
这两个问题,归根结底还是方法命名的问题。只要方法名称能够清晰地表达其意图,读其名即可知其实,就没有必要刨根问底追溯方法的内部实现了。钱锺书先生拒绝追慕者时,委婉地说道:“假如你吃了个鸡蛋觉得不错,何必认识那下蛋的母鸡呢?”阅读代码遵循同样的道理,如果你已经从方法名称中知晓它的意图与职责,何必还要解剖方法体的内部呢?而这正是封装的意义。唯一的例外是你希望学习和挖掘实现的内部机制。
至于方法之间的跳转问题,目前,许多IDE工具已经提供了方便跳转的功能。因此,不足为虑。
好的代码应该像一篇优秀的散文,明白通畅,清新自然。写文章要求专注于一件事,不要让别的无关内容干扰它。如果事情较为棘手,可以将一部分功能字斟句酌,分而治之。编写可读性强的代码尤其需要重视构成代码的基本元素:方法。虽然在面向对象设计中,我们应以对象的角度思考问题,但对于方法而言,我们仍然可以借鉴面向过程的设计,需要理清该方法履行职责的过程。就好似在心中绘制出方法的流程图,对流程进行分解,并以各个方法代表每个步骤,就能写出像散文一般的代码。
一种常见的做法是在实现一个主要方法时,先不去实现具体的代码,而是写一些小方法的名称,即首先完成主方法的模板框架,再分别实现构成主方法的小方法。看似很难,实际上只要运用得当,就会使编码变得更加轻松。它使得我们能够从具体的方法实现中解放出来,先确定实现该功能的流程,再考虑每一步的具体实现。
例如,我要实现这样一个方法,它需要通过传入的ServletRequest对象,获取某些信息,执行步骤如下所示:
1)首先获得ReportObject对象;
2)根据request对象收集必须的参数;
3)通过ReportObject对象以及收集获得参数来组建一个ReportTable对象;
4)然后,将ReportTable设置给ExcelReportExporter对象;
5)最后,将ExcelReportExporter对象存储到Session中。
我在编写该方法时,并没有实现每个步骤的具体细节,而是以各个小方法体现这些步骤,最后再利用Eclipse提供的功能,快速生成这些方法,并实现之。
public void saveReportExporter(ServletRequest request) {
ReportObject reportObj = getReportObject(request);
Map<String, Object> externalParams = collectExternalParams(request);
ReportTable report = getReportTable(reportObj, externalParams);
getReportExporterAndSaveToSession(report,request);
}
主方法的代码没有超过五行,且每一行代码都通过方法的名称表达了清晰的功能职责。在阅读这样的代码时,若不需要了解每一步骤的具体细节,则阅读主方法的实现即可理解其意图,甚至不需要再为它添加额外的注释。
这并非重构,但与Extract Method重构所要达成的目的一脉相承。
这样分解方法还有一个好处,就是它能够帮助我们更清晰地分辨职责。当你发现分解出来的方法所履行的职责,与它所属的类格格不入,或者需要公开给其他类调用时,正是需要重新分配职责的好时机。