硬编码的字符串是否可以接受?

发布于 2024-07-13 00:56:50 字数 785 浏览 18 评论 0原文

类似于 硬编码文字是否可以接受?,但我特别这里想到“魔法弦”。

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

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

数百个)。

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

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

当然,系统代码中的许多地方可能需要检查同一个选项。)

当添加新选项时,我正在考虑添加一个函数像这样隐藏“魔术字符串”:

if (config_options.foo_enabled()) ...

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

  • 这就是我们通常做的事情
  • 时更容易看到发生了什么

它可以让调试代码 麻烦的是,我能明白他们的观点! 实际上,我们永远不会出于任何原因重命名选项,因此我能想到的函数的唯一优点是编译器会捕获任何拼写错误,例如 fo_enabled(),但不会捕获“FO_ENABLED”。

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

Similar to Is hard-coding literals ever acceptable?, but I'm specifically thinking of "magic strings" here.

On a large project, we have a table of configuration options like these:

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

(Hundreds of them).

The common practice is to call a generic function to test an option like this:

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

(Of course, this same option may need to be checked in many places in the system code.)

When adding a new option, I was considering adding a function to hide the "magic string" like this:

if (config_options.foo_enabled()) ...

However, colleagues thought I'd gone overboard and objected to doing this, preferring the hard-coding because:

  • That's what we normally do
  • It makes it easier to see what's going on when debugging the code

The trouble is, I can see their point! Realistically, we are never going to rename the options for any reason, so about the only advantage I can think of for my function is that the compiler would catch any typo like fo_enabled(), but not 'FO_ENABLED'.

What do you think? Have I missed any other advantages/disadvantages?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

只有一腔孤勇 2024-07-20 00:56:50

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

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

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

If I use a string once in the code, I don't generally worry about making it a constant somewhere.

If I use a string twice in the code, I'll consider making it a constant.

If I use a string three times in the code, I'll almost certainly make it a constant.

我恋#小黄人 2024-07-20 00:56:50
if (config_options.isTrue('FOO_ENABLED')) {...
}

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

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

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

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

Restrict your hard coded Y check to one place, even if it means writing a wrapper class for your Map.

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

Might seem okay until you have 100 configuration options and 100 methods (so here you can make a judgement about future application growth and needs before deciding on your implementation). Otherwise it is better to have a class of static strings for parameter names.

if (config_options.isTrue(ConfigKeys.FOO_ENABLED)) {...
}
病女 2024-07-20 00:56:50

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

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

I realise the question is old, but it came up on my margin.

AFAIC, the issue here has not been identified accurately, either in the question, or the answers. Forget about 'harcoding strings" or not, for a moment.

  1. The database has a Reference table, containing config_options. The PK is a string.

  2. There are two types of PKs:

    • Meaningful Identifiers, that the users (and developers) see and use. These PKs are supposed to be stable, they can be relied upon.

    • Meaningless Id columns which the users should never see, that the developers have to be aware of, and code around. These cannot be relied upon.

  3. It is ordinary, normal, to write code using the absolute value of a meaningful PK IF CustomerCode = "IBM" ... or IF CountryCode = "AUS" etc.

    • referencing the absolute value of a meaningless PK is not acceptable (due to auto-increment; gaps being changed; values being replaced wholesale).
      .
  4. Your reference table uses meaningful PKs. Referencing those literal strings in code is unavoidable. Hiding the value will make maintenance more difficult; the code is no longer literal; your colleagues are right. Plus there is the additional redundant function that chews cycles. If there is a typo in the literal, you will soon find that out during Dev testing, long before UAT.

    • hundreds of functions for hundreds of literals is absurd. If you do implement a function, then Normalise your code, and provide a single function that can be used for any of the hundreds of literals. In which case, we are back to a naked literal, and the function can be dispensed with.

    • the point is, the attempt to hide the literal has no value.
      .

  5. It cannot be construed as "hardcoding", that is something quite different. I think that is where your issue is, identifying these constructs as "hardcoded". It is just referencing a Meaningfull PK literally.

  6. Now from the perspective of any code segment only, if you use the same value a few times, you can improve the code by capturing the literal string in a variable, and then using the variable in the rest of the code block. Certainly not a function. But that is an efficiency and good practice issue. Even that does not change the effect IF CountryCode = @cc_aus

緦唸λ蓇 2024-07-20 00:56:50

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

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

