单线与可读性:何时停止减少代码?[关闭]


14

语境

我最近对产生更好的格式化代码感兴趣。更好的意思是“遵循足够多的人认可的规则,将其视为一种良好的做法”(当然,因为永远不会有一种独特的“最佳”编码方式)。

如今,我主要使用Ruby进行编码,因此我开始使用linter(Rubocop)为我提供一些有关代码“质量”的信息(此“质量”由社区驱动的项目ruby-style-guide定义)。

请注意,我将使用“质量”作为“质量格式化的”,与其说是对代码的效率,即使在某些情况下,代码效率实际上由代码是怎么写的影响。

无论如何,做完所有这些事情,我意识到(或至少记得)一些事情:

  • 一些语言(最著名的是Python,Ruby等)允许编写出色的一线代码
  • 遵循一些代码准则,可以使代码大大缩短,但仍然非常清晰
  • 但是,过于严格地遵循这些准则会使代码不太清晰/不易阅读
  • 该代码几乎可以完全遵守某些准则,但质量仍然很差
  • 代码的可读性主要是主观的(如“我发现对开发人员完全不清楚的内容”)

这些只是观察,并非绝对的规则。您还将注意到,此时代码的可读性和遵循的准则似乎无关紧要,但此处的准则是一种缩小重写代码块的方式的方法。

现在,举一些例子,使所有这些变得更加清楚。

例子

我们来看一个简单的用例:我们有一个带有“ User”模型的应用程序。用户具有可选firstnamesurname必填email地址。

我想编写一个方法“ name”,firstname + surname如果至少存在用户firstname或,则将返回该用户的名称(),如果没有,则返回surnameemail后备值。

我还希望此方法采用“ use_email”作为参数(布尔值),从而允许将用户电子邮件用作后备值。此“ use_email”参数应默认(如果未传递)为“ true”。

用Ruby编写最简单的方法是:

def name(use_email = true)
 # If firstname and surname are both blank (empty string or undefined)
 # and we can use the email...
 if (firstname.blank? && surname.blank?) && use_email
  # ... then, return the email
  return email
 else
  # ... else, concatenate the firstname and surname...
  name = "#{firstname} #{surname}"
  # ... and return the result striped from leading and trailing spaces
  return name.strip
 end
end

这段代码是最简单易懂的方式。即使对于不“说” Ruby的人。

现在,让我们尝试将其缩短:

def name(use_email = true)
 # 'if' condition is used as a guard clause instead of a conditional block
 return email if (firstname.blank? && surname.blank?) && use_email
 # Use of 'return' makes 'else' useless anyway
 name = "#{firstname} #{surname}"
 return name.strip
end

它更短,但即使不容易理解,也仍然易于理解(guard子句比条件块更自然阅读)。Guard子句还使其更符合我使用的准则,因此在这里是双赢的。我们还降低了缩进级别。

现在,让我们使用一些Ruby魔术使它更短:

def name(use_email = true)
 return email if (firstname.blank? && surname.blank?) && use_email
 # Ruby can return the last called value, making 'return' useless
 # and we can apply strip directly to our string, no need to store it
 "#{firstname} #{surname}".strip
end

甚至更短,并且完全遵循准则……但是由于缺乏return语句,因此不清楚得多,这对于那些不熟悉这种做法的人来说有点令人困惑。

在这里,我们可以开始提问:真的值得吗?我们应该说“不,使其可读并添加' return'”(知道这将不遵守准则)。还是应该说:“很好,这是Ruby的方法,学习该死的语言!”?

如果我们选择选项B,那么为什么不做得更短:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) || "#{firstname} #{surname}".strip
end

这就是单线!当然,它更短...在这里,我们利用Ruby将返回一个值另一个值取决于定义的一个事实的事实(因为电子邮件将在与以前相同的条件下定义)。

我们也可以这样写:

def name(use_email = true)
 (email if [firstname, surname].all?(&:blank?) && use_email) || "#{firstname} #{surname}".strip
