方法提取与基本假设


27

当我将大方法(或过程或函数)拆分特定于OOP的问题时,但由于我99%的时间都使用OOP语言,这是我最熟悉的术语,所以请分成许多小方法,我经常发现自己对结果不满意。与这些小方法只是大代码中的代码块相比,要推理这些方法变得更加困难,因为当我提取它们时,我失去了很多来自调用者上下文的基础假设。

后来,当我查看这段代码并看到各个方法时,我不立即知道它们从何处调用,而是将它们视为可以从文件中任何位置调用的普通私有方法。例如,想象一下一个初始化方法(构造方法或其他方法)分成一系列小的方法:在方法本身的上下文中,您清楚地知道对象的状态仍然无效,但是在普通的私有方法中,您可能会假设该对象已经初始化并且处于有效状态。

我所看到的唯一解决方案是whereHaskell中的子句,该子句允许您定义仅在“父”函数中使用的小函数。基本上,它看起来像这样:

len x y = sqrt $ (sq x) + (sq y)
    where sq a = a * a

但是我使用的其他语言没有这样的东西-最近的事情是在本地范围内定义lambda,这可能会更加令人困惑。

因此,我的问题是-您是否遇到了这个问题,甚至看到了这个问题吗?如果这样做,通常如何解决,特别是在“主流” OOP语言中,例如Java / C#/ C ++?

编辑重复项:正如其他人所注意到的,已经有讨论拆分方法的问题和只有一线的小问题。我阅读了它们,他们没有讨论可以从调用者的上下文派生出来的基础假设的问题(在上面的示例中,对象已初始化)。这就是我的问题的重点,这就是为什么我的问题与众不同。

更新:如果您关注下面的问题和讨论,您可能会喜欢John Carmack的这篇文章,尤其是:

除了了解正在执行的实际代码外,内联函数还具有无法从其他位置调用该函数的好处。这听起来很荒谬,但是有一点是正确的。随着代码库在多年使用中的增长,将有很多机会采用快捷方式并仅调用仅执行您认为需要完成的工作的函数。可能有一个FullUpdate()函数调用PartialUpdateA()和PartialUpdateB(),但是在某些特定情况下,您可能意识到(或认为)您只需要执行PartialUpdateB(),并且通过避免使用其他方法可以提高效率。工作。大量的错误源于此。大多数错误是由于执行状态不完全符合您的想法而导致的。




@gnat您链接到的问题讨论了是否完全提取函数,而我对此并不质疑。相反,我质疑最理想的方法。
Max Yankov

2
@gnat那里还有其他相关的问题,但是没有一个讨论这个代码可能依赖于仅在调用者上下文中有效的特定假设这一事实。
Max Yankov

1
以我的经验,@ Doval确实可以。当有麻烦的辅助方法挂像你描述的,提取新的凝聚力类负责的这个
蚊蚋

Answers:


29

例如,假设一个初始化方法分为一系列小方法:在方法本身的上下文中,您清楚地知道对象的状态仍然无效,但是在普通的私有方法中,您可能会以假设对象已经被初始化且为处于有效状态。我看到的唯一解决方案是...

您的担心是有根据的。还有另一种解决方案。

退后一步。方法的根本目的是什么?方法只能执行以下两项操作之一:

  • 产生价值
  • 引起效果

或不幸的是,两者都有。我尽量避免同时使用两种方法,但是很多方法都可以。假设产生的效果或产生的值是方法的“结果”。

您注意到方法在“上下文”中调用。那是什么背景?

  • 参数的值
  • 方法之外的程序状态

从本质上讲,您要指出的是:方法结果的正确性取决于调用它的上下文

我们将在方法主体开始之前为该方法产生正确结果所需条件称为前提条件,并将在方法主体返回其后置条件之后将要生成条件称为条件

因此,您实际上要指出的是:当我将代码块提取到其自己的方法中时,我正在丢失有关前置条件和后置条件的上下文信息

解决该问题的方法是在程序中明确前提条件和后置条件。例如,在C#中,您可以使用Debug.Assert或代码合同来表达前提条件和后置条件。

例如:我曾经在一个编译器上工作,该编译器经历了编译的几个“阶段”。首先,对代码进行分类处理,然后对其进行解析,然后对类型进行解析,然后对继承层次结构进行循环检查,依此类推。代码的每一部分都对其上下文非常敏感。例如,问“此类型是否可以转换为该类型?”将是灾难性的。如果尚未知道基本类型的图是非循环的!因此,每一段代码都清楚地记录了其前提条件。我们将assert在检查类型可转换性的方法中已经通过“基本类型acylic”检查,然后对于读者来说,可以清楚地知道该方法可以在何处调用和不可以在何处调用。

当然,好的方法设计可以通过多种方式缓解您发现的问题:

  • 制定对自己的效果或价值有用的方法,但不能同时对两者有用
  • 使方法尽可能“纯净”;“纯”方法产生的值取决于其参数,而没有效果。这些是最简单的推理方法,因为它们所需的“上下文”非常局限。
  • 最小化在程序状态下发生的突变数量;突变是难以解释代码的地方

