“硬编码”是行话术语,意味着将值直接固定在程序代码中,而不是将其提取到设置或配置中。硬编码是开发中最著名的反模式之一,因为它降低了代码的灵活性和可重用性。根据Refactoring Guru,硬编码使测试、维护和应用程序适应不同环境变得困难。有意识地使用常量代替硬编码是成熟架构的标志。
要点
硬编码 – 将特定值嵌入程序代码中,以至于更改它需要编辑源代码并重新编译应用程序。“硬编码”这个比喻准确地反映了本质:值被牢牢固定,只能费力地将其从代码中分离出来。
硬编码的例子——写在函数体中的服务器URL字符串。如果服务器迁移到另一个地址,开发人员需要在代码中找到该字符串,更改它,重新构建应用程序并发布新版本。在具有正确架构的应用程序中,这样的URL会被提取到配置文件、环境变量或配置服务中。
“硬编码”一词带有更多情感色彩:它强调值是牢固嵌入且无法快速替换的。在中文环境中,这两种表达方式都作为完全同义词使用,带有负面含义。有时硬编码被戏称为“从常量中提取到单独常量的常量”。
硬编码是一种反模式,因为它违反了代码的可维护性、可测试性和可扩展性原则。在值被“硬编码”的代码中,任何环境、设计或逻辑的更改都需要在源代码中手动查找和替换。这增加了错误的风险并减慢了开发速度。
让我们以典型的移动应用程序为例来考察硬编码的具体后果。如果所有按钮的间距是由代码中的数字而不是通过资源来指定的——设计更改将需要查找所有出现的地方并替换。如果端点URL被硬编码——在环境(开发、测试、生产)之间切换而不重新编译是不可能的。
| 后果 | 描述 | 严重程度 |
|---|---|---|
| 维护困难 | 更改需要搜索整个代码 | 高 |
| 复制错误 | 并非所有出现的地方都被找到和替换 | 高 |
| 无法测试 | 无法替换测试数据 | 中 |
| 本地化问题 | 代码中的文本无法翻译 | 中 |
| 代码审查复杂化 | 审查者必须记住所有上下文 | 低 |
使用魔法数字和硬编码字符串的函数是硬编码的经典例子。一个月后,作者将不记得18、0.07和2.5是什么意思。一年后——团队中没有人敢于更改这些数字,担心破坏逻辑。将值提取到命名常量使代码成为自文档化的。
// 坏:魔法数字和字符串
fun calculatePrice(base: Double): Double {
val tax = base * 0.07
val tip = base * 0.15
val discount = if (base > 100) 10 else 0
return base + tax + tip - discount
}
硬编码的数据库URL将不允许在本地内存中数据库上运行测试。开发人员将不得不启动完整的服务器或在测试前修改代码。将配置从代码中提取出来解决了这个问题:测试使用测试参数,生产使用真实参数,而代码保持不变。
硬编码是一种反模式,但存在合理的例外,即硬编码的值不仅允许,而且更可取。界限沿着可变性轴划分:如果值在应用程序的生命周期中从未或几乎从未更改过,则可以硬编码。如果至少有可能更改——请提取到配置中。
数学和物理常量——圆周率、重力加速度、每秒毫秒数——对硬编码来说是安全的。它们由自然或标准定义,不会改变。由规范定义的常量数组大小也可以硬编码,但需附带关于数字来源的注释。
每秒毫秒数是由时间标准定义的稳定常量。将其提取到配置中没有意义,因为它永远不会改变。然而,即使是这样的常量也最好用可理解的名称声明,这样代码就不会包含“魔法数字”:将1000写成MILLISECONDS_IN_SECOND。
// 合理的硬编码:稳定常量
private const val MILLIS_IN_SECOND = 1000
private const val LOGIN_TIMEOUT_SECONDS = 30
fun formatDuration(ms: Long): String {
val seconds = ms / MILLIS_IN_SECOND
return "${seconds} sec."
}
有几种经过验证的方法可以避免硬编码,每种方法适用于特定类型的值。替代方案的选择取决于值变化的频率以及谁在更改它:开发人员、运维人员还是最终用户。
对于服务器URL、API密钥和功能标志,使用JSON、YAML或TOML格式的配置文件。在Android上是带有buildConfigField或res/values/config.xml的build.gradle。在iOS上是Info.plist或xcconfig。配置与应用程序一起构建,但对于不同的构建方案可能不同。
对于机密信息(令牌、密码)和环境参数,使用环境变量。它们不会进入代码仓库,并且在开发、测试和生产服务器上可能不同。在移动开发中,环境变量通常通过Xcode构建方案或Gradle中的构建风味来模拟。
字符串、颜色、尺寸、图像应提取到资源文件中:Android中的strings.xml、iOS中的Localizable.strings、Flutter中的ARB文件。这简化了本地化、适应不同屏幕和深色模式。更改资源中的字符串不需要重写代码。
<!-- Android: res/values/strings.xml -->
<resources>
<string name="app_name">MyApp</string>
<string name="api_base_url">https://api.example.com</string>
</resources>
对于服务和提供者,使用依赖注入,在Android上通过Dagger、Hilt或Koin,在iOS上通过Swinject。依赖注入框架允许动态替换实现——用于测试、不同环境、不同用户。这是最高级别的抽象,其中值的“硬编码”被外部注入所取代。
重构硬编码是将硬编码值提取到配置或资源的过程。如果方法得当,这是最安全的重构操作之一。下面描述的序列适用于任何语言和平台。
可以通过集成开发环境(在项目中搜索)或脚本进行搜索。搜索字符串、URL、数字字面量、尺寸、超时。特别注意重复值:如果同一个数字出现在五个地方,那么它就是提取到常量的候选对象。使用grep或集成开发环境/ Xcode的内置搜索。
为每个找到的值创建具有有意义名称的常量。按模块或类对常量进行分组。名称应解释值的含义,而不是如何使用:API_TIMEOUT,而不是TIMEOUT_30。替换后,代码中不应有任何数字没有解释。
// 之前:魔法数字0.4
let cardHeight = screenHeight * 0.4
// 之后:命名常量
private let cardHeightRatio: CGFloat = 0.4
let cardHeight = screenHeight * cardHeightRatio
如果值可能在构建或环境之间变化——将其提取到配置文件或应用程序资源中。对于字符串,使用本地化文件。对于URL——构建配置或xcconfig。对于尺寸——资源文件(Android中的dimens.xml)。检查应用程序在提取后是否正确编译和运行。
重构后,编写一个测试来检查配置是否正确加载以及值是否符合预期。如果将来有人更改配置,测试将指出不一致。配置测试是防止回归的快速可靠方法。
提取到配置后,检查所有使用旧值的地方是否引用单一来源。删除注释掉的代码和不再使用的旧常量。用描述哪些值以及被提取到何处的消息提交完成重构。
常见问题
硬编码 – 将值直接写入源代码,而不是将其提取到配置或资源中。这使得代码不太灵活且更难以维护。
硬编码使更改应用程序行为变得困难,妨碍测试,产生重复并增加复制错误的风险。更改硬编码的值需要重新构建和重新发布应用程序。
允许用于数学常量、在应用程序生命周期中不变的稳定值以及临时原型。在生产中,即使是常量也最好提取到命名变量中。
通过搜索找到所有魔法数字,用命名常量替换它们或提取到配置文件中。编写一个测试来检查配置的加载。删除重复项并用描述更改的提交提交。
常量 – 代码中的命名值,可在单个位置更改。硬编码 – 分布在代码各处的未命名值。良好实践:始终使用具有有意义名称的命名常量。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。