9.8 拆分和组合方法
Last updated
Last updated
何时细分的问题不仅适用于类,也适用于方法:是否有的时候,将一个现有的方法分成多个小方法更好?或者,是否应该将两个较小的方法合并成一个较大的方法?长的方法往往比短的方法更难理解,所以很多人认为仅仅长度就可以作为拆分一个方法的理由。课堂上的学生经常被赋予严格的标准,例如“把任何超过20行的方法拆开!”
然而,长度本身很少是拆分一个方法的好理由。一般来说,开发者往往将方法拆分得过多。拆分一个方法会引入额外的接口,从而增加了复杂性。它还分离了原始方法的各个部分,如果这些部分实际上是相关的,这会使代码更难阅读。你不应该拆分一个方法,除非它使整个系统变得更简单;我将在下面讨论这种情况如何发生。
长方法并不总是坏事。例如,假设一个方法包含5个20行的代码块,按顺序执行。如果这些代码块是相对独立的,则该方法可以一次一个块地阅读和理解;将每个代码块移到一个单独的方法中并没有什么好处。如果这些块有复杂的交互,那么把它们放在一起就更重要了,这样读者就可以一次看到所有的代码;如果每个块都在一个单独的方法中,读者就不得不在这些分散的方法之间来回翻阅,以了解它们如何协同工作。包含数百行代码的方法如果有一个简单的签名,并且容易阅读就没什么问题。这些方法是深的(功能很多,接口简单),这很好。
在设计方法时,最重要的目标是提供干净和简单的抽象。每个方法都应该做一件事,而且要做得完整。方法应该有一个干净简单的接口,这样用户就不需要在脑子里有很多信息就能正确使用它。该方法应当是深的:它的接口应当比实现简单得多。如果一个方法具备所有这些特性,那么它长不长可能并不重要。
只有当拆分一个方法能带来更干净的抽象时,它才有意义。有两种方法可以做到这一点,如图9.3所示。最好的方法是将一个子任务分解成一个单独的方法,如图9.3(b)所示。细分的结果是一个包含子任务的子方法和一个包含原方法剩余部分的父方法;父方法调用子方法。新的父方法的接口与原方法相同。如果有一个子任务可以与原方法的其余部分干净地分开,这种形式的细分是有意义的,这意味着(a)阅读子方法的人不需要知道关于父方法的任何事情,(b)阅读父方法的人不需要理解子方法的实现。通常情况下,这意味着子方法是相对通用的:可以想象它能够被除父方法之外的其他方法所使用。如果你做了这种形式的拆分,然后发现自己在父方法和子方法之间来回切换,以了解它们是如何一起工作的,这就是一个危险信号(“连体方法”),表明这种拆分可能是一个坏主意。
拆分一个方法的第二种方式是将其拆分成两个独立的方法,每个方法对原始方法的调用者都是可见的,如图9.3(c)。如果原来的方法有一个过于复杂的接口,因为它试图做多个没有密切联系的事情,那么这种做法是有意义的。如果是这种情况,也许可以将该方法的功能分成两个或更多的小方法,每个方法只具有原方法的一部分功能。如果你做了这样的拆分,那么所产生的每个方法的接口应该比原始方法的接口更简单。理想情况下,大多数调用者应该只需要调用这两个新方法中的一个;如果调用者必须同时调用这两个新方法,那么就会增加复杂性,这使得拆分不大可能是一个好主意。新的方法将在它们所做的事情上更加集中。如果新的方法比原来的方法更具通用性(也就是说,你可以想象在其他情况下分别使用它们),这是个好现象。
图 9.3(c) 所示形式的拆分通常没有意义,因为它们导致调用者不得不处理多个方法而不是一个。当你以这种方式拆分时,你会冒着以几个浅方法告终的风险,如图 9.3(d) 所示。如果调用者必须调用每个单独的方法,在它们之间来回传递状态,那么拆分就不是一个好主意。如果你正在考虑像图 9.3(c) 中的拆分,你应该根据它是否为调用者简化事情来判断。
在有些情况下,可以通过将方法组合在一起来使系统变得更简单。例如,连接方法可能会用一个更深的方法取代两个浅的方法;它可能会消除代码重复;它可能会消除原始方法或中间数据结构之间的依赖关系;它可能会导致更好的封装,从而使以前存在于多个地方的知识现在被隔离在一个地方;或者它可能会产生更简单的接口,正如中所讨论的。