end

简短,不是很难读(我的意思是,我们都已经看到了丑陋的单行代码会是什么样子),好的Ruby,它符合我使用的准则...但是,与第一种编写方式相比它,它不容易阅读和理解。我们也可以说这行太长(超过80个字符)。

一些代码示例可以表明,在“全尺寸”代码及其许多降级版本(低至著名的单线)之间进行选择可能很困难,因为如我们所见,单线可能并不那么可怕,但不过,就可读性而言,没有什么比“完整”代码更好的了……

所以这才是真正的问题:在哪里停下来?什么时候短,足够短?如何知道代码何时变得“太短”且可读性差(请记住,这是很主观的)?甚至更多:当我感到满意时,如何始终进行相应的编码并避免将单行代码与“完整”的代码块混合使用?

TL; DR

这里的主要问题是:当要在“长而清晰,可读和可理解的代码块”和“强大,短而难于阅读/理解的一线代码”之间进行选择时,要知道这两个是最重要的刻度的底部,而不是两个唯一的选择:如何定义“足够清晰”和“不够清晰”之间的边界?

主要问题不是经典的“单线与可读性:哪一种更好?” 但是“如何找到两者之间的平衡?”

编辑1

代码示例中的注释旨在“忽略”,此处用于阐明正在发生的事情,但在评估代码的可读性时不予考虑。


7
答案太短了:保持迭代式重构,直到您不确定它是否比上一次迭代好,然后停止并逆转最后一次重构。
Dom

8
我宁愿变3 return添加关键字。这七个字符使我眼神清晰。
cmaster-恢复莫妮卡

2
如果您感觉真的很恐怖,可以将整个内容写成[firstname,surname,!use_email].all?(&:blank?) ? email : "#{firstname} #{surname}".strip...,因为false.blank?返回true,三元运算符可以为您节省一些字符... \ _(ツ)_ /
¯– DaveMongoose

1
好的,我不得不问:return关键字应该添加什么清晰度?它不提供任何信息。真是混乱。
康拉德·鲁道夫'18

2
简洁变得清晰的观念不仅受制于收益递减规律,而且在推向极端时会逆转。如果要重写以缩短简短功能,那是在浪费时间,而试图证明这种做法是正确的。
sdenham

Answers:


26

无论您编写什么代码,可读性都是最好的。短是第二好的。可读性通常意味着足够简短,因此您可以理解代码,名称明确的标识符,并遵循编写代码所用语言的常见习语。

如果这与语言无关,我认为这绝对是基于意见的,但是在Ruby语言的范围内,我认为我们可以回答。

首先,编写Ruby的功能和惯用方式是return在返回值时忽略关键字,除非从方法中提前返回。

与习惯用法相结合的另一个功能是使用尾随if语句来提高代码的可读性。Ruby的驱动思想之一是编写可读取为自然语言的代码。为此,我们去看_why的Ruby Poignant指南第3章

大声朗读以下内容。

5.times { print "Odelay!" }

在英语句子中,标点符号(例如句号,感叹号,括号)是无声的。标点符号为单词增添了含义,有助于暗示作者句子的意图。因此,让我们将以上内容读为:五次打印“ Odelay!”。

鉴于此,对于Ruby,代码示例#3最常见:

def name(use_email = true)
  return email if firstname.blank? && surname.blank? && use_email

  "#{firstname} #{surname}".strip
end

现在,当我们阅读代码时,它说:

如果名字为空,姓氏为空,请返回电子邮件并使用电子邮件

(返回)去除名字和姓氏

这与实际的Ruby代码非常相似。

它只有两行实际代码,因此非常简洁,并且遵循该语言的习惯用法。


好点。确实,这个问题并不是要以Ruby为中心的,但是我同意在这里不可能有一个与语言无关的答案。
Sudiukil

