通常,我使用私有方法来封装可在类中多个位置重用的功能。但是有时我有一个大型的公共方法,可以将其分解为更小的步骤,每个步骤都使用自己的私有方法。这会使公共方法更短一些,但我担心强迫任何读取该方法的人跳到不同的私有方法会损害可读性。
对此有共识吗?拥有较长的公共方法,还是将它们分解成较小的块(即使每个块都不可重用)会更好吗?
通常,我使用私有方法来封装可在类中多个位置重用的功能。但是有时我有一个大型的公共方法,可以将其分解为更小的步骤,每个步骤都使用自己的私有方法。这会使公共方法更短一些,但我担心强迫任何读取该方法的人跳到不同的私有方法会损害可读性。
对此有共识吗?拥有较长的公共方法,还是将它们分解成较小的块(即使每个块都不可重用)会更好吗?
Answers:
不,这不是一个不好的风格。实际上,这是一个非常好的风格。
私有功能不必仅仅因为可重用而存在。这当然是创建它们的一个很好的理由,但是还有另一个原因:分解。
考虑一个功能太多的函数。它有一百行,不可能推理。
如果将此功能拆分为较小的部分,则它仍会像以前一样“完成”许多工作,但会较小。它调用其他应具有描述性名称的函数。主要功能读起来几乎像一本书:先执行A,然后执行B,然后执行C,等等。它调用的函数只能在一个地方调用,但是现在它们变小了。任何特定功能都必须与其他功能沙盒化:它们具有不同的作用域。
当您将一个大问题分解为较小的问题时,即使这些较小的问题(功能)仅使用/解决了一次,您也会获得以下好处:
可读性。没有人能读懂单片函数并完全理解它的作用。您可以继续对自己撒谎,也可以将其分成有意义的小块。
参考地点。现在变得不可能声明和使用变量,然后将其保留,并在100行之后再次使用。这些功能具有不同的范围。
测试。虽然只需要对班级中的公共成员进行单元测试,但也可能需要对某些私人成员进行测试。如果长功能的关键部分可能会从测试中受益,则不可能在不将其提取到单独功能的情况下对其进行独立测试。
模块化。现在您有了私有函数,无论是仅在此处使用还是可重复使用的函数,您都可以找到它们中的一个或多个可以提取到单独的类中。到前一点,由于需要公共接口,因此这个单独的类也可能更易于测试。
将大代码分成更易于理解和测试的较小部分的想法是Bob叔叔的书Clean Code的重点。在撰写此答案时,这本书已经有9年历史了,但今天和当时一样重要。
这可能是个好主意!
在将长的线性动作序列拆分为单独的函数时,我确实会遇到问题,纯粹是为了减少代码库中的平均函数长度:
function step1(){
// ...
step2(zarb, foo, biz);
}
function step2(zarb, foo, biz){
// ...
step3(zarb, foo, biz, gleep);
}
function step3(zarb, foo, biz, gleep){
// ...
}
现在,您实际上已经添加了源代码行,并大大降低了总可读性。尤其是如果您现在在每个函数之间传递大量参数以跟踪状态。kes!
但是,如果您设法将一个或多个行提取到一个纯粹的函数中,该函数可以实现一个明确的目的(即使仅调用一次),那么您就可以提高可读性:
function foo(){
f = getFrambulation();
g = deglorbFramb(f);
r = reglorbulate(g);
}
在现实情况下,这可能并不容易,但是如果您考虑了足够长的时间,通常可以嘲笑一些纯功能。
当您拥有带有漂亮动词名称的函数,并且当您的父函数调用它们并且整个过程看起来像一段散文时,您就会知道自己处在正确的轨道上。
然后,几周后返回时,您会添加更多功能,并且您发现实际上可以重用这些功能之一,然后,狂喜!多么奇妙的光芒四射的喜悦!
foo = compose(reglorbulate, deglorbFramb, getFrambulation);
答案是,这在很大程度上取决于情况。@Snowman涵盖了拆分大型公共职能的积极意义,但记住您的关注也很重要,它也有负面影响。
许多具有副作用的私有函数会使代码难以阅读且非常脆弱。当这些私有功能依赖于彼此的副作用时,尤其如此。避免具有紧密耦合的功能。
抽象是泄漏的。假装如何执行某项操作或如何存储数据无关紧要,但是在某些情况下,它确实是很重要的,因此识别它们很重要。
语义和上下文很重要。如果您不能清楚地捕捉到功能名称的作用,则可以再次降低可读性并增加公共功能的脆弱性。当您开始创建具有大量输入和输出参数的私有函数时,尤其如此。从public函数中,您可以看到它调用了什么私有函数,而从private函数中,您看不到公共函数调用了什么。这可能导致私有功能中的“错误修复”,从而破坏了公共功能。
大量分解的代码对于作者而言总是比其他人更清楚。这并不意味着它对其他人仍然不清楚,但是可以很容易地说,在编写本文bar()
之前必须先调用它是完全有意义foo()
的。
危险重用。您的公共职能可能会限制每个私有职能的可能输入。如果未正确捕获这些输入假设(很难记录所有假设),则有人可能会不正确地重用您的私有函数之一,从而将错误引入代码库。
根据函数的内部内聚和耦合而不是绝对长度来分解函数。
MainMethod
(很容易得到,通常包括符号,并且您应该有符号取消引用文件),它是2k行(行号从不包含在其中)错误报告),并且这是一个NRE,可能发生在其中的1k行中。但是,如果他们告诉我它发生在SomeSubMethodA
8行中,并且例外是NRE,那么我可能会发现并修复该问题足够容易。
恕我直言,纯粹是为了打破复杂性而取出代码块的价值,通常与以下两者之间的复杂性差异有关:
有关代码功能的完整且准确的人工语言描述,包括代码如何处理极端情况,以及
代码本身。
如果代码比描述复杂得多,则用函数调用替换内联代码可能会使周围的代码更容易理解。另一方面,某些概念可以用计算机语言比用人类语言更可读地表达。w=x+y+z;
例如,我认为比更具可读性w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);
。
随着大型功能的分解,子功能的复杂性与其描述之间的差异将越来越小,而进一步细分的优势将降低。如果事情分裂到描述比代码复杂的程度,进一步的分裂会使代码更糟。
int average3(int n1, int n2, int n3)
。拆分“平均”功能意味着,想要查看它是否适合需求的某人将不得不在两个地方查看。
这是一个平衡的挑战。
私有方法有效地提供了所包含代码的名称,有时还提供了有意义的签名(除非其参数的一半是完全临时的流氓数据,并且具有相互之间不明确的,未记录的相互依赖性)。
给名称命名以代码构造通常是好的,只要名称对调用者暗示有意义的约定,并且私有方法的约定准确地对应于名称所暗示的含义。
通过强迫自己考虑代码的较小部分的有意义的约定,初始开发人员可以发现一些错误,甚至在进行任何开发人员测试之前就可以轻松地避免它们。这仅在开发人员力求简洁命名(听起来简单但准确)并且愿意适应私有方法的边界以使简洁命名成为可能时才起作用。
后续维护也得到了帮助,因为附加的命名有助于使代码自我记录。
是否有可能过度给小块代码命名,并以太多,太小的私有方法结束?当然。以下一种或多种症状表明方法太小而无法使用:
XxxInternal
或DoXxx
,特别是如果没有统一的名称来介绍这些装饰。LogDiskSpaceConsumptionUnlessNoUpdateNeeded
与其他人所说的相反,我认为长期的公共方法是一种设计气味,不能通过分解为私有方法来纠正。
但是有时候我有一个大型的公共方法,可以分解为较小的步骤
如果是这种情况,那么我认为每个步骤都应该是其自己的头等公民,并且每个人都负有单一责任。在面向对象的范例中,我建议为每个步骤创建一个接口和一个实现,以使每个步骤都有一个易于识别的职责,并且可以以明确职责的方式来命名。这使您可以对(以前)大型的公共方法以及每个独立的步骤进行单元测试。它们也都应记录在案。
为什么不分解为私有方法?原因如下:
但我担心强迫任何阅读该方法的人跳到不同的私有方法会损害可读性
这是我最近与一位同事争论的话题。他认为,将模块的整个行为包含在相同的文件/方法中可以提高可读性。我同意这些代码在一起时更易于遵循,但是随着复杂性的增加,代码也就不那么容易推理了。随着系统的发展,对整个模块作为一个整体进行推理变得很困难。当您将复杂的逻辑分解为几个类,每个类具有单个职责时,则对每个部分进行推理就变得容易得多。