干净代码-我应该将文字1更改为常量吗?


14

为了避免出现幻数,我们经常听到我们应该给一个字面值一个有意义的名字。如:

//THIS CODE COMES FROM THE CLEAN CODE BOOK
for (int j = 0; j < 34; j++) {
    s += (t[j] * 4) / 5;
}

-------------------- Change to --------------------

int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j = 0; j < NUMBER_OF_TASKS; j++) {
    int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
    int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
    sum += realTaskWeeks;
}

我有一个这样的虚拟方法:

说明:我想我有一个服务对象列表,默认情况下,我们只花5美元购买食物,但是当我们有一个以上的人时,我们需要购买水和食物,我们必须花更多的钱,也许是6美元。我将更改代码,请关注文字1,即我对此的疑问。

public int getMoneyByPersons(){
    if(persons.size() == 1){ 
        // TODO - return money for one person
    } else {
        // TODO - calculate and return money for people.
    }

}

当我问我的朋友检查我的代码时,一个人说给值1命名会产生更简洁的代码,另一个人说这里不需要常量名,因为该值本身就有意义。

因此,我的问题是我应该为文字值1命名吗?值什么时候是幻数,什么时候不是?如何区分上下文以选择最佳解决方案?


persons来自哪里,它描述了什么?您的代码没有任何注释,因此很难猜测它在做什么。
Hubert Grzeskowiak

7
它没有比1更清晰的信息。不过,我将“ size”更改为“ count”。moneyService本身就是愚蠢的,它应该能够根据收款人来决定退还多少钱。因此,我会让其他人到达getMoney,并让它解决所有例外情况。
马丁·马特

4
您还可以将逻辑表达式从if子句提取到新方法。也许将其称为IsSinglePerson()。这样,您不仅可以提取一个变量,还可以使if子句更具可读性……
selmaohneh


2
也许这种逻辑将更适合moneyService.getMoney()?如果是1个人,您会在某个时刻打电话给getMoney吗?但我会同意这样的普遍观点,即1是明确的。魔术数字是您必须挠头问程序员如何到达该数字的数字。即if(getErrorCode().equals(4095)) ...
Neil

Answers:


23

否。在该示例中,1完全有意义。

但是,如果person.size()为零怎么办?似乎很奇怪,它persons.getMoney()适用于0和2,但不适用于1。


我同意1是有意义的。谢谢,顺便说一句,我更新了我的问题。您可以再次看到它以了解我的想法。
杰克

3
多年来,我多次听到的一句话是,魔术数字是除0和1之外的任何未命名常量。 虽然我可能会想到应命名这些数字的示例,但这是遵循99%的简单规则。时间。
Baldrickk '18

@Baldrickk有时,其他数字文字也不应该隐藏在名称后面。
Deduplicator

@Deduplicator有什么例子吗?
Baldrickk

@Baldrickk 阿蒙。
Deduplicator

18

为什么一段代码包含那个特定的文字值?

  • 这个值在问题域中是否具有特殊含义?
  • 还是该值只是实现细节,而该值是周围代码的直接结果?

如果文字值的含义在上下文中不清楚,那么可以,通过常量或变量为该值命名是有帮助的。以后,当忘记原始上下文时,具有有意义的变量名的代码将更易于维护。请记住,您的代码的读者主要不是编译器(编译器将很乐于使用可怕的代码),而是该代码的未来维护者–如果代码有些解释,他们将不胜感激。

  • 在第一示例中,文字等的含义3445不是从上下文显而易见。相反,其中一些值在您的问题域中具有特殊含义。因此,给他们起个名字很好。

  • 在您的第二个示例中,文字的含义1从上下文中非常清楚。引入名称没有帮助。

实际上,为明显的值引入名称也可能是不好的,因为它隐藏了实际值。

  • 如果命名值被更改或不正确,则这可能会掩盖错误,尤其是在不相关的代码段中重复使用相同变量的情况下。

  • 一段代码对于特定的值可能也可以正常工作,但在一般情况下可能不正确。通过引入不必要的抽象,代码显然不再正确。

“显而易见的”文字没有大小限制,因为这完全取决于上下文。例如,1024在文件大小计算的上下文中,文字可能是完全显而易见的;在31哈希函数的上下文中,文字可能是完全显而易见的;在padding: 0.5emCSS样式表的上下文中,文字可能是完全显而易见的。


