硬编码的STRINGS是否可以接受?

时间:2009-01-28 12:45:05

标签: language-agnostic literals string-literals hard-coding

Is hard-coding literals ever acceptable?相似,但我在这里特别想到“魔术字符串”。

在一个大型项目中,我们有一个配置选项表,如下所示:

Name         Value
----         -----
FOO_ENABLED  Y
BAR_ENABLED  N
...

(数百人)。

通常的做法是调用泛型函数来测试这样的选项:

if (config_options.value('FOO_ENABLED') == 'Y') ...

(当然,可能需要在系统代码的许多地方检查相同的选项。)

添加新选项时,我正在考虑添加一个隐藏“魔术字符串”的函数,如下所示:

if (config_options.foo_enabled()) ...

然而,同事们认为我已经过火了并反对这样做,更喜欢硬编码因为:

  • 这就是我们通常做的事情
  • 可以更轻松地查看调试代码时发生的事情

麻烦的是,我可以看到他们的观点!实际上,我们永远不会出于任何原因重命名选项,所以我能想到的唯一优势就是编译器会捕获像fo_enabled()这样的拼写错误,而不是'FO_ENABLED'。

你怎么看?我错过了其他任何优点/缺点吗?

9 个答案:

答案 0 :(得分:34)

如果我在代码中使用一次字符串,我通常不会担心在某处使它成为常量。

如果我在代码中使用了两次字符串,我将考虑使其成为常量。

如果我在代码中使用三次字符串,我几乎肯定会使它成为常量。

答案 1 :(得分:9)

if (config_options.isTrue('FOO_ENABLED')) {...
}

将您的硬编码Y支票限制在一个地方,即使这意味着为您的地图编写包装类。

if (config_options.isFooEnabled()) {...
}

在您拥有100个配置选项和100个方法之前,可能看起来没问题(因此,您可以在决定实施之前对未来的应用程序增长和需求做出判断)。否则,最好为参数名称设置一类静态字符串。

if (config_options.isTrue(ConfigKeys.FOO_ENABLED)) {...
}

答案 2 :(得分:6)

我意识到这个问题已经过时了,但是它出现在我的边缘。

AFAIC,这个问题尚未在问题或答案中准确确定。暂时忘掉“扼杀字符串”。

  1. 数据库有一个参考表,其中包含config_options。 PK是一个字符串。

  2. 有两种类型的PK:

    • 有意义的标识符,用户(和开发人员)查看和使用的标识符。这些PK应该是稳定的,可以依赖它们。

    • 无用的Id列,用户永远不应该看到,开发人员必须知道,并编写代码。这些都不能依赖。

  3. 使用有意义的PK IF CustomerCode = "IBM" ...IF CountryCode = "AUS"等的绝对值来编写代码是正常的。

    • 参考无意义PK的绝对值是不可接受的(由于自动增量;间隙被改变;值被批量替换)。
  4. 您的参考表使用有意义的PK。在代码中引用这些文字字符串是不可避免的。隐藏价值将使维护更加困难;代码不再是字面意思;你的同事是对的。另外还有额外的冗余功能可以咀嚼循环。如果字面上有拼写错误,你很快就会在开发测试期间找到它,早在UAT之前。

    • 数百个文字的数百个函数是荒谬的。如果你实现了一个函数,那么规范化你的代码,并提供一个可以用于数百个文字中的任何一个的函数。在这种情况下,我们回到裸体文字,并且可以免除该功能。

    • 重点是,隐藏文字的尝试没有价值 。

  5. 它不能被解释为“硬编码”,这是完全不同的东西。我认为这就是你的问题所在,将这些结构标识为“硬编码”。它只是字面上引用了一个有意义的PK。

  6. 现在从任何代码段的角度来看,如果你使用相同的值几次,你可以通过捕获变量中的文字字符串来改进代码,然后在其余部分使用变量代码块。当然不是一个功能。但这是一个效率和良好实践问题。即使这样也不会改变效果IF CountryCode = @cc_aus

答案 3 :(得分:5)

根据我的经验,这类问题掩盖了一个更深层次的问题:未能实际实施OOP并遵循DRY原则。

简而言之,在启动时通过针对每个操作内部 if语句的适当定义捕获决策,然后丢弃config_options和运行时间测试。

详情如下。

样本用法是:

if (config_options.value('FOO_ENABLED') == 'Y') ...

提出了一个显而易见的问题,“省略号中发生了什么?”,特别是考虑到以下陈述:

  

(当然,可能需要在系统代码的许多地方检查相同的选项。)

让我们假设这些config_option值中的每一个确实对应于单个问题域(或实现策略)概念。

