与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'。
你怎么看?我错过了其他任何优点/缺点吗?答案 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,这个问题尚未在问题或答案中准确确定。暂时忘掉“扼杀字符串”。
数据库有一个参考表,其中包含config_options
。 PK是一个字符串。
有两种类型的PK:
有意义的标识符,用户(和开发人员)查看和使用的标识符。这些PK应该是稳定的,可以依赖它们。
无用的Id
列,用户永远不应该看到,开发人员必须知道,并编写代码。这些都不能依赖。
使用有意义的PK IF CustomerCode = "IBM" ...
或IF CountryCode = "AUS"
等的绝对值来编写代码是正常的。
您的参考表使用有意义的PK。在代码中引用这些文字字符串是不可避免的。隐藏价值将使维护更加困难;代码不再是字面意思;你的同事是对的。另外还有额外的冗余功能可以咀嚼循环。如果字面上有拼写错误,你很快就会在开发测试期间找到它,早在UAT之前。
数百个文字的数百个函数是荒谬的。如果你实现了一个函数,那么规范化你的代码,并提供一个可以用于数百个文字中的任何一个的函数。在这种情况下,我们回到裸体文字,并且可以免除该功能。
重点是,隐藏文字的尝试没有价值 。
它不能被解释为“硬编码”,这是完全不同的东西。我认为这就是你的问题所在,将这些结构标识为“硬编码”。它只是字面上引用了一个有意义的PK。
现在从任何代码段的角度来看,如果你使用相同的值几次,你可以通过捕获变量中的文字字符串来改进代码,然后在其余部分使用变量代码块。当然不是一个功能。但这是一个效率和良好实践问题。即使这样也不会改变效果IF CountryCode = @cc_aus
答案 3 :(得分:5)
根据我的经验,这类问题掩盖了一个更深层次的问题:未能实际实施OOP并遵循DRY原则。
简而言之,在启动时通过针对每个操作内部 if
语句的适当定义捕获决策,然后丢弃config_options
和运行时间测试。
详情如下。
样本用法是:
if (config_options.value('FOO_ENABLED') == 'Y') ...
提出了一个显而易见的问题,“省略号中发生了什么?”,特别是考虑到以下陈述:
(当然,可能需要在系统代码的许多地方检查相同的选项。)
让我们假设这些config_option
值中的每一个确实对应于单个问题域(或实现策略)概念。
而不是这样做(反复地,在整个代码的不同地方):
我建议封装“可配置动作”的概念。
让我们以一个例子(显然就像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)
我认为这里有两个不同的问题:
答案 7 :(得分:1)
如果在整个代码中使用强类型配置类,我也更喜欢它。使用正确命名的方法,您不会失去任何可读性。如果您需要从字符串转换到另一种数据类型(decimal / float / int),则无需重复在多个位置执行转换的代码,并且可以缓存结果,因此转换只会发生一次。你已经掌握了这个基础,所以我认为不习惯新的做事方式。
答案 8 :(得分:1)
要考虑的另一件事是意图。如果您正在进行需要本地化的项目,则硬编码字符串可能不明确。请考虑以下事项:
const string HELLO_WORLD = "Hello world!";
print(HELLO_WORLD);
程序员的意图很明确。使用常量意味着不需要对此字符串进行本地化。现在看看这个例子:
print("Hello world!");
我们不太确定。程序员真的不希望这个字符串被本地化,或者程序员在编写这段代码时是否忘记了本地化?