可读性与可维护性,编写嵌套函数调用的特殊情况


57

我的嵌套函数调用的编码样式如下:

var result_h1 = H1(b1);
var result_h2 = H2(b2);
var result_g1 = G1(result_h1, result_h2);
var result_g2 = G2(c1);
var a = F(result_g1, result_g2);

我最近转到了一个部门,该部门非常使用以下编码样式:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

我的编码方式的结果是,在功能崩溃的情况下,Visual Studio可以打开相应的转储并指出发生问题的行(我特别担心访问冲突)。

我担心,如果因第一种方式编程的相同问题而导致崩溃,我将无法知道是哪个函数导致了崩溃。

另一方面,行上进行的处理越多,一页上得到的逻辑就越多,从而增强了可读性。

我的恐惧是正确的还是我缺少什么?总的来说,在商业环境中这是首选吗?可读性还是可维护性?

我不知道它是否相关,但是我们正在使用C ++(STL)/ C#。


17
@gnat:您提到的是一个一般性问题,尽管我对提到的嵌套函数调用情况以及崩溃转储分析的结果特别感兴趣,但是感谢您的链接,它包含了一些有趣的信息。
多米尼克'18

9
请注意,如果将此示例应用于C ++(如您在项目中使用的那样),那么这不仅是样式问题,因为and 调用的求值顺序可能在单行中发生变化,因为函数参数的求值顺序不确定。如果您出于某种原因依赖于调用中的副作用(明知或不知不觉)的顺序,则这种“样式重构”最终可能不仅会影响可读性/可维护性。HXGX
dfri

4
变量名result_g1是您实际使用的名称,还是该值实际上代表具有合理名称的名称?例如percentageIncreasePerSecond。这实际上是我在两者之间做出决定的考验
理查德·廷格

3
无论您对编码风格有什么看法,都应该遵循已经存在的约定,除非约定很明显不对(在这种情况下似乎不对)。
n00b

4
@ t3chb0t您可以随意随意投票,但是请注意,为了鼓励在本站点上鼓励讨论有用,有用的主题性问题(并阻止不良问题),赞成或反对投票的目的是表示问题是否有用且清晰,因此出于其他原因进行投票,例如使用投票作为对某些示例代码的批评,这些代码有助于发布问题,这通常不利于维护网站的质量: softwareengineering.stackexchange.com/help/privileges/vote-down
Ben Cottrell,

Answers:


111

如果您不得不像

 a = F(G1(H1(b1), H2(b2)), G2(c1));

我不会怪你 这不仅难以阅读,而且难以调试。

为什么?

  1. 浓密的
  2. 一些调试器只会一次突出显示整个内容
  3. 它没有描述性名称

如果以中间结果扩展它,您将得到

 var result_h1 = H1(b1);
 var result_h2 = H2(b2);
 var result_g1 = G1(result_h1, result_h2);
 var result_g2 = G2(c1);
 var a = F(result_g1, result_g2);

而且仍然很难阅读。为什么?它解决了两个问题,并介绍了第四个问题:

  1. 浓密的
  2. 一些调试器只会一次突出显示整个内容
  3. 它没有描述性名称
  4. 杂乱无章的名字

如果您使用添加了新的,良好的语义含义的名称进行扩展,那就更好了!一个好名字可以帮助我理解。

 var temperature = H1(b1);
 var humidity = H2(b2);
 var precipitation = G1(temperature, humidity);
 var dewPoint = G2(c1);
 var forecast = F(precipitation, dewPoint);

现在至少这可以讲一个故事。它可以解决问题,并且明显比这里提供的任何其他工具都要好,但它要求您提供名称。

如果您使用无意义的名称(例如result_this和),并且result_that因为您根本想不出好名而已,那么我真的希望您为我们省去无意义的名称,并使用一些旧的空白将其扩展:

int a = 
    F(
        G1(
            H1(b1), 
            H2(b2)
        ), 
        G2(c1)
    )
;

它与具有无意义的结果名称的可读性一样好(如果不是更多的话)(不是这些函数名那么好)。

  1. 浓密的
  2. 一些调试器只会一次突出显示整个内容
  3. 它没有描述性名称
  4. 杂乱无章的名字

当您想不出好名声时,那就好了。

由于某些原因,调试器喜欢换行,因此您应该发现调试起来并不困难:

在此处输入图片说明

如果这还不够,请想象G2()在一个以上的地方被调用,然后发生这种情况:

Exception in thread "main" java.lang.NullPointerException
    at composition.Example.G2(Example.java:34)
    at composition.Example.main(Example.java:18)

我认为这很不错,因为每个G2()调用都在自己的线路上,所以这种风格可以将您直接带到main中令人讨厌的调用。