8

这段代码有几个问题,可以这样缩短:

public List<Money> getMoneyByPersons() {
    return persons.size() == 1 ?
        moneyService.getMoneyIfHasOnePerson() :
        moneyService.getMoney(); 
}
  1. 不清楚为什么一个人是特例。我认为有一个特定的业务规则可以告诉您,从一个人那里获取金钱与从多个人那里获取金钱根本不同。但是,我必须去看看里面既getMoneyIfHasOnePersongetMoney,希望能理解为什么会有不同的情况。

  2. 名字 getMoneyIfHasOnePerson看起来不正确。从名字上,我希望该方法可以检查是否有一个人,如果是这样,可以从他那里得到钱。否则,什么都不做。从您的代码来看,这不是正在发生的事情(或者您在两次执行条件)。

  3. 是否有任何理由退还 List<Money>而不是一个集合吗?

回到您的问题,因为目前尚不清楚为什么对一个人有特殊待遇,因此应将一个数字替换为一个常数,除非有另一种使规则明确的方法否则。在这里,一个与任何其他幻数没有太大区别。您可能有业务规则告诉您特殊待遇适用于一,二或三人,或仅适用于十二个人以上。

如何区分上下文以选择最佳解决方案?

您可以做使代码更明确的任何事情。

例子1

想象一下下面的代码:

if (sequence.size() == 0) {
    return null;
}

return this.processSequence(sequence);

零是一个神奇的值吗?代码非常清晰:如果序列中没有元素,就不要对其进行处理并返回一个特殊值。但是此代码也可以这样重写:

if (sequence.isEmpty()) {
    return null;
}

return this.processSequence(sequence);

在这里,不再有常量,并且代码更加清晰。

例子2

再看一段代码:

const result = Math.round(input * 1000) / 1000;

不需要太多时间就能了解它在没有round(value, precision)重载的语言(例如JavaScript)中的作用。

现在,如果要引入一个常数,将如何调用它?您可以获得的最接近的术语是Precision。所以:

const precision = 1000;
const result = Math.round(input * precision) / precision;

它会提高可读性吗?有可能。在这里,常量的值是相当有限的,您可能会问自己是否真的需要执行重构。这里的好处是,现在精度只被声明一次,因此,如果精度被更改,您就不会冒犯以下错误的风险:

const result = Math.round(input * 100) / 1000;

在一个位置更改该值,而忘记在另一个位置进行更改。

例子3

从这些示例中,您可能会感到在每种情况下均应将数字替换为常量。这不是真的。在某些情况下,具有常量不会导致代码改进。

采取以下代码:

class Point
{
    ...
    public void Reset()
    {
        x, y = (0, 0);
    }
}

如果尝试用变量替换零,那么困难将是找到一个有意义的名称。您如何命名?ZeroPositionBaseDefault?在此处引入常量不会以任何方式增强代码。这样会使它更长一些,仅此而已。

但是,这种情况很少见。因此,每当您在代码中找到数字时,请尽一切努力查找如何重构代码。问问自己这个号码是否有业务意义。如果是,则常量是强制性的。如果没有,您如何命名?如果您找到一个有意义的名称,那就太好了。如果不是,您可能会发现不需要常量的情况。


我要添加3a:不要在变量名称,金额,余额等中使用金钱一词。收款没有意义,收款或余额没有意义。(好的,我确实有少量的外国钱(或者说是硬币),但这是另一种非编程问题)。
弯曲

3

您可以使一个函数使用单个参数,并返回乘以四的时间除以五的值,从而在仍使用第一个示例的情况下提供干净的别名。

我只是提供自己的常规策略,但也许我也会学到一些东西。

我在想的是。

// Just an example name  
function normalize_task_value(task) {  
    return (task * 4) / 5;  // or to `return (task * 4) / NUMBER_OF_TASKS` but it really matters what logic you want to convey and there are reasons to many ways to make this logical. A comment would be the greatest improvement

}  

// Is it possible to just use tasks.length or something like that?  
// this NUMBER_OF_TASKS is the only one thats actually tricky to   
// understand right now how it plays its role.  

function normalize_all_task_values(tasks, accumulator) {  
    for (int i = 0; i < NUMBER_OF_TASKS; i++) {  
        accumulator += normalize_task_value(tasks[i]);
    }
}  

