很长的方法总是需要重构吗?

时间:2010-08-04 12:01:22

标签: c++ refactoring

我遇到的情况是我们有很多非常长的方法,1000行或更多。

为了给你提供更多详细信息,我们有一个传入的高级命令列表,每个生成会产生更长(有时候很大)的低级命令列表。有一个工厂为每个传入命令创建一个类的实例。每个类都有一个处理方法,其中所有较低级别的命令按顺序添加生成。正如我所说,这些命令序列及其参数经常导致处理方法达到数千行。

有很多重复。许多命令模式在不同命令之间共享,但代码反复重复。这让我觉得重构是一个非常好的主意。

相反,我们的规格与当前代码完全相同。每个传入命令的命令列表很长。当我尝试一些重构时,我开始对规格感到不舒服。我想念规范和代码之间的明显比喻,并浪费时间深入研究新创建的公共类。

然后在这里提出一个问题:一般来说,你认为这么长的方法总是需要重构,或者在类似的情况下它是可以接受的吗? (遗憾的是,重构规范不是一种选择)


编辑: 我删除了每个“生成”的引用,因为它实际上令人困惑。它不是自动生成的代码。

class InCmd001 {

  OutMsg process ( InMsg& inMsg ) {

     OutMsg outMsg = OutMsg::Create();

     OutCmd001 outCmd001 = OutCmd001::Create();
     outCmd001.SetA( param.getA() );
     outCmd001.SetB( inMsg.getB() );

     outMsg.addCmd( outCmd001 );

     OutCmd016 outCmd016 = OutCmd016::Create();
     outCmd016.SetF( param.getF() );

     outMsg.addCmd( outCmd016 );

     OutCmd007 outCmd007 = OutCmd007::Create();
     outCmd007.SetR( inMsg.getR() );

     outMsg.addCmd( outCmd007 );

     // ......

     return outMsg;
  }
}

这里是一个传入命令类的示例(用伪c ++手动编写)

19 个答案:

答案 0 :(得分:38)

代码从不需要重构。代码既可以使用,也可以不运行。如果它工作,代码不需要任何东西。

重构的需要来自程序员 you 。阅读,编写,维护和扩展代码的人。

如果您无法理解代码,则需要对其进行重构。如果通过清理和重构代码来提高效率,则需要对其进行重构。

一般情况下,我会说为了你自己重构1000多行函数是一个好主意。但是你没有这样做,因为代码需要它。您这样做是因为这样可以让您更容易理解代码,测试其正确性并添加新功能。

另一方面,如果代码是由其他工具自动生成的,您将永远不需要阅读或编辑它。那么重构它有什么意义呢?

答案 1 :(得分:11)

我完全理解您的来源,并且可以确切地了解您为什么按原样构建代码,但需要更改。

尝试重构时的不确定性可以通过编写单元测试来改善。如果您针对每个规范进行了特定的测试,那么每个规范的代码都可以重构,直到您面对蓝色,并且您可以对它充满信心。

第二种选择是,是否可以从数据结构中自动生成代码? 如果你有一套完成驴工作和边缘情况的核心课程,你可以随意自动生成重复的1000行方法。

但是,每条规则都有例外情况 如果这些方法是规范的字面解释(非常少的附加逻辑),并且规格不经常改变,并且规范的“公共”部分(即恰好相同的位)在不同时间发生变化,并且你很快就会被要求从代码中获得10倍的性能提升,然后(并且只有这样)。 。 。你可能会对自己拥有的东西感觉更好。

。 。 。但总的来说,重构。

答案 2 :(得分:10)

是的,永远。 1000行比任何函数都要长至少10倍,我很想说100x,除了在处理输入解析和验证时,编写20行左右的函数就很自然了。

编辑:重新阅读您的问题,我不清楚一点 - 你在谈论机器生成的代码,没有人必须触摸?在这种情况下,我会保留原样。

答案 3 :(得分:10)

修复与从头开始编写不同。虽然你永远不应该编写这样的代码,但在重构代码之前,你需要考虑重构的成本,在破坏已经运行的代码方面的相关风险,以及在未来节省的时间方面的净效益。只有当净收益超过相关成本和风险时才会重构。

有时候包装和重写可能是一种更安全,更具成本效益的解决方案,即使乍一看它看起来很昂贵。

答案 4 :(得分:6)

如果人类维持(因此需要理解)长方法需要重构。

答案 5 :(得分:3)

根据经验,首先是人类代码。我不同意功能需要简短的常见想法。我认为你需要瞄准的是当一个人阅读你的代码时,他们会很快找到它。

为此,尽可能简化事情是一个好主意 - 但不仅仅是这样。为每个函数委派大约一个任务是个好主意。关于“大致一个任务”意味着什么没有规则:你必须使用自己的判断。但是要认识到将功能分解为太多其他功能本身会降低可读性。想想第一次读取你的功能的人:他们必须跟随一个接一个的函数调用,不断地进行上下文切换并在他们的脑海中维持一个堆栈。这是机器的任务,而不是人类的任务。