而不是这样做(反复地,在整个代码的不同地方):

  1. 取一个字符串(标签),
  2. 找到相应的其他字符串(值),
  3. 将该值测试为布尔值等效,
  4. 根据该测试,决定是否执行某些操作。
  5. 我建议封装“可配置动作”的概念。

    让我们以一个例子(显然就像FOO_ENABLED ...... ;-)那样,你的代码必须以英制单位或公制单位工作。如果METRIC_ENABLED为“true”,请将用户输入的数据从公制转换为英语以进行内部计算,并在显示结果之前进行转换。

    定义界面:

    public interface MetricConverter {
        double toInches(double length);
        double toCentimeters(double length);
        double toPounds(double weight);
        double toKilograms(double weight);
    }
    

    在一个地方标识与METRIC_ENABLED概念相关的所有行为。

    然后编写这些行为的所有方式的具体实现:

    public class NullConv implements MetricConverter {
        double toInches(double length) {return length;}
        double toCentimeters(double length) {return length;}
        double toPounds(double weight)  {return weight;}
        double toKilograms(double weight)  {return weight;}
    }
    

    // lame implementation, just for illustration!!!!
    public class MetricConv implements MetricConverter {
        public static final double LBS_PER_KG = 2.2D;
        public static final double CM_PER_IN = 2.54D
        double toInches(double length) {return length * CM_PER_IN;}
        double toCentimeters(double length) {return length / CM_PER_IN;}
        double toPounds(double weight)  {return weight * LBS_PER_KG;}
        double toKilograms(double weight)  {return weight / LBS_PER_KG;}
    }
    

    在启动时,不是加载一堆config_options值,而是初始化一组可配置的操作,如:

    MetricConverter converter = (metricOption()) ? new MetricConv() : new NullConv();
    

    (上面的表达式metricOption()是您需要进行的一次性检查的替身,包括查看METRIC_ENABLED的值; - )

    然后,无论代码在哪里说:

    double length = getLengthFromGui();
    if (config_options.value('METRIC_ENABLED') == 'Y') {
        length = length / 2.54D;
    }
    // do some computation to produce result
    // ...
    if (config_options.value('METRIC_ENABLED') == 'Y') {
        result = result * 2.54D;
    }
    displayResultingLengthOnGui(result);
    

    将其重写为:

    double length = converter.toInches(getLengthFromGui());
    // do some computation to produce result
    // ...
    displayResultingLengthOnGui(converter.toCentimeters(result));
    

    由于与该概念相关的所有实施细节现在都已完全打包,因此与METRIC_ENABLED相关的所有未来维护都可以在一个地方完成。此外,运行时权衡是一个胜利;与从Map中获取String值并执行String#equals的开销相比,调用方法的“开销”是微不足道的。

答案 4 :(得分:4)

我相信你提到的两个原因,可能在字符串中拼写错误,直到运行时才能检测到,并且名称更改的可能性(虽然很小)将证明你的想法。

最重要的是你可以获得类型化的函数,现在看起来你只存储了布尔值,如果你需要存储一个int,一个字符串等等。我宁愿使用get_foo()类型,而不是get_string(“FOO” “)或get_int(”FOO“)。

答案 5 :(得分:4)

我真的应该使用常量而不是硬编码的文字。

你可以说他们不会被改变,但你可能永远不会知道。最好养成习惯。使用符号常量。

答案 6 :(得分:3)

我认为这里有两个不同的问题:

  • 在当前项目中,使用硬编码字符串的惯例已经很好地建立,因此所有从事该项目的开发人员都熟悉它。对于已列出的所有原因,它可能是次优的约定,但熟悉代码的每个人都可以查看它并本能地知道代码应该做什么。更改代码,以便在某些部分,它使用“新”功能将使代码稍微难以阅读(因为人们将不得不思考和记住新约定的作用),因此有点难以维护。但我猜想,除非你能快速编写转换脚本,否则将整个项目更改为新约定可能会非常昂贵。
  • new 项目中,由于列出的所有原因,符号常量是IMO的方式。 特别是,因为任何使编译器在编译时捕获错误的东西,否则会在运行时被人捕获,这是一个非常有用的约定。

答案 7 :(得分:1)

如果在整个代码中使用强类型配置类,我也更喜欢它。使用正确命名的方法,您不会失去任何可读性。如果您需要从字符串转换到另一种数据类型(decimal / float / int),则无需重复在多个位置执行转换的代码,并且可以缓存结果,因此转换只会发生一次。你已经掌握了这个基础,所以我认为不习惯新的做事方式。

答案 8 :(得分:1)

要考虑的另一件事是意图。如果您正在进行需要本地化的项目,则硬编码字符串可能不明确。请考虑以下事项:

const string HELLO_WORLD = "Hello world!";
print(HELLO_WORLD);

程序员的意图很明确。使用常量意味着不需要对此字符串进行本地化。现在看看这个例子:

print("Hello world!");

我们不太确定。程序员真的不希望这个字符串被本地化,或者程序员在编写这段代码时是否忘记了本地化?