驯服烂代码
问题
我们有很多C#代码想要访问某种配置,而所有这些都是通过以下方式完成的:
ConfigurationManager.AppSettings["SomeSettingOrOther"]
例如,我们有一个
CurrencyConversion
类,它具有以下代码。
public class CurrencyConversion
{
Currency GetDefaultCurrency() {
// get the config setting
string configCurrency = ConfigurationManager.AppSettings["MarketCurrency"];
// return the equivalent Enum
return CurrencyFromString(configCurrency);
}
}
此代码有几个问题。
- 它对
设置的依赖是隐式的(即,我们不能通过查看类的公共接口来了解这一点,我们必须查看内部)。MarketCurrency
- 如果配置设置丢失或格式不正确,它可能会引发异常,并且只有在运行此特定代码段时,我们才会发现这一点。
- 代码的其他部分可能也使用此配置设置,这将重复字符串到Enum的转换以及任何错误处理。
- 可能是因为使用了错误的配置设置(或为了方便起见,重用了现有设置),并且应该更正确地使用
。 通过查看代码我们无法知道这一点,而这些知识仅存在于开发人员的头脑中。DefaultCurrency
- 测试很困难,因为我们必须设置ConfigurationManager,并且在确定所需的配置时可能会遇到一些反复试验。
- 配置源(
)是硬编码的,并且可能在整个代码库中有所不同(即某些使用ConfigurationManager的方法和某些读取环境变量的方法)ConfigurationManager
还有更高层次的问题。 这些调用散布在整个代码中,有时深入共享库中,并且无法知道哪些代码位需要哪些设置(不读取所有代码)。
这意味着很难验证配置文件是否包含了所需的所有信息,而且没有人敢于删除配置设置。 反过来,这导致我们的配置文件变得肿且令人困惑。
解决方案
为了解决这些问题,我们转向使用接口来定义我们的配置。
如果我们重构上面的示例代码以通过接口进行配置,则会得到以下信息(在现实生活中,由于只有一种设置,您可能决定直接将其直接传递,但请耐心等待)。
public interface ICurrencyConversionConfiguration
{
Currency DefaultCurrency;
}
public class CurrencyConversion
{
readonly ICurrencyConversionConfiguration configuration;
public CurrencyConversion(ICurrencyConversionConfiguration configuration) {
Contract.Requires(configuration != null);
this.configuration = configuration;
}
Currency GetDefaultCurrency() {
return configuration.DefaultCurrency;
}
}
该代码具有以下改进
- 现在,它对
依赖性是明确的。 没有它就无法创建该类。DefaultCurrency
- 通过构造函数注入可以满足对
的依赖关系。DefaultCurrency
- 测试时很容易模拟
。ICurrencyConversionConfiguration
- 名称是一致的。
- 读取和解析配置的责任已被删除。
为了处理读取和解析配置的责任,我们在下面添加了代码。 这依赖于一个简单的
Configuration
类,您可以在GitHub上的https://github.com/resgroup/configuration上看到它。
public class EconomicModelConfiguration : ICurrencyConversionConfiguration
{
readonly Configuration configuration;
public EconomicModelConfiguration(Configuration configuration) {
Contract.Requires(configuration != null);
this.configuration = configuration;
Validate();
}
void Validate() =>
using (var validator = configuration.CreateValidator)
validator.Check(() => DefaultCurrency);
public string DefaultCurrency =>
configuration.GetEnum<Currency>(MethodBase.GetCurrentMethod());
}
配置类本身由配置源实例化,该源从下面的示例中的环境变量中读取。 这样可以轻松遵守12项因子应用程序的建议( https://12factor.net/config )。
new Configuration(new GetFromEnvironment());
这具有以下改进
- 创建类时将检查配置设置(应在应用程序的入口点),因此会立即清除所有配置问题。
- 从字符串转换为货币的代码是集中的。
- 配置源已封装。
如果我们移动另一个类以使用新的配置系统,则会得到类似的信息。
public class EconomicModelConfiguration : ICurrencyConversionConfiguration, IConcreteCostConfiguration
{
readonly Configuration configuration;
public EconomicModelConfiguration(Configuration configuration) {
Contract.Requires(configuration != null);
this.configuration = configuration;
Validate();
}
void Validate() {
using (var validator = configuration.CreateValidator) {
validator.Check(() => DefaultCurrency);
validator.Check(() => DefaultConcreteCost);
}
}
public string DefaultCurrency =>
configuration.GetEnum<Currency>(MethodBase.GetCurrentMethod());
public double DefaultConcreteCost =>
configuration.GetDouble(MethodBase.GetCurrentMethod());
}
随着将更多类移至新系统,该过程将继续进行,并具有易于安装控制反转的优势,因为我们只需向其实现的所有接口注册
EconomicModelConfiguration
即可。
旧版代码
像任何成熟的软件团队一样,我们有一些遗留代码,其中一些尚无法通过控制反转创建。
对于这些类,我们创建一个静态Configuation类
public static class EconomicModelConfigurationStatic
{
readonly static EconomicModelConfiguration base = new EconomicModelConfiguration();
public static IEconomicModelConfiguration Settings =>
base;
}
然后在旧版代码中,替换
ConfigurationManager.AppSettings["SomeSettingOrOther"]
与
EconomicModelConfigurationStatic.Settings.SomeSettingOrOther
这给我们带来了新系统的很多好处,只需对现有代码进行很小的改动即可。
结论
以这种方式封装配置逻辑并通过接口提供配置具有以下好处。
- 没有魔术字符串 ,因此在编译时会捕获任何拼写错误
- 可以使用重构工具 (例如
),并保证所有实例都已更新rename
- 使用Visual Studio可以轻松找到对配置项的所有引用
- 代码是明确的有关所需配置的信息,并且只能定义所需配置的子集
- 可以检查配置文件以查看它们是否包含所有必需的信息
- 可以检查配置文件以查看它们是否包含任何多余信息
- 配置逻辑(例如默认值和转换)集中处理
- 保证配置项在配置文件和代码中具有相同的名称
如果您想使用它,可以使用nuget包 ,其源位于GitHub上 。
翻译自: https://hackernoon.com/taming-configuration-in-c-a2706b2d4741
驯服烂代码