找到平衡点。

在这里,您会看到命名事物的重要性。您将看到为变量和函数选择名称并不容易,这需要时间,但另一方面,它可以在人类读者方面节省很多混淆。再次,找到节省时间和友好人类将跟随你的时间之间的平衡。

至于重复,这是一个坏主意。这是需要修复的东西,就像内存泄漏一样。这是一颗定时炸弹。

正如其他人在我之前所说,改变代码可能很昂贵。你需要考虑是否会花费所有这些时间和精力,面对变革的风险,以获得更好的代码。你现在可能会失去很多时间,让自己一个接一个地头疼,以便以后节省大量的时间和头痛。

答案 6 :(得分:3)

查看相关问题How many lines of code is too many?。在那里的答案中有很多智慧。

重新发布一个引用(虽然我会尝试在这里再多评论一下)......前段时间,我读了这个passage from Ovid's journal

  

我最近写了一些代码   Class :: Sniff会检测到“长   方法“并将它们作为代码报告   闻。我甚至写了一篇博文   我怎么做的(quelle惊讶,嗯?)。   那是Ben Tilly问的时候   令人尴尬的明显问题:如何   我知道长方法是一种代码   闻?

     

我抛弃了通常的理由,   但他不会放松。他要   信息和他引用的优秀   书代码完成为   相反的观点。我得到了我的副本   这本书并开始阅读“如何   应该是一个常规的“(第175页,   第二版)。作者史蒂夫   麦康奈尔认为,惯例应该是   不超过200行。圣   CRUD!这是太久了。如果一个   常规长于约20或30   我觉得现在是打破它的时候了   起来。

     

令人遗憾的是,麦康奈尔脸红了   引用六个单独的研究,全部   发现更长的惯例   不仅没有与更大的相关   缺陷率,但也经常   开发更便宜,更容易   理解。结果,最新的   现在版本的Class :: Sniff在github上   较长例程的文档可能不会   毕竟是代码味道。本是   对。我错了。

(TDD的其余部分也值得一读。)

来自“更短的方法更好”阵营,这给了我很多思考。

以前我的大型方法通常仅限于“我需要在此处内联,并且编译器不合作”,或者“由于某种原因,巨型开关块确实比调度表运行得更快”或“这个东西”只是按顺序调用,我真的不想在这里调用函数调用“。所有相对罕见的案例。

但是,在你的情况下,我对于不接触事物有很大的偏见:重构带来一些固有的风险,而且目前可能超过奖励。 (免责声明:我有点偏执;我通常是最终修复崩溃的人。)

考虑将您的努力花在可以强化现有代码的测试,断言或文档上,并在任何重构尝试之前倾斜风险/奖励等级:invariant checksbound function分析和{{3 }};来自pre/postcondition tests的任何其他有用的概念;也许甚至是另一种语言的并行实现(可能是像Erlang这样的消息会给你一个更好的视角,给出你的代码示例),甚至是你想要遵循的某些DBC规范燃烧的时间。

任何这些努力通常会产生一些结果,即使您没有重构代码:您学到了一些东西,增加了您(和您组织)对代码和规范的理解和使用能力,你可能会发现一些真正需要填补的漏洞现在,你对自己做出改变的能力更有信心,同时减少了造成灾难性后果的可能性。

当您对问题域有了更好的理解时,您可能会发现有不同的方法可以重构您以前没想过的。

这并不是说“你应该有一个全覆盖的测试套件,并且DBC断言,以及正式的逻辑规范”。只是你处于一个典型的不完美状态,并且有点多样化 - 寻找新方法来解决你发现的问题(可维护性?模糊规范?学习系统的简易性?) - 可能会给你一点点前进进步和一些增加的信心,之后你可以采取更大的步骤。

因此,从“太多线是一个问题”的角度思考更少,而更多来自“这可能是代码味道,它会给我们带来什么问题,是否有任何容易和/或有益的事情我们可以做到了吗?“

让它在后座上烹饪一点 - 回来并重新审视它,因为时间和巧合允许(例如“我今天在代码附近工作,也许我会徘徊,看看我是否不能记录假设好一点......“)可能会产生良好的结果。然后再次,在做出明智的决定并决定必须对这种情况做出的事情也是有效的。

我在这里成功了吗?我认为,我的观点是,代码闻起来,模式/反模式,最佳实践等 - 他们在那里为您服务。尝试习惯它们,然后采取对你当前情况有意义的东西,剩下的就剩下了。

答案 7 :(得分:2)

我认为你首先需要“重构”规范。如果规范中有重复,如果它使用了一些“基本构建块”,它也会变得更容易阅读。


编辑:只要你不能重构规范,我就不会改变代码。 编码样式指南都是为了更容易的代码维护而制作的,但在您的特殊情况下,遵循规范可以轻松实现维护。