因此,请不要以问题1和2为借口使我们陷入问题4。请在想到时使用好名字。尽量避免使用无意义的名称。

Orbit 评论中的 Lightness Races 正确地指出,这些功能是虚假的,并且本身具有不良的恶名。因此,下面是将这种样式应用于野外代码的示例:

var user = db.t_ST_User.Where(_user => string.Compare(domain,  
_user.domainName.Trim(), StringComparison.OrdinalIgnoreCase) == 0)
.Where(_user => string.Compare(samAccountName, _user.samAccountName.Trim(), 
StringComparison.OrdinalIgnoreCase) == 0).Where(_user => _user.deleted == false)
.FirstOrDefault();

即使在不需要自动换行的情况下,我也讨厌看到那种噪音。这种样式下的外观如下:

var user = db
    .t_ST_User
    .Where(
        _user => string.Compare(
            domain, 
            _user.domainName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(
        _user => string.Compare(
            samAccountName, 
            _user.samAccountName.Trim(), 
            StringComparison.OrdinalIgnoreCase
        ) == 0
    )
    .Where(_user => _user.deleted == false)
    .FirstOrDefault()
;

如您所见,我发现这种样式与移入面向对象空间的功能代码很好地结合在一起。如果您能拿出好名声以中间风格做到这一点,那么您将获得更多的力量。在那之前,我一直在使用它。但无论如何,请找到某种方法来避免无意义的结果名称。他们使我的眼睛受伤。


20
@Steve,我不是告诉你不要。我求一个有意义的名字。经常,我经常看到中间样式是无意识的。坏名字比每行稀疏代码消耗的精力更多。我不允许宽度或长度的考虑促使我使代码密集或名字简短。我让他们激励我分解更多。如果不会有好名声,请考虑解决此问题,避免产生无意义的噪音。
candied_orange

6
我添加到您的帖子中:我有一条经验法则:如果您无法命名,则可能表明它定义不明确。 我在实体,属性,变量,模块,菜单,助手类,方法等上使用它。在许多情况下,此小规则显示了设计中的严重缺陷。因此,从某种意义上来说,良好的命名不仅有助于提高可读性和可维护性,还可以帮助您验证设计。当然,每个简单规则都有例外。
Alireza

4
扩展版本看起来很丑。那里有太多 空白,这会降低它的有效性,因为用它调整的所有事物都意味着没有提示。
Mateen Ulhaq

5
@MateenUlhaq唯一的额外空白是几个换行符和一些缩进,并且所有这些空格都小心地放置在有意义的边界处。相反,您的评论会将空格置于无意义的边界。我建议您稍微近一点,更开放一些。
jpmc26

3
与@MateenUlhaq不同,在这个特定示例中,我在空白处使用这样的函数名称,但是使用真实的函数名称(长度超过两个字符,对吗?),这可能就是我想要的。
Lightness Races'Orbit Race's Orbit

50

另一方面,行上进行的处理越多,一页上得到的逻辑就越多,从而增强了可读性。

我完全不同意这一点。仅查看您的两个代码示例就可以将其称为不正确:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

听说阅读。“可读性”并不意味着信息密度;它的意思是“易于阅读,理解和维护”。

有时,代码很简单,使用一行就有意义。在其他时候,这样做只会增加阅读难度,除了将更多内容挤在一行上没有明显的好处。

但是,我也会呼吁您声称“易于诊断崩溃”意味着代码易于维护。不会崩溃的代码更容易维护。“易于维护”主要是通过易于阅读和理解的代码以及一组良好的自动化测试来实现的。

因此,如果只是因为代码经常崩溃并且需要更好的调试信息而将一个表达式转换为具有多个变量的多行表达式,则请停止这样做,并使代码更健壮。您应该更喜欢编写不需要调试的代码,而不是易于调试的代码。


37
虽然我同意F(G1(H1(b1), H2(b2)), G2(c1))很难理解,但这与挤得太紧无关。(不确定是否要这样说,但是可以这样解释。)在一行中嵌套三个或四个函数可以很容易地理解,特别是当某些函数是简单的infix运算符时。这里是非描述性名称的问题,但是在多行版本中,引入更多非描述性名称的问题更加严重。仅添加样板几乎不会增加可读性。
leftaboutabout

23
@leftaroundabout:对我来说,困难在于G1采用3个参数还是仅采用2个参数还是的G2另一个参数并不明显F。我不得不斜视并计算括号。
Matthieu M.

4
@MatthieuM。这可能是一个问题,尽管如果函数是众所周知的,通常很明显需要多少个参数。具体地说,正如我所说,对于infix函数,很明显它们有两个参数。(此外,大多数语言使用的括号元组语法加剧了这个问题;在一种倾向于Currying的语言中,它会自动F (G1 (H1 b1) (H2 b2)) (G2 c1)
清除

5
就我个人而言,我喜欢更紧凑的形式,只要像我之前的评论中那样围绕它进行样式设置,因为它可以保证较少的状态在精神上保持跟踪- result_h1如果它不存在,则无法重用,并且这四个变量之间的关系是明显。
Izkata

8
我发现,通常易于调试的代码是不需要调试的代码。
罗布K

25

您的第一个示例单分配表格不可读,因为选择的名称完全没有意义。这可能是试图不透露您内部信息的人工产物,我们不能说,在这方面,真正的代码可能很好。无论如何,它由于极低的信息密度而it绕,这通常不容易理解。

您的第二个例子简直荒唐可笑。如果这些函数具有有用的名称,那么可能会很好并且可读性很好,因为其中的名称太多了,但是按原样,它会在另一个方向上造成混淆。

引入有意义的名称后,您可能会发现其中一种形式看起来是否自然,或者是否有黄金中间可以拍摄。

既然您具有可读的代码,大多数错误将显而易见,而其他错误至少很难隐藏。


17

和往常一样,在可读性方面,失败是极端的。您可以采纳任何良好的编程建议,将其转变为宗教法规,然后使用它来生成完全不可读的代码。(如果您不相信我,请查看这两个IOCCC冠军borsanyigoren,看看他们使用函数使代码完全不可读的区别。提示:Borsanyi仅使用一个函数,goren很多,更多...)

在您的情况下,两种极端情况是:1)仅使用单个表达式语句,以及2)将所有内容连接到大型,简洁和复杂的语句中。两种方法都采用极端方法会使您的代码不可读。

作为程序员,您的任务是保持平衡。对于您编写的每条语句,您的任务是回答以下问题:“此语句易于掌握,是否有助于使我的函数易读?”


关键是,没有任何可衡量的语句复杂性可以决定在单个语句中包含什么是好的。以以下行为例:

double d = sqrt(square(x1 - x0) + square(y1 - y0));

这是一个非常复杂的语句,但是任何值得他们投入工作的程序员都应该能够立即了解它的作用。这是一个众所周知的模式。因此,它比同等语言更具可读性

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

这将众所周知的模式分解为看似毫无意义的简单步骤。但是,您的问题中的陈述

var a = F(G1(H1(b1), H2(b2)), G2(c1));

即使比距离计算少一个操作,对我来说似乎也过于复杂。当然,这是我不知道任何事情的直接后果F()G1()G2()H1(),或H2()。如果我对它们了解更多,我可能会做出不同的决定。但这正是问题所在:语句的建议复杂度在很大程度上取决于上下文和所涉及的操作。而且,作为程序员,您是一个必须了解这种情况并决定在单个语句中包括哪些内容的人。如果您关心可读性,则不能将这一责任转移到某些静态规则上。


14

@Dominique,我认为在您的问题分析中,您犯了一个错误,即“可读性”和“可维护性”是两个不同的东西。

是否有可维护但不可读的代码?相反,如果代码具有极强的可读性,为什么由于可读性而变得难以维护?我从来没有听说过任何程序员会互相抵消这些因素,而不得不选择其中一个!

在决定是否对嵌套函数调用使用中间变量的情况下,在给定3个变量,对5个独立函数的调用以及某些嵌套3个深度的调用的情况下,我倾向于至少使用一些中间变量来分解嵌套变量,正如您所做的。

但是我当然不会说绝对不能嵌套函数调用。在这种情况下,这是一个判断问题。

我要说的是以下几点:

  1. 如果被调用的函数表示标准的数学运算,则它们比表示某些晦涩领域逻辑的函数更易于嵌套,这些函数的结果是不可预测的,读者不一定会对其进行脑力评估。

  2. 与具有多个参数的函数相比,具有单个参数的函数更有能力参与嵌套(作为内部函数或外部函数)。在不同的嵌套级别混合不同的工具的功能易于使代码看起来像猪的耳朵。

  3. 程序员习惯于以特定方式表示的功能嵌套-也许是因为它代表了具有标准实现方式的标准数学技术或方程式-可能更难以阅读和验证是否分解为中间变量。

  4. 函数调用一个小巢,执行简单的功能,而且已经很清楚阅读,然后过度分解和雾化,能够被不止一个,这不是打破在所有阅读更加困难。


3
+1表示“是否可能具有可维护但不可读的代码?”。那也是我的第一个想法。
罗恩·约翰

4

两者都是次优的。考虑评论。

// Calculating torque according to Newton/Dominique, 4th ed. pg 235
var a = F(G1(H1(b1), H2(b2)), G2(c1));

或特定功能而非一般功能:

var a = Torque_NewtonDominique(b1,b2,c1);

在决定要说明的结果时,请牢记每个语句的成本(复制与参考,左值与右值),可读性和风险。

例如,将简单的单位/类型转换移至其自己的行没有任何附加价值,因为它们易于阅读且极不可能失败:

var radians = ExtractAngle(c1.Normalize())
var a = Torque(b1.ToNewton(),b2.ToMeters(),radians);

关于您分析崩溃转储的担心,输入验证通常更为重要-实际的崩溃很可能发生在这些函数中,而不是在调用它们的行中发生,即使没有,通常也不需要确切地告诉您在哪里事情炸了。知道事情在哪里开始崩溃比知道它们最终在哪里爆炸更重要,这就是输入验证的目的。


重新传递arg的代价:有两个优化规则。1)不要。2)(仅适用于专家)尚未
RubberDuck

