具有单个引用的私有方法是否不好?


139

通常,我使用私有方法来封装可在类中多个位置重用的功能。但是有时我有一个大型的公共方法,可以将其分解为更小的步骤,每个步骤都使用自己的私有方法。这会使公共方法更短一些,但我担心强迫任何读取该方法的人跳到不同的私有方法会损害可读性。

对此有共识吗?拥有较长的公共方法,还是将它们分解成较小的块(即使每个块都不可重用)会更好吗?



7
所有答案均基于意见。原始作者始终认为他们的馄饨代码更具可读性;随后的编辑者称其为馄饨。
Frank Hileman

5
我建议您阅读清洁代码。它推荐这种风格,并有很多示例。它还提供了解决“跳跃”问题的解决方案。

1
我认为这取决于您的团队以及所包含的代码行。
HopefullyHelpful

1
STRUTS的整个框架是否不是基于一次性使用的Getter / Setter方法,而所有方法都是一次性使用的方法?
Zibbobz

Answers:


203

不,这不是一个不好的风格。实际上,这是一个非常好的风格。

私有功能不必仅仅因为可重用而存在。这当然是创建它们的一个很好的理由,但是还有另一个原因:分解。

考虑一个功能太多的函数。它有一百行,不可能推理。

如果将此功能拆分为较小的部分,则它仍会像以前一样“完成”许多工作,但会较小。它调用其他应具有描述性名称的函数。主要功能读起来几乎像一本书:先执行A,然后执行B,然后执行C,等等。它调用的函数只能在一个地方调用,但是现在它们变小了。任何特定功能都必须与其他功能沙盒化:它们具有不同的作用域。

当您将一个大问题分解为较小的问题时,即使这些较小的问题(功能)仅使用/解决了一次,您也会获得以下好处:

  • 可读性。没有人能读懂单片函数并完全理解它的作用。您可以继续对自己撒谎,也可以将其分成有意义的小块。

  • 参考地点。现在变得不可能声明和使用变量,然后将其保留,并在100行之后再次使用。这些功能具有不同的范围。

  • 测试。虽然只需要对班级中的公共成员进行单元测试,但也可能需要对某些私人成员进行测试。如果长功能的关键部分可能会从测试中受益,则不可能在不将其提取到单独功能的情况下对其进行独立测试。

  • 模块化。现在您有了私有函数,无论是仅在此处使用还是可重复使用的函数,您都可以找到它们中的一个或多个可以提取到单独的类中。到前一点,由于需要公共接口,因此这个单独的类也可能更易于测试。

将大代码分成更易于理解和测试的较小部分的想法是Bob叔叔的书Clean Code的重点。在撰写此答案时,这本书已经有9年历史了,但今天和当时一样重要。


7
不要忘记保持一致的抽象水平和良好的名称,以确保在方法中偷看不会让您感到惊讶。如果整个对象都隐藏在其中,请不要感到惊讶。
candied_orange

14
有时拆分方法可能很好,但保持联机状态也可以带来好处。例如,如果可以在代码块的内部或外部合理地进行边界检查,那么将该代码块保持在行内将使您很容易看到是否一次,多次或根本没有进行边界检查。 。将代码块放入其自己的子例程中将使此类事情更加难以确定。
超级猫

7
“可读性”项目符号可以标记为“抽象”。这是关于抽象的。“一口大小的块” 只做一件事情,一件底层的事情,当您阅读或逐步使用公共方法时,您实际上并不关心这些细节。通过将其抽象为功能,您可以单步执行并继续关注大型事物方案/公共方法在更高层次上所做的工作。
Mathieu Guindon

7
国际海事组织对“测试”项目符号表示怀疑。如果您的私人成员想接受测试,则他们可能属于公共成员,属于自己的班级。当然,提取类/接口的第一步是提取方法。
Mathieu Guindon

6
完全不考虑实际功能的情况下,仅通过行号,代码块和变量范围拆分功能是一个糟糕的主意,我认为更好的替代方法是将其留在一个功能中。您应该根据功能拆分功能。请参阅DaveGauer的答案。您在回答中稍有暗示(提及名称和问题),但是我不禁觉得这方面需要更多地关注,因为它在问题中的重要性和相关性。
Dukeling

36

这可能是个好主意!

在将长的线性动作序列拆分为单独的函数时,我确实会遇到问题,纯粹是为了减少代码库中的平均函数长度:

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);
}

在现实情况下,这可能并不容易,但是如果您考虑了足够长的时间,通常可以嘲笑一些纯功能。

当您拥有带有漂亮动词名称的函数,并且当您的父函数调用它们并且整个过程看起来像一段散文时,您就会知道自己处在正确的轨道上。

然后,几周后返回时,您会添加更多功能,并且您发现实际上可以重用这些功能之一,然后,狂喜!多么奇妙的光芒四射的喜悦!