8
我发现使代码听起来像自然语言的想法被大大高估了(有时甚至有问题)。但是,即使没有这种动机,我也会得出与这个答案相同的结论。
康拉德·鲁道夫'18

1
我会考虑对代码进行另一项调整。也就是说,将它放在use_email其他条件之前,因为它是变量而不是函数调用。但是,字符串插值还是再次淹没了差异。
John Dvorak

按照自然的语言结构来构造代码会使您陷入语言陷阱。例如,当您阅读下一个要求时do send an email if A, B, C but no D,很自然地键入2个if / else块很自然,这可能会更容易编写代码if not D, send an email。在阅读自然语言并将其转换为代码时,请当心,因为它可以使您编写“无聊的故事”的新版本。具有类,方法和变量。毕竟没什么大不了的。
Laiv

@Laiv:让代码像自然语言一样阅读并不意味着从字面上翻译需求。这意味着编写代码,以便在大声读出时允许读者理解逻辑,而无需阅读每一位代码,一个字符一个字符,一个语言构造一个语言。如果编码if !D更好,那么最好使用D一个有意义的名称。而且,如果!操作员在其他代码中迷路了,则最好使用一个称为的标识符NotD
格雷格·伯格哈特

15

我认为您不会比“运用最佳判断”得到更好的答案。总之,你应该力求清晰,而不是急促。通常,最短的代码也是最清晰的,但是如果您只专注于实现简短,那么清晰度可能会受到影响。在后两个示例中显然是这种情况,与前三个示例相比,需要更多的精力来理解。

一个重要的考虑因素是代码的受众。可读性当然完全取决于阅读者。您期望自己(和您自己一起)阅读代码的人是否真的了解Ruby语言的成语?这个问题不是互联网上随机的人可以回答的,这只是您自己的决定。


我确实同意听众的观点,但这是我奋斗的一部分:由于我的软件通常是开源的,因此听众可能由初学者和“ Ruby之神”组成。我可以简单地使它对大多数人可用,但是这有点像在浪费该语言提供的优势。
Sudiukil

1
由于必须接管,扩展和维护一些真正可怕的代码,因此必须赢得清晰度。记住古老的谚语-编写代码,就像维护者是一个复仇的地狱天使一样,他知道您住在哪里,孩子在哪里上学。
uɐɪ

2
@Sudiukil:这很重要。我建议您在那种情况下争取惯用的代码(即,假设您对语言有充分的了解),因为无论如何初学者都不会为开源代码做出贡献。(或者,如果这样做,他们将准备努力学习该语言。)
JacquesB,

7

这里的部分问题是“什么是可读性”。对于我来说,我看一下您的第一个代码示例:

def name(use_email = true)
 # If firstname and surname are both blank (empty string or undefined)
 # and we can use the email...
 if (firstname.blank? && surname.blank?) && use_email
  # ... then, return the email
  return email
 else
  # ... else, concatenate the firstname and surname...
  name = "#{firstname} #{surname}"
  # ... and return the result striped from leading and trailing spaces
  return name.strip
 end
end

而且我发现很难阅读,因为它充满了仅重复代码的“嘈杂”注释。去除它们:

def name(use_email = true)
 if (firstname.blank? && surname.blank?) && use_email
  return email
 else
  name = "#{firstname} #{surname}"
  return name.strip
 end
end

而且现在更具可读性。在阅读它时,我认为“嗯,我想知道Ruby是否支持三元运算符吗?在C#中,我可以将其编写为:

string Name(bool useEmail = true) => 
    firstName.Blank() && surname.Blank() && useEmail 
    ? email 
    : $"{firstname} {surname}".Strip();

红宝石有可能出现这种情况吗?仔细阅读您的文章,我发现有:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) || "#{firstname} #{surname}".strip
end

所有的好东西。但这对我来说是不可读的。只是因为我必须滚动才能看到整条线。因此,让我们修复一下:

def name(use_email = true)
 (email if (firstname.blank? && surname.blank?) && use_email) 
 || "#{firstname} #{surname}".strip