+1是根据前提条件/后置条件解释问题的答案。
QuestionC

5
我要补充一点,通常有可能(也是一个好主意!)将检查前置条件和后置条件委托给类型系统。如果您有一个使用a string并将其保存到数据库中的函数,则如果忘记清理它,则可能会面临SQL注入的风险。另一方面,如果您的函数采用SanitisedString,而获取的唯一方法SantisiedString是通过调用Sanitise,那么您已经通过构造排除了SQL注入错误。我越来越发现自己在寻找使编译器拒绝不正确代码的方法。
本杰明·霍奇森

+1需要注意的一件事是,将大方法拆分成较小的块是有代价的:除非先决条件和后置条件比原先的条件更宽松,否则它通常没有用,而您最终不得不通过重新执行您本来应该已经完成​​的检查来支付费用。这不是一个完全“免费”的重构过程。
Mehrdad

“那是什么背景?” 只是为了澄清,我主要是指调用此方法的对象的私有状态。我想它已包含在第二类中。
Max Yankov

这是一个很好的,发人深省的答案,谢谢。(当然,并不是说其他​​答案都是不好的)。我暂时不会将问题标记为已回答,因为我真的很喜欢这里的讨论(当答案被标记为已回答时,讨论往往会停止),并且需要时间进行处理和思考。
Max Yankov

13

我经常看到这一点,并同意这是一个问题。通常,我通过创建一个方法对象来解决它:一个新的特殊类,其成员是原始的太大方法中的局部变量。

新类倾向于使用“导出器”或“制表符”之类的名称,并且可以通过较大的上下文传递执行特定任务所需的所有信息。然后,它是自由的定义更小的辅助代码段是在没有被用于任何危险,但制表或导出。


我越想越喜欢这个想法。它可以是公共类或内部类中的私有类。您不必使用只在本地非常关心的类来使名称空间杂乱无章,这是一种标记它们是“构造函数帮助程序”或“解析帮助程序”之类的方法。
Mike

最近,从架构的角度来看,我只是一个理想的选择。我用一个renderer类和一个公共render方法编写了一个软件renderer,它具有很多上下文,可以用来调用其他方法。我打算为此创建一个单独的RenderContext类,但是,每帧分配和取消分配该项目似乎非常浪费。github.com/golergka/tinyrenderer/blob/master/src/renderer.h
Max Yankov

6

许多语言使您可以嵌套Haskell之类的函数。在这方面,Java / C#/ C ++实际上是相对离群的。不幸的是,他们是如此受欢迎,人们都认为,“这是一个坏主意,否则我最喜欢的‘主流’的语言将允许它。”

Java / C#/ C ++基本上认为,类应该是您需要的唯一方法组。如果您有太多无法确定其上下文的方法,则有两种通用方法:按上下文排序或按上下文拆分。

按上下文排序是“ 清洁代码”中的一项建议,作者在其中描述了“ TO段落”的模式。基本上,这是将您的辅助函数放在调用它们的函数之后,因此您可以像阅读报纸文章中的段落一样阅读它们,获得更多的详细信息。我认为他在视频中甚至缩进了。

另一种方法是拆分班级。这不可能走太远,因为在调用对象上的任何方法之前都需要实例化对象,这很烦人,并且还存在固有的问题,即确定几个小类中的哪个应该拥有每个数据。但是,如果您已经确定了几种真正只适合于一种情况的方法,那么它们可能是考虑放入自己的类中的一个不错的选择。例如,复杂的初始化可以以诸如builder的创建模式来完成。


嵌套函数...那不是lambda函数在C#(和Java 8)中实现的功能吗?
Arturo TorresSánchez15年

我在想的更像是使用名称定义的闭包,例如这些python示例。Lambda不是执行此类操作的最清晰方法。它们更适用于像过滤谓词这样的简短表达式。
Karl Bielefeldt

这些Python示例在C#中当然是可能的。例如,阶乘。它们可能更冗长,但是100%可能。
Arturo TorresSánchez2015年

2
没有人说这是不可能的。OP甚至在他的问题中提到使用lambda。只是如果您出于可读性考虑而提取方法,那么它更具可读性就很好。
Karl Bielefeldt

您的第一段似乎暗示这是不可能的,尤其是引用您的话:“这一定是一个坏主意,否则我最喜欢的'主流'语言将允许这样做。”
Arturo TorresSánchez15年

4

我认为大多数情况下的答案是上下文。作为开发人员编写代码,您应该假设自己的代码将来会被更改。一个类可以与另一个类集成在一起,可以替换其内部算法,也可以分为多个类以创建抽象。这些是新手开发人员通常不考虑的事情,导致需要杂乱的解决方法或稍后进行彻底的大修。