2
我已经听到这种说法很多年了,在现实世界中从来没有出现过。我专门讲一两个行函数,仅从一个地方调用。尽管原始作者始终认为它“更具可读性”,但随后的编辑者通常将其称为馄饨代码。一般来说,这取决于通话深度。
Frank Hileman

34
@FrankHileman坦白说,我从未见过任何开发人员赞美其前任的代码。
T. Sar

4
@TSar以前的开发人员是所有问题的根源!
Frank Hileman '17

18
@TSar当我是以前的开发人员时,我什至从未称赞过以前的开发人员。
IllusiveBrian

3
分解的foo = compose(reglorbulate, deglorbFramb, getFrambulation);
好处

19

答案是,这在很大程度上取决于情况。@Snowman涵盖了拆分大型公共职能的积极意义,但记住您的关注也很重要,它也有负面影响。

  • 许多具有副作用的私有函数会使代码难以阅读且非常脆弱。当这些私有功能依赖于彼此的副作用时,尤其如此。避免具有紧密耦合的功能。

  • 抽象是泄漏的。假装如何执行某项操作或如何存储数据无关紧要,但是在某些情况下,它确实是很重要的,因此识别它们很重要。

  • 语义和上下文很重要。如果您不能清楚地捕捉到功能名称的作用,则可以再次降低可读性并增加公共功能的脆弱性。当您开始创建具有大量输入和输出参数的私有函数时,尤其如此。从public函数中,您可以看到它调用了什么私有函数,而从private函数中,您看不到公共函数调用了什么。这可能导致私有功能中的“错误修复”,从而破坏了公共功能。

  • 大量分解的代码对于作者而言总是比其他人更清楚。这并不意味着它对其他人仍然不清楚,但是可以很容易地说,在编写本文bar()之前必须先调用它是完全有意义foo()的。

  • 危险重用。您的公共职能可能会限制每个私有职能的可能输入。如果未正确捕获这些输入假设(很难记录所有假设),则有人可能会不正确地重用您的私有函数之一,从而将错误引入代码库。

根据函数的内部内聚和耦合而不是绝对长度来分解函数。


6
忽略可读性,让我们看一下错误报告方面:如果用户报告错误发生在其中MainMethod(很容易得到,通常包括符号,并且您应该有符号取消引用文件),它是2k行(行号从不包含在其中)错误报告),并且这是一个NRE,可能发生在其中的1k行中。但是,如果他们告诉我它发生在SomeSubMethodA8行中,并且例外是NRE,那么我可能会发现并修复该问题足够容易。
410_Gone

2

恕我直言,纯粹是为了打破复杂性而取出代码块的价值,通常与以下两者之间的复杂性差异有关:

  1. 有关代码功能的完整且准确的人工语言描述,包括代码如何处理极端情况,以及

  2. 代码本身。

如果代码比描述复杂得多,则用函数调用替换内联代码可能会使周围的代码更容易理解。另一方面,某些概念可以用计算机语言比用人类语言更可读地表达。w=x+y+z;例如,我认为比更具可读性w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);

随着大型功能的分解,子功能的复杂性与其描述之间的差异将越来越小,而进一步细分的优势将降低。如果事情分裂到描述比代码复杂的程度,进一步的分裂会使代码更糟。


2
您的函数名称具有误导性。w = x + y + z上没有任何内容可以指示任何类型的溢出控制,而方法名称本身似乎暗示其他含义。例如,函数的更好名称是“ addAll(x,y,z)”,甚至只是“ Sum(x,y,z)”。
T. Sar

6
既然谈论的是私有方法,那么这个问题就不可能指的是C。无论如何,这不是我的意思-您可以仅调用同一函数“ sum”,而无需处理方法签名上的假设。根据您的逻辑,例如,任何递归方法都应称为“ DoThisAssumingStackDoesntOverflow”。“ FindSubstringAssumingStartingPointIsInsideBounds”也是如此。
T. Sar

1
换句话说,基本假设,无论它们可能是什么(两个数字都不会溢出,堆栈不会溢出,索引正确传递)不应出现在方法名称上。当然,如果需要的话,这些东西应该被记录下来,但是不要在方法签名上记录下来。
T. Sar

1
@TSar:我对这个名称有些不满意,但是关键是(切换到平均,因为这是一个更好的示例),在上下文中看到“(x + y + z + 1)/ 3”的程序员将相较于查看的定义或调用站点的人员,要更好地了解这是否是计算给定调用代码的平均值的合适方法int average3(int n1, int n2, int n3)。拆分“平均”功能意味着,想要查看它是否适合需求的某人将不得不在两个地方查看。
超级猫

1
如果以C#为例,您将只有一个“ Average”方法,可以接受任何数量的参数。实际上,在c#中,它可以处理任何数字集合 -而且我相信它比将原始数学处理到代码中更容易理解。
T. Sar

2

这是一个平衡的挑战。

越细越好