end

现在我很高兴。我不确定语法的工作原理,但是我可以理解代码的作用。

但这就是我。其他人对于什么使代码易于阅读有不同的想法。因此,您在编写代码时需要了解您的听众。如果您是绝对的教学初学者,那么您将希望保持简单,并可能像第一个示例一样编写它。如果您在一组具有多年红宝石经验的专业开发人员中工作,请编写利用该语言并使其简短的代码。如果介于两者之间,则瞄准两者之间的某个位置。

不过,我会说一件事:当心“聪明的代码”,例如在您的上一个示例中。问问自己,[firstname, surname].all?(&:blank?)除了使您变得更聪明以外添加了其他任何东西,即使它现在变得有点难读,因为它可以展示您的技能?我想说这个例子很可能完全属于该类别。如果您正在比较五个值,我会认为它是一个很好的代码。同样,这里没有绝对界限,请注意不要太聪明。

因此,总而言之:可读性要求您了解读者并相应地定位代码,并编写简洁但清晰的代码;永远不要写“聪明”的代码。保持简短,但不要太短。


2
好吧,我忘了提及它,但是这些注释本来是“被忽略的”,它们在这里只是为了帮助那些不太了解Ruby的人。尽管关于听众的观点是正确的,但我并未考虑这一点。至于让您满意的版本:如果重要的是行长,那么我的代码的第三个版本(只有一个return语句的版本)可以做到这一点,甚至更容易理解,不是吗?
Sudiukil

1
@Sudiukil不是红宝石开发人员,但我发现它最难阅读,并且与我从(从另一种语言的角度来看)“最佳”解决方案的要求不符。但是,对于熟悉红宝石是返回最后一个表达式的值的那些语言之一的事实的人来说,它可能代表了最简单,最容易阅读的版本。再次,这都是关于您的受众的。
大卫·阿诺

不是Ruby开发人员,但是这对我来说比最受好评的答案有意义,它的意思是:“这是我要返回的内容(脚注:在特定的长期条件下)。此外,这是一个很晚才出现的字符串去派对。” 基本上只是一个case语句的逻辑应该像一个统一的case语句那样编写,而不是散布在多个看似无关的语句之间。
保罗

就我个人而言,我将与您的第二个代码块一起使用,除了将您的else分支中的两个语句合并为一个:return "#{firstname} #{surname}".strip
Paul,

2

这可能是一个很难不给出基于意见的答案的问题,但这是我的两分钱。

如果发现缩短代码不会影响可读性,甚至不会提高可读性,那就去做吧。如果代码的可读性降低,那么您需要考虑是否有充分的理由将其保留为这种方式。仅仅因为它更短或更酷,或者仅仅因为您可以就这样做,就是不好的原因。您还需要考虑缩短代码是否会使与您合作的其他人难以理解。

那么,这将是一个很好的理由吗?确实,这是一个判断电话,但是示例可能类似于性能优化(当然,不是在性能测试之后)。某些东西会为您带来一些好处,而您愿意为此付出的代价是降低可读性。在这种情况下,您可以通过提供有用的注释(说明代码的作用以及为什么必须使其变得有点神秘)来减轻缺点。更好的是,您可以将该代码提取到具有有意义名称的单独函数中,以便在调用站点上仅一行来解释正在发生的事情(通过函数名),而无需详细说明(但是,人们会有所不同)。对此有意见,因此这是您必须做出的另一项判断)。


1

答案有点主观,但是如果您在一两个月后重新使用该代码,是否能够理解该代码,则必须诚实地问自己。