提取方法是好的,但在一定程度上。在检查代码或编写代码之前,我总是尝试问自己以下问题:

  • 此代码仅由此类/功能使用吗?将来会保持不变吗?
  • 如果我需要切换一些具体的实现,可以轻松做到吗?
  • 我团队中的其他开发人员能否理解此功能的作用?
  • 这个类的其他地方是否使用了相同的代码?您应该在几乎所有情况下都避免重复。

无论如何,请始终考虑单一责任。一类应该承担一个责任,其功能应该为一项不变的服务提供服务,并且如果它们执行许多动作,那么这些动作应该具有自己的功能,因此以后区分或更改它们很容易。


1

与这些小方法只是大代码中的代码块相比,要推理这些方法变得更加困难,因为当我提取它们时,我失去了很多来自调用者上下文的基础假设。

直到我采用ECS来鼓励更大,循环的系统功能(系统是唯一具有功能的系统)并且依赖流向原始数据(而不是抽象)的时候,我才意识到这有多大的问题。

与过去使用的代码库相比,令我惊讶的是,生成的代码库比以前工作时要容易得多,因为在调试过程中,您不得不跟踪各种微小的小函数,通常通过抽象函数调用纯接口,导致谁知道直到您跟踪到哪里,才产生一些级联的事件,这些事件导致您从未想到过的代码应该到达的地方。

与约翰·卡马克(John Carmack)不同,我对那些代码库的最大问题不是性能,因为我从来没有对AAA游戏引擎的超高延迟要求,而且我们大多数性能问题都与吞吐量有关。当然,当您在越来越少的函数和类的范围越来越狭窄的情况下工作时,如果没有这种结构的阻碍,您也可能开始变得越来越难以优化热点(要求您将所有这些小片段融合在一起)在更大的范围内,甚至无法开始有效地解决它)。

对我来说,最大的问题是尽管通过了所有测试,但仍无法自信地推断出系统的整体正确性。太多的事情要引起我的理解,因为那种类型的系统不允许您在不考虑所有这些微小细节以及微小功能与无处不在的无处不在的交互作用下进行推理。有太多的“假设”吗?需要在正确的时间调用太多的事情,如果在错误的时间调用它们会发生什么的太多问题(当您开始时,这些问题会引发妄想症)让一个事件触发另一个事件触发另一个事件,从而导致您到达各种不可预测的地方),等等。

现在,我喜欢我的大屁股80线功能在这里和那里,只要它们仍然执行单个明确的职责并且没有像8个级别的嵌套块一样。它们导致一种感觉,即即使这些较大的函数的较小的,切成小块的版本只是私有实现细节,其他任何人都无法调用,但是系统中需要测试和理解的东西更少了……感觉好像整个系统中的交互较少。我什至喜欢一些非常适度的代码重复,只要它不是复杂的逻辑(比如说只有2-3行代码),如果它意味着更少的功能。我喜欢Carmack关于内联的推理,使该功能无法在源文件中的其他位置调用。那里'

如果该选项介于一个功能强大的函数与12个超级简单的函数之间,并且通过复杂的依赖关系相互调用,那么简单性并不会总在总体上降低复杂性。归根结底,您通常必须推理出某个功能之外的情况,必须推理出这些功能加起来最终会做什么,如果您必须从功能推论中得出较大的图景,则可能会更困难。最小的拼图。

当然,经过良好测试的非常通用的库类型代码可以免除该规则,因为此类通用代码通常可以正常运行并独立运行。与代码相比,它离您的应用程序领域更近(几千行代码,而不是数百万行),因此它往往显得有些笨拙,并且应用广泛,因此开始成为日常词汇表的一部分。但是对于某些特定于您的应用程序的东西,您必须维护的系统范围内的不变式远远超出了单个函数或类,因此我倾向于发现无论出于何种原因,拥有更强大的函数都是有帮助的。我发现处理较大的拼图碎片要想弄清楚大图情况要容易得多。


0

我认为这不是问题,但我同意这很麻烦。通常,我只是将帮助程序放置在受益人之后,并添加“ Helper”后缀。加上private访问说明符应使其作用明确。如果在调用辅助程序时不存在某些不变量,则在辅助程序中添加一条注释。

不幸的是,该解决方案的缺点是没有捕获其帮助功能的范围。理想情况下,您的函数很小,因此希望不会导致参数过多。通常,您可以通过定义新的结构或类来捆绑参数来解决此问题,但是所需的样板文件的数量很可能会长于帮助程序本身,然后回到没有明显关联方式的起点具有功能的结构。

您已经提到了另一种解决方案-在主函数中定义助手。在某些语言中,这可能是一种不太常见的习惯用法,但我认为这不会造成混淆(除非您的同伴通常对lambda感到困惑)。不过,这仅在您可以轻松定义函数或类似函数的对象时才有效。例如,我不会在Java 7中尝试这种方法,因为匿名类甚至需要为最小的“函数”引入2级嵌套。尽可能接近letor where子句;您可以在定义之前引用局部变量,并且不能在该范围之外使用帮助器。

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.