私有方法有效地提供了所包含代码的名称,有时还提供了有意义的签名(除非其参数的一半是完全临时的流氓数据,并且具有相互之间不明确的,未记录的相互依赖性)。

给名称命名以代码构造通常是好的,只要名称对调用者暗示有意义的约定,并且私有方法的约定准确地对应于名称所暗示的含义。

通过强迫自己考虑代码的较小部分的有意义的约定,初始开发人员可以发现一些错误,甚至在进行任何开发人员测试之前就可以轻松避免它们。这仅在开发人员力求简洁命名(听起来简单但准确)并且愿意适应私有方法的边界以使简洁命名成为可能时才起作用。

后续维护也得到了帮助,因为附加的命名有助于使代码自我记录

  • 小代码合同
  • 较高级别的方法有时会变成简短的调用序列,每个调用序列都执行一些重要且明确命名的调用-伴随着薄薄的常规,最外层的错误处理。对于必须迅速成为模块整体结构专家的人来说,这种方法可以成为宝贵的培训资源。

直到太细为止

是否有可能过度给小块代码命名,并以太多,太小的私有方法结束?当然。以下一种或多种症状表明方法太小而无法使用:

  • 太多的重载实际上并不能代表相同基本逻辑的替代签名,而是一个固定的调用堆栈。
  • 同义词仅用于重复引用相同的概念(“有趣的命名”)
  • 许多敷衍命名的装饰,例如XxxInternalDoXxx,特别是如果没有统一的名称来介绍这些装饰。
  • 笨拙的名称几乎比实现本身更长,例如 LogDiskSpaceConsumptionUnlessNoUpdateNeeded

1

与其他人所说的相反,我认为长期的公共方法是一种设计气味,不能通过分解为私有方法来纠正。

但是有时候我有一个大型的公共方法,可以分解为较小的步骤

如果是这种情况,那么我认为每个步骤都应该是其自己的头等公民,并且每个人都负有单一责任。在面向对象的范例中,我建议为每个步骤创建一个接口和一个实现,以使每个步骤都有一个易于识别的职责,并且可以以明确职责的方式来命名。这使您可以对(以前)大型的公共方法以及每个独立的步骤进行单元测试。它们也都应记录在案。

为什么不分解为私有方法?原因如下:

  • 紧密耦合和可测试性。通过减小公共方法的大小,可以提高其可读性,但是所有代码仍然紧密地耦合在一起。您可以对单个私有方法进行单元测试(使用测试框架的高级功能),但是您不能轻易地独立于私有方法来测试公共方法。这违反了单元测试的原则。
  • 类的大小和复杂性。您降低了方法的复杂度,但增加了类的复杂度。公共方法更易于阅读,但现在类却更难以阅读,因为它具有更多定义其行为的函数。我的首选是小型单一职责类,因此使用长方法表示该类做得太多。
  • 无法轻易重用。通常情况下,作为代码主体成熟的可重用性很有用。如果您的步骤采用私有方法,则必须先以某种方式提取它们,然后才能在其他任何地方重用它们。此外,当需要在其他位置执行某个步骤时,它可能会鼓励复制粘贴。
  • 以这种方式进行拆分可能是任意的。我会辩称,拆分长的公共方法并不会像将职责分解为类那样花很多时间去思考或设计。每个类都必须使用适当的名称,文档和测试来证明其合理性,而私有方法并不需要太多考虑。
  • 隐藏问题。因此,您已决定将公共方法拆分为小型私有方法。现在没有问题!您可以通过添加越来越多的私有方法来不断添加越来越多的步骤!相反,我认为这是一个主要问题。它建立了一种增加类复杂性的模式,随后将进行后续的错误修复和功能实现。不久,你的私有方法会不断地他们将不得不分开。

但我担心强迫任何阅读该方法的人跳到不同的私有方法会损害可读性

这是我最近与一位同事争论的话题。他认为,将模块的整个行为包含在相同的文件/方法中可以提高可读性。我同意这些代码在一起时更易于遵循,但是随着复杂性的增加,代码也就不那么容易推理了。随着系统的发展,对整个模块作为一个整体进行推理变得很困难。当您将复杂的逻辑分解为几个类,每个类具有单个职责时,则对每个部分进行推理就变得容易得多。


1
我将不同意。过多或过早的抽象会导致不必要的复杂代码。私有方法可能是可能需要接口和抽象的路径的第一步,但是您的方法建议在可能永远不需要访问的地方徘徊。
WillC

1
我知道您来自哪里,但是我认为代码闻起来就像一个OP询问的那样,这表明它已经准备好进行更好​​的设计。在您遇到该问题之前,过早的抽象将在设计这些接口。
塞缪尔

我同意这种方法。实际上,当您遵循TDD时,这是自然而然的过程。另一个人说这是过早的抽象,但是我发现在一个单独的类中(在接口的后面)快速模拟所需的功能要比在私有方法中实际实现以便通过测试容易得多。 。
Eternal21
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.