每次更改都应提高普通人理解代码的能力。为了使代码易于理解,使用以下准则会有所帮助:

  • 尊重语言的成语。C#,Java,Ruby,Python都有他们喜欢做相同事情的方式。惯用语构造有助于理解您不熟悉的代码。
  • 当代码的可读性降低时,请停止。在您提供的示例中,这种情况发生在您遇到最后一对简化代码时。您失去了上一个示例的惯用优势,并引入了许多符号,需要大量的思考才能真正理解正在发生的事情。
  • 仅在您不得不证明某些意外情况时才使用注释。我知道您的示例在那里向不熟悉Ruby的人们解释了结构,这是一个很好的问题。我更喜欢使用注释来解释意外的业务规则,如果代码可以说明问题,则避免使用它们。

就是说,有时候扩展代码有助于更好地理解正在发生的事情。其中一个示例来自C#和LINQ。LINQ是一个很棒的工具,在某些情况下可以提高可读性,但是我也遇到了很多情况,这些情况更加令人困惑。我在同行评审中得到了一些反馈,建议将表达式转换为带有适当if语句的循环,以便其他人可以更好地维护它。当我遵守时,他们是对的。从技术上讲,LINQ对于C#更为惯用,但是在某些情况下,它会降低可理解性,而更冗长的解决方案会改善它。

我这么说就是这样:

改善何时可以使代码更好(更易于理解)

请记住,您或类似的人以后必须维护该代码。下次遇到时,可能要等几个月。帮自己一个忙,不要追求减少行数,而要以能够理解你的代码为代价。


0

可读性是您想要拥有的属性,而拥有多个内衬则不是。因此,而不是“单线vs可读性”,问题应该是:

单线何时会增加可读性,何时会损害可读性?

我相信单行代码在满足以下两个条件时,对于可读性很有用:

  1. 它们过于具体,无法提取到功能。
  2. 您不想中断阅读周围代码的“流程”。

例如,假设name您的方法名称不好。将名字和姓氏结合起来,或者使用电子邮件代替名字,这并非自然而然的事情。因此,name您可能想到的不是最好的方法,而是冗长而繁琐的:

puts "Name: #{user.email_if_there_is_no_name_otherwise_use_firstname_and_surname(use_email)}"

如此长的名称表示该名称非常具体-如果使用更通用的名称,则可以找到一个更通用的名称。因此,将其包装在方法中既不会提高可读性(太长),也不会提高DRYness(太具体了而无法在其他任何地方使用),因此最好将代码保留在其中。

仍然-为什么将它做成单线?它们通常比多行代码可读性差。这是我们应该检查第二个条件的地方-周围代码的流程。如果您有这样的事情怎么办:

puts "Group: #{user.group}"
puts "Title: #{user.title}"
if user.firstname.blank? && user.surname.blank?) && use_email
  name = email
else
  name = "#{firstname} #{surname}"
  name.strip
end
puts "Name: #{name}"
puts "Age: #{user.age}"
puts "Address: #{user.address}"

多行代码本身是可读的-但是当您尝试读取周围的代码(打印各个字段)时,多行构造会中断流程。这更容易阅读:

puts "Group: #{user.group}"
puts "Title: #{user.title}"
puts "Name: #{(email if (user.firstname.blank? && user.surname.blank?) && use_email) || "#{user.firstname} #{user.surname}".strip}"
puts "Age: #{user.age}"
puts "Address: #{user.address}"

您的流程不会被打断,您可以根据需要关注特定的表达式。

这是你的情况吗?当然不!

第一个条件不太重要-您已经认为它足够通用,值得使用一个方法,并为该方法想出一个比其实现更具可读性的名称。显然,您不会再将其提取到函数中。

至于第二种情况-是否会中断周围代码的流程?没有!周围的代码是一个方法声明,name唯一的目的是选择。选择名称的逻辑不打断周围代码的流程-这是周围代码的目的!

结论-不要让整个功能体成为一线人

当您想做一些复杂的事情而又不中断流程时,单线是很好的。函数声明已经在中断流程(因此在您调用该函数时不会被中断),因此将整个函数主体设置为单行代码无助于提高可读性。

注意

我指的是“功能全面”的函数和方法-不是内联函数或lambda表达式,它们通常是周围代码的一部分,需要适应其流程。

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.