【问题标题】:Are hard-coded STRINGS ever acceptable?硬编码的字符串是否可以接受?
【发布时间】:2010-10-04 00:03:01
【问题描述】:

类似于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'。

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

【问题讨论】:

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


    【解决方案1】:

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

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

    如果我在代码中使用了 3 次字符串,我几乎肯定会将其设为常量。

    【讨论】:

    • 绝对!我也遵循这种务实的做法。我认为过早创建字符串常量只是 YAGNI 的另一种形式。
    • “骗我一次,可耻……可耻……你。”长时间的、不舒服的停顿。 “骗我——不能再被骗了!” ;)
    • 我有一位同事为他的代码中引用的 EVERY XML 标记的名称创建了一个常量。看到之后,我更喜欢你的方法。
    【解决方案2】:
    if (config_options.isTrue('FOO_ENABLED')) {...
    }
    

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

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

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

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

    【讨论】:

    • 为什么还要使用一类静态字符串?只需使用一类枚举。指针/整数比较比字符串比较便宜。
    【解决方案3】:

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

    你可以说它们不会改变,但你可能永远不会知道。最好让它成为一种习惯。使用符号常量。

    【讨论】:

      【解决方案4】:

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

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

      详情如下。

      示例用法为:

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

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

      (当然,同样的选项可能需要在系统代码的很多地方都勾选上。)

      让我们假设每个config_option 值确实对应于一个问题域(或实施策略)概念。

      不要这样做(在整个代码的不同地方重复):

      1. 取一个字符串(标签),
      2. 找到它对应的其他字符串(值),
      3. 将该值测试为布尔等效项,
      4. 根据该测试,决定是否执行某些操作。

      我建议封装“可配置操作”的概念。

      让我们举个例子(显然就像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 的开销相比,调用方法的“开销”微不足道。

      【讨论】:

        【解决方案5】:

        我意识到这个问题很老了,但它出现在我的边缘。

        AFAIC,无论是在问题中还是在答案中,这里的问题都没有被准确地识别出来。暂时忘掉“编码字符串”吧。

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

        2. 有两种类型的PK:

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

          • 用户不应该看到的毫无意义的Id 列,开发人员必须注意这些列,并编写代码。这些不能依赖。

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

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

          • 数百个字面量的数百个函数是荒谬的。如果您确实实现了一个函数,那么规范化您的代码,并提供一个可用于数百个文字中的任何一个的单个函数。在这种情况下,我们又回到了一个裸字,函数可以省略了。

          • 关键是,隐藏文字的尝试没有任何价值。
            .

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

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

        【讨论】:

          【解决方案6】:

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

          最重要的是你可以得到类型化的函数,现在你似乎只存储布尔值,如果你需要存储一个 int、一个字符串等。我宁愿使用带有类型的 get_foo(),而不是 get_string("FOO ") 或 get_int("FOO")。

          【讨论】:

            【解决方案7】:

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

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

            【讨论】:

              【解决方案8】:

              要考虑的另一件事是意图。如果您在一个需要本地化的项目中,硬编码字符串可能会模棱两可。考虑以下几点:

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

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

              print("Hello world!");
              

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

              【讨论】:

                【解决方案9】:

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-08-09
                  • 1970-01-01
                  • 2013-05-22
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-03-27
                  • 1970-01-01
                  相关资源
                  最近更新 更多