1

可读性是可维护性的主要部分。怀疑我?用一种您不知道的语言(最好是编程语言和程序员的语言)选择一个大型项目,然后看看您将如何重构它...

我将可读性放在可维护性的80到90之间。其余的10%到20%是重构的程度。

也就是说,您实际上将2个变量传递给了最终函数(F)。这两个变量是使用其他三个变量创建的。您最好将b1,b2和c1传递给F,如果F已经存在,则创建D来为F进行合成并返回结果。那时,只需给D取一个好名字,而使用哪种样式都无关紧要。

对于不相关的问题,您说页面上的更多逻辑有助于提高可读性。这是不正确的,度量标准不是页面,而是方法,而方法包含的LESS逻辑则更具可读性。

可读表示程序员可以将逻辑(输入,输出和算法)掌握在脑海中。它做的越多,程序员可以理解的就越少。阅读有关循环复杂性的文章。


1
我同意您所说的可读性。但我不同意,裂化的逻辑运算为单独的方法中,一定使得它比裂化它被分成几行更多可读的(这两种技术,其可以,当使用过度,使简单的逻辑的可读性,并且使整个程序更杂乱) -如果您将事情深入到方法中,则最终会模拟汇编语言宏,而看不到它们如何整体集成。同样,在这种单独的方法中,您仍然会面临相同的问题:嵌套调用,或将其破解为中间变量。
史蒂夫