这里的一些人询问是否生成了代码。在我看来并不重要:如果代码遵循规范“逐行”,那么如果生成或手写代码则没有区别。

答案 8 :(得分:2)

1000万行代码什么都没有。我们的功能长度为6到12,000行。当然,这些功能是如此之大,以至于事情在那里会丢失,而且没有任何工具可以帮助我们查看它们的高级抽象。不幸的是,代码难以理解。 我对那些大的功能的看法是,它们不是由优秀的程序员编写的,而是由不称职的黑客编写的,他们不应该被放在电脑附近 - 但是应该被解雇并留下麦当劳的汉堡。这样的代码通过留下无法添加或改进的功能而肆虐。 (对顾客来说太糟糕了)。代码是如此脆弱,以至于任何人都无法修改它 - 即使是原始作者也是如此。

是的,这些方法应该重构或丢弃。

答案 9 :(得分:1)

在我看来,您已在应用程序中实现了单独的语言 - 您是否考虑过这样做?

答案 10 :(得分:1)

您是否必须阅读或维护生成的代码?

如果是,那么我认为可能会有一些重构。

如果不是,那么更高级别的语言实际上就是您正在使用的语言 - C ++只是通往编译器的中间表示 - 并且可能没有必要进行重构。

答案 11 :(得分:1)

据我所知,建议重构超过100行代码的任何方法。

答案 12 :(得分:1)

我认为在他最常在IDE中查看代码的时代,某些规则可能会略有不同。如果代码不包含可利用的重复,那么每行将引用1,000行,并且以明确的方式共享大量变量,将代码分成100行例程,每个例程都被调用曾经可能没有像格式良好的 1,000行模块那样改进,其中包括#region标签或相当于允许轮廓式观看的等效模块。

我的理念是某些代码布局通常意味着某些事情。在我看来,当一段代码被放入它自己的例程中时,这表明代码可以在多个上下文中使用(例外:回调处理程序等在不支持匿名方法的语言中)。如果代码段#1将对象保留为只能由代码段#2使用的模糊状态,并且代码段#2仅可用于保留在#1创建的状态的数据对象,则缺少某些令人信服的理由将段放在不同的例程中,它们应该出现在同一个例程中。如果一个程序通过一系列晦涩的状态来扩展对象,这些状态可以扩展到数百行代码,那么重新编写代码的 design 可能会将操作细分为更自然的更小的部分。 “前后条件,但没有一些令人信服的理由这样做,我不赞成在不改变设计的情况下拆分代码。

答案 13 :(得分:1)

为了进一步阅读,我强烈建议在Portland Pattern Repository上对这个主题进行长时间,富有洞察力,有趣,有时甚至是痛苦的讨论。

答案 14 :(得分:0)

1000行?当然,他们需要重构。同样不是这样,例如,Checkstyle中的可执行语句的默认最大数量为30,这是众所周知的编码标准检查程序。

答案 15 :(得分:0)

  

接下来是问题:一般来说,做   你认为这么长的方法会   总是需要重构,

如果您一般会问,我们会说

  

或在   类似的情况可以接受吗?   (不幸的是重构规范   不是一个选项)

有时可接受,但非常不寻常,我会给你一对例子: 有一些名为Microchip PIC的8位微控制器只有一个固定的8级堆栈,因此你不能嵌套8个以上的调用,因此必须注意避免“堆栈溢出”,因此在这种特殊情况下有很多小的函数(嵌套)不是最好的方法。 其他示例是在进行代码优化时(在非常低的级别),因此您必须考虑跳转和上下文节省成本。小心使用。

修改

即使在生成的代码中,您也可能需要重构其生成的方式,例如节省内存,节省能源,生成人类可读,美观,谁知道等等。

答案 16 :(得分:0)

我见过不是这种情况的情况(例如,在.Net中创建Excel电子表格通常需要很多代码来形成工作表),但大多数情况下,最好的事情是确实会重构它。

我个人尝试将功能设置得足够小,以便它全部出现在我的屏幕上(当然不会影响可读性)。

答案 17 :(得分:0)

如果你重构,当你重构时,添加一些注释来解释它正在做什么。

如果它有评论,那么重构的候选人就不太可能了,因为对于从头开始的人来说,阅读和关注会更容易。

答案 18 :(得分:0)

一直有很好的一般建议,这里是您的样本的实用建议:

普通模式可以用普通的馈线方法隔离:

void AddSimpleTransform(OutMsg & msg, InMsg const & inMsg, 
                        int rotateBy, int foldBy, int gonkBy = 0)
{
   // create & add up to three messages
}

您甚至可以通过使其成为OutMsg的成员并使用流畅的界面来改进,以便您可以编写

OutMsg msg;
msg.AddSimpleTransform(inMsg, 12, 17)
   .Staple("print")
   .AddArtificialRust(0.02);

这可能是情况下的额外改进。