抱歉,如果我不满意,但我只是一个JavaScript开发人员。我不确定我用什么组成来证明这一点,我想不是所有的东西都必须放在数组或列表中,但是.length会很有意义。* 4将使善常数保持不变,因为其起源是模糊的。


5
但是,这些值4和5是什么意思呢?如果其中之一需要更改,您是否能够找到在类似上下文中使用数字的所有位置并相应地更新这些位置?这就是原始问题的症结所在。
Bart van Ingen Schenau,

我认为第一个示例是相当自我解释的,因此我可能无法回答。当您需要编写新程序来完成所需的操作时,将来的操作应该不会改变。评论是我认为最有利于此目的的内容。用我的语言减少一线通话。使外行人完全理解它有多重要?
etisdew

@BartvanIngenSchenau整个函数的名称不正确。我希望有一个更好的函数名称,带有规范的注释以及该函数应该返回的内容,以及注释* 4/5实现该目标的原因。常量名称并不是真正需要的。
gnasher729

@ gnasher729:如果这些常数在一个命名充分,文档明确的函数的源代码中恰好出现一次,则可能不需要使用命名的常数。只要常数具有相同的含义多次出现,给该常数命名就可以确保您不必弄清楚文字42的所有这些实例是否都具有相同的含义。
Bart van Ingen Schenau

从本质上讲,如果您期望,可能需要更改值。但是,我将其更改为const来表示变量。我认为情况并非如此,这应该只是位于其上方范围的中间变量。我不想说出所有内容作为函数的参数,但是如果它可以更改,但是很少我将其设为该函数或对象/结构作用域的内部参数,而您正在其中进行更改仅全球范围。
etisdew

2

这个数字1,可以是另一个数字吗?可以是2或3,还是有逻辑原因为什么必须为1?如果必须为1,则使用1即可。否则,您可以定义一个常数。不要将该常数称为1。(我已经看过了)。

一分钟60秒-您需要一个常数吗?好吧,这 60秒,而不是50秒或70秒。每个人都知道。这样可以保留一个数字。

每页可打印60个项目-该数字很容易是59或55或70。实际上,如果更改字体大小,则可能变成55或70。因此,在这里需要一个有意义的常数。

含义是否清楚也有关系。如果您输入“分钟=秒/ 60”,则很清楚。如果您输入“ x = y / 60”,则不清楚。某处必须有一些有意义的名称。

有一个绝对规则:没有绝对规则。通过练习,您将弄清楚何时使用数字以及何时使用命名常量。不要这样做,因为一本书是这样说的-直到您理解为什么这样说。


“一分钟60秒……每个人都知道。因此可以保留一个数字。” -对我来说不是有效的论点。如果我的代码中有200个使用“ 60”的地方怎么办?我如何找到将其用作秒至分钟的地方与其他可能同样“自然”使用的地方?-但是,实际上,我也敢于使用minutes*60,即使hours*3600在需要时也无需声明其他常量。几天以来,我可能会写信,d*24*3600或者d*24*60*60因为86400接近边缘而使人无法一眼就认出这个魔幻数字。
JimmyB

@JimmyB如果您在代码的200个位置上使用“ 60”,则该解决方案很可能是重构函数而不是引入常量。
klutt

0

我已经在数据库检索中看到了大量的代码,例如(修改的)OP中的代码。该查询返回一个列表,但是业务规则说只能有一个元素。然后,当然,有些事情从“仅此一种情况”更改为包含多个项的列表。(是的,我说了很多次。这几乎就像他们……nm)

因此,而不是创建一个常量,我将(使用干净的代码方法)创建一个方法来给出名称或弄清楚条件要检测的内容(并封装其检测方式):

public int getMoneyByPersons(){
  if(isSingleDepartmentHead()){ 
    // TODO - return money for one person
  } else {
    // TODO - calculate and return money for people.
  }
}

public boolean isSingleDepartmentHead() {
   return persons.size() == 1;
}

最好统一处理。无论n是什么,都可以统一对待n个元素的列表。
Deduplicator

我看到不是这样,实际上,单个案例的值(边际值)与如果同一用户属于n大小列表时的情况不同。在做得好的OO中,这可能不是问题,但是,在处理旧系统时,良好的OO实现并不总是可行的。我们得到的最好的结果是在DAO逻辑上有一个合理的面向对象外观。
克里斯蒂安H
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.