@Steve:我并不是说总是这样做,但是如果您正在考虑使用5行来获取单个值,则很有可能函数会更好。至于多行还是复杂行:如果这是一个名字很好的函数,那么两者都可以很好地工作。
jmoreno

1

无论您使用的是C#还是C ++,只要您使用的是调试版本,可能的解决方案都是包装函数

var a = F(G1(H1(b1), H2(b2)), G2(c1));

您可以编写oneline表达式,并且仍然可以通过查看堆栈跟踪来指出问题所在。

returnType F( params)
{
    returnType RealF( params);
}

当然,如果您在同一行中多次调用同一函数,则无法知道哪个函数,但是仍然可以识别它:

  • 看功能参数
  • 如果参数相同且该函数没有副作用,则两个相同的调用将变为2个相同的调用,依此类推。

这不是灵丹妙药,但还算不错。

更不用说包装功能组甚至可以使代码的可读性更好:

type CallingGBecauseFTheorem( T b1, C b2)
{
     return G1( H1( b1), H2( b2));
}

var a = F( CallingGBecauseFTheorem( b1,b2), G2( c1));

1

我认为,无论使用哪种语言,自记录代码都可更好地实现可维护性和可读性。

上面给出的语句很密集,但是“自我记录”:

double d = sqrt(square(x1 - x0) + square(y1 - y0));

当分成多个阶段(肯定更容易进行测试)时,它将失去上述所有上下文:

double dx = x1 - x0;
double dy = y1 - y0;
double dxSquare = square(dx);
double dySquare = square(dy);
double dSquare = dxSquare + dySquare;
double d = sqrt(dSquare);

显然,使用清楚说明其目的的变量和函数名称非常有用。

甚至“ if”块在自我记录方面也可能好坏。这很不好,因为您不能轻易地强制前两个条件测试第三个条件...都是不相关的:

if (Bill is the boss) && (i == 3) && (the carnival is next weekend)

这具有更多的“集体”意义,并且更容易创建测试条件:

if (iRowCount == 2) || (iRowCount == 50) || (iRowCount > 100)

从自我记录的角度来看,该语句只是一个随机的字符串:

var a = F(G1(H1(b1), H2(b2)), G2(c1));

综上所述,如果函数H1和H2都更改相同的“系统状态变量”而不是统一为单个“ H”功能,则可维护性仍然是一个主要挑战,因为有人最终会更改H1,甚至没有想到查看H2功能并可能会破坏H2。

我认为良好的代码设计非常具有挑战性,因为没有可以系统地检测和执行的严格规则。

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.