3

类似于硬编码文字是否可以接受?,但我在这里特别考虑“魔术字符串”。

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

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

(数百个)。

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

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

(当然,同样的选项可能需要在系统代码中的很多地方进行检查。)

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

if (config_options.foo_enabled()) ...

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

  • 这就是我们通常所做的
  • 它使调试代码时更容易看到发生了什么

麻烦的是,我能看出他们的意思!实际上,我们永远不会出于任何原因重命名选项,所以我能想到的函数的唯一优势是编译器会捕捉到任何像 fo_enabled() 这样的错字,但不会捕捉到“FO_ENABLED”。

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

4

9 回答 9

37

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

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

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

于 2009-01-28T12:54:36.513 回答
9
if (config_options.isTrue('FOO_ENABLED')) {...
}

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

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

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

if (config_options.isTrue(ConfigKeys.FOO_ENABLED)) {...
}
于 2009-01-28T13:19:16.420 回答
5

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

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

于 2009-01-28T12:52:50.980 回答
5

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

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

详情如下。

示例用法是:

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

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

(当然,同样的选项可能需要在系统代码中的很多地方进行检查。)

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

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

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

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

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

定义一个接口:

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 的开销相比,调用方法的“开销”微不足道。

于 2009-01-28T13:44:21.047 回答
5

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

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

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

  2. PK有两种类型:

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

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

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

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

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

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

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

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

于 2011-01-31T03:02:33.243 回答
4

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

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

于 2009-01-28T12:51:05.353 回答
3

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

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

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

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

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

print("Hello world!");

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

于 2013-09-11T04:18:39.477 回答
1

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

于 2009-01-28T13:09:19.840 回答