I really should use constants and no hard coded literals.

You can say they won't be changed, but you may never know. And it is best to make it a habit. To use symbolic constants.

樱花坊 2024-07-20 00:56:50

根据我的经验,此类问题掩盖了更深层次的问题:未能进行实际的 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 的开销相比,调用方法的“开销”是微不足道的。

In my experience, this kind of issue is masking a deeper problem: failure to do actual OOP and to follow the DRY principle.

In a nutshell, capture the decision at startup time by an appropriate definition for each action inside the if statements, and then throw away both the config_options and the run-time tests.

Details below.

The sample usage was:

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

which raises the obvious question, "What's going on in the ellipsis?", especially given the following statement:

(Of course, this same option may need to be checked in many places in the system code.)

Let's assume that each of these config_option values really does correspond to a single problem domain (or implementation strategy) concept.

Instead of doing this (repeatedly, in various places throughout the code):

  1. Take a string (tag),
  2. Find its corresponding other string (value),
  3. Test that value as a boolean-equivalent,
  4. Based on that test, decide whether to perform some action.

I suggest encapsulating the concept of a "configurable action".

Let's take as an example (obviously just as hypthetical as FOO_ENABLED ... ;-) that your code has to work in either English units or metric units. If METRIC_ENABLED is "true", convert user-entered data from metric to English for internal computation, and convert back prior to displaying results.

Define an interface:

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

which identifies in one place all the behavior associated with the concept of METRIC_ENABLED.

Then write concrete implementations of all the ways those behaviors are to be carried out:

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;}
}

and

// 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;}
}

At startup time, instead of loading a bunch of config_options values, initialize a set of configurable actions, as in:

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

(where the expression metricOption() above is a stand-in for whatever one-time-only check you need to make, including looking at the value of METRIC_ENABLED ;-)

Then, wherever the code would have said:

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);

rewrite it as:

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

Because all of the implementation details related to that one concept are now packaged cleanly, all future maintenance related to METRIC_ENABLED can be done in one place. In addition, the run-time trade-off is a win; the "overhead" of invoking a method is trivial compared with the overhead of fetching a String value from a Map and performing String#equals.

萌︼了一个春 2024-07-20 00:56:50

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

最重要的是,你可以获取类型化函数,现在看来你只存储布尔值,如果你需要存储 int、字符串等怎么办?我宁愿使用 get_foo() 与类型,而不是 get_string("FOO") 或get_int(“FOO”)。

I believe that the two reasons you have mentioned, Possible misspelling in string, that cannot be detected until run time and the possibility (although slim) of a name change would justify your idea.

On top of that you can get typed functions, now it seems you only store booleans, what if you need to store an int, a string etc. I would rather use get_foo() with a type, than get_string("FOO") or get_int("FOO").

宛菡 2024-07-20 00:56:50

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

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

I think there are two different issues here:

  • In the current project, the convention of using hard-coded strings is already well established, so all the developers working on the project are familiar with it. It might be a sub-optimal convention for all the reasons that have been listed, but everybody familiar with the code can look at it and instinctively knows what the code is supposed to do. Changing the code so that in certain parts, it uses the "new" functionality will make the code slightly harder to read (because people will have to think and remember what the new convention does) and thus a little harder to maintain. But I would guess that changing over the whole project to the new convention would potentially be prohibitively expensive unless you can quickly script the conversion.
  • On a new project, symbolic constants are the way IMO, for all the reasons listed. Especially because anything that makes the compiler catch errors at compile time that would otherwise be caught by a human at run time is a very useful convention to establish.
佞臣 2024-07-20 00:56:50

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

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

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

print("Hello world!");

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

Another thing to consider is intent. If you are on a project that requires localization hard coded strings can be ambiguous. Consider the following:

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

The programmer's intent is clear. Using a constant implies that this string does not need to be localized. Now look at this example:

print("Hello world!");

Here we aren't so sure. Did the programmer really not want this string to be localized or did the programmer forget about localization while he was writing this code?

浅笑依然 2024-07-20 00:56:50

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

I too prefer a strongly-typed configuration class if it is used through-out the code. With properly named methods you don't lose any readability. If you need to do conversions from strings to another data type (decimal/float/int), you don't need to repeat the code that does the conversion in multiple places and can cache the result so the conversion only takes place once. You've already got the basis of this in place already so I don't think it would take much to get used to the new way of doing things.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文