馴服爛代碼
問題
我們有很多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
馴服爛代碼