引言
約定優於配置,配置趨於靈活
約定優於配置(convention over configuration),也稱作按約定編程,是一種軟件設計范式,旨在減少軟件開發人員需做決定的數量,獲得簡單的好處,而又不失靈活性。(這個約定,常見於團隊開發規范、項目結構、代碼規范、數據庫軍規等等。)
配置趨於靈活,這句話是我總結的。雖然推崇約定優於配置,但一個大型的復雜項目,總有這樣那樣的配置項,需要提供給用戶配置或外置於配置文件中,以供靈活變更。
那如何設計一個通用的配置模塊呢?
下面我將嘗試用最簡單易懂的方式,對Abp源碼中通用配置模塊的實現方式加以提煉和精簡,盡量繼承原作者的設計思想,給大家呈現通用配置模塊的“最佳實踐”。
提煉通用要素
上面提到的是通用配置模塊的設計,那我們就需要提煉通用部分。
- 配置的定義:都是基於Key/Value的配置項
- 配置的設置方式:代碼預置或外部配置文件預置
- 配置的持久化
- 配置值的讀取
配置的定義
簡單來說,配置的定義主要包含:
- 配置的名稱
- 配置的默認值
- 配置的簡要描述
- 配置的應用范圍
SettingDefinition
就是對配置定義的抽象:
/// <summary>
/// Defines a setting
/// </summary>
public class SettingDefinition
{
/// <summary>
/// Unique name of the setting.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Default value of the setting.
/// </summary>
public string DefaultValue { get; set; }
/// <summary>
/// Display name of the setting.
/// This can be used to show setting to the user.
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// A brief description for this setting.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Scopes of this setting.
/// Default value: <see cref="SettingScopes.Application"/>.
/// </summary>
public SettingScopes Scopes { get; set; }
public SettingDefinition(string name, string defaultValue, string displayName, string description, SettingScopes scopes)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
DefaultValue = defaultValue;
DisplayName = displayName;
Description = description;
Scopes = scopes;
}
public SettingDefinition(string name, string defaultValue)
: this(name, defaultValue, null, null, SettingScopes.Application)
{
}
}
SettingScopes
枚舉:
public enum SettingScopes
{
/// <summary>
/// Represents a setting that can be configured/changed for the application level.
/// </summary>
Application = 1,
/// <summary>
/// Represents a setting that can be configured/changed for each Tenant.
/// This is reserved
/// </summary>
Tenant = 2,
/// <summary>
/// Represents a setting that can be configured/changed for each User.
/// </summary>
User = 4,
/// <summary>
/// Represents a setting that can be configured/changed for all levels
/// </summary>
All = Application | Tenant | User
}
配置的設置和讀取
有了配置的定義,接下來我們就要考慮配置的設置和讀取。
我們先來定義ISettingDefinitionManager
接口來讀取配置的定義:
public interface ISettingDefinitionManager
{
/// <summary>
/// Get the <see cref="SettingDefinition"/> object with the unique name.
/// </summary>
/// <param name="name">Unique name of the Setting</param>
/// <returns>The <see cref="SettingDefinition"/>object.</returns>
SettingDefinition GetSettingDefinition(string name);
/// <summary>
/// Get a list of all setting definitions.
/// </summary>
/// <returns>All Settings</returns>
IEnumerable<SettingDefinition> GetAllSettingDefinitions();
}
再來研究配置的設置。配置的設置有以下幾種方式:
- 通過代碼預置
- 通過配置文件預置
像這種一種定義多種實現的需求,我們可以通過策略模式來實現。定義SettingProvider
抽象類用於獲取配置項:
/// <summary>
/// 設置提供者,用來返回具體的配置項列表。
/// </summary>
public abstract class SettingProvider
{
public abstract IEnumerable<SettingDefinition> GetSettingDefinitions();
}
如果通過代碼預置,可以通過以下方式實現:
public class TestSettingProvider : SettingProvider
{
public override IEnumerable<SettingDefinition> GetSettingDefinitions()
{
return new List<SettingDefinition>()
{
new SettingDefinition("EmailSettingNames.DefaultFromAddress", "admin@mydomain.com"),
new SettingDefinition("EmailSettingNames.DefaultFromDisplayName", "mydomain.com mailer"),
new SettingDefinition("EmailSettingNames.Smtp.Port", "587"),
new SettingDefinition("EmailSettingNames.Smtp.Host", "smtp.qq.com"),
new SettingDefinition("EmailSettingNames.Smtp.UserName", "ysjshengjie@qq.com"),
new SettingDefinition("EmailSettingNames.Smtp.Password", "123456"),
new SettingDefinition("EmailSettingNames.Smtp.Domain", ""),
new SettingDefinition("EmailSettingNames.Smtp.EnableSsl", "true"),
new SettingDefinition("EmailSettingNames.Smtp.UseDefaultCredentials", "false")
};
}
}
如果通過配置文件讀取,在.NET Core中可以注入IConfiguration
來獲取。
有了統一的配置設置接口,我們肯定需要一個容器來容納所有的配置項。
/// <summary>
/// 用於提供入口去注入設置提供者類型
/// </summary>
public interface ISettingConfiguration
{
ITypeList<SettingProvider> Providers { get; }
}
public class SettingConfiguration : ISettingConfiguration
{
public ITypeList<SettingProvider> Providers { get; private set; }
public SettingConfiguration()
{
Providers = new TypeList<SettingProvider>();
}
}
有了這個接口,我們通過實例化ISettingConfiguration
即可動態添加設置提供者類型。
var settingConfiguration = new SettingConfiguration();
settingConfiguration.Providers.Add<TestSettingProvider>();
有了這個統一的ISettingConfiguration
,我們再讀取配置就容易多了。我們在實現ISettingDefinitionManager
時注入ISettingConfiguration
,即可獲得系統預置的設置提供者類型。
public class SettingDefinitonManager : ISettingDefinitionManager
{
private readonly ISettingConfiguration _settingConfiguration;
private readonly IDictionary<string, SettingDefinition> _settings;
public SettingDefinitonManager(ISettingConfiguration settingConfiguration)
{
_settingConfiguration = settingConfiguration;
_settings = new Dictionary<string, SettingDefinition>();
}
/// <summary>
/// 初始化(加載系統定義的所有設置項)
/// </summary>
public void Initialize()
{
foreach (var providerType in _settingConfiguration.Providers)
{
if (Activator.CreateInstance(providerType) is SettingProvider provider)
foreach (var setting in provider.GetSettingDefinitions())
{
_settings[setting.Name] = setting;
}
}
}
/// <summary>
/// 根據設置項的名稱獲取設置定義
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public SettingDefinition GetSettingDefinition(string name)
{
if (!_settings.TryGetValue(name, out var settingDefinition))
{
throw new Exception("There is no setting defined with name: " + name);
}
return settingDefinition;
}
/// <summary>
/// 獲取所有的設置定義
/// </summary>
/// <returns></returns>
public IEnumerable<SettingDefinition> GetAllSettingDefinitions()
{
return _settings.Values;
}
}
至此就完成了配置項的定義、設置和讀取。如果對設計模式熟悉的話,這就是傳說中的Provider Pattern。
配置的持久化
因為配置會因SettingScopes
的不同,其對應的值也不同。所以配置的持久化,實際上是針對不同SettingScopes
下進行配置值的持久化。據此,我們可以抽象出SettingInfo
用來保存具體配置的值。
/// <summary>
/// Represents a setting information.
/// </summary>
[Serializable]
public class SettingInfo
{
/// <summary>
/// TenantId for this setting.
/// TenantId is null if this setting is not Tenant level.
/// </summary>
public int? TenantId { get; set; }
/// <summary>
/// UserId for this setting.
/// UserId is null if this setting is not user level.
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// Unique name of the setting.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Value of the setting.
/// </summary>
public string Value { get; set; }
/// <summary>
/// Creates a new <see cref="SettingInfo"/> object.
/// </summary>
public SettingInfo()
{
}
/// <summary>
/// Creates a new <see cref="SettingInfo"/> object.
/// </summary>
/// <param name="tenantId">TenantId for this setting. TenantId is null if this setting is not Tenant level.</param>
/// <param name="userId">UserId for this setting. UserId is null if this setting is not user level.</param>
/// <param name="name">Unique name of the setting</param>
/// <param name="value">Value of the setting</param>
public SettingInfo(int? tenantId, long? userId, string name, string value)
{
TenantId = tenantId;
UserId = userId;
Name = name;
Value = value;
}
}
據此,定義ISettingStore
用於SettingInfo
的CURD。
/// <summary>
/// 實現該接口以完成設置項的CURD
/// </summary>
public interface ISettingStore
{
/// <summary>
/// Gets a setting or null.
/// </summary>
/// <param name="tenantId">TenantId or null</param>
/// <param name="userId">UserId or null</param>
/// <param name="name">Name of the setting</param>
/// <returns>Setting object</returns>
Task<SettingInfo> GetSettingOrNullAsync(int? tenantId, long? userId, string name);
/// <summary>
/// Deletes a setting.
/// </summary>
/// <param name="setting">Setting to be deleted</param>
Task DeleteAsync(SettingInfo setting);
/// <summary>
/// Adds a setting.
/// </summary>
/// <param name="setting">Setting to add</param>
Task CreateAsync(SettingInfo setting);
/// <summary>
/// Update a setting.
/// </summary>
/// <param name="setting">Setting to add</param>
Task UpdateAsync(SettingInfo setting);
/// <summary>
/// Gets a list of setting.
/// </summary>
/// <param name="tenantId">TenantId or null</param>
/// <param name="userId">UserId or null</param>
/// <returns>List of settings</returns>
Task<List<SettingInfo>> GetAllListAsync(int? tenantId, long? userId);
}
我們可以根據自己項目的實際情況進行實現。比如在內存中存儲,或在數據庫中進行持久化。決定權在於我們自己。
配置值的讀取
因為ISettingStore
已經提供了必要的CURD接口,所以配置項值的讀取就很簡單。我們僅需根據SettingScopes
提供相應的讀取接口,在實現時注入ISettingStore
和ISettingDefinintionManager
即可實現配置項值的按需讀取。
/// <summary>
/// This is the main interface that must be implemented to be able to load/change values of settings.
/// </summary>
public interface ISettingManager
{
/// <summary>
/// Gets current value of a setting.
/// It gets the setting value, overwritten by application, current tenant and current user if exists.
/// </summary>
/// <param name="name">Unique name of the setting</param>
/// <returns>Current value of the setting</returns>
Task<string> GetSettingValueAsync(string name);
/// <summary>
/// Gets current value of a setting for the application level.
/// </summary>
/// <param name="name">Unique name of the setting</param>
/// <returns>Current value of the setting for the application</returns>
Task<string> GetSettingValueForApplicationAsync(string name);
/// <summary>
/// Gets current value of a setting for a tenant level.
/// It gets the setting value, overwritten by given tenant.
/// </summary>
/// <param name="name">Unique name of the setting</param>
/// <param name="tenantId">Tenant id</param>
/// <returns>Current value of the setting</returns>
Task<string> GetSettingValueForTenantAsync(string name, int tenantId);
/// <summary>
/// Gets current value of a setting for a user level.
/// It gets the setting value, overwritten by given tenant and user.
/// </summary>
/// <param name="name">Unique name of the setting</param>
/// <param name="tenantId">Tenant id</param>
/// <param name="userId">User id</param>
/// <returns>Current value of the setting for the user</returns>
Task<string> GetSettingValueForUserAsync(string name, int? tenantId, long userId);
}
當然也可按需添加修改接口。具體的實現就不再列出。而該類的設計就是門面模式了。
模塊梳理
以上就是通用配置模塊的設計,在實際使用時,我們只需以下步驟即可:
- 注入
ISettingConfiguration
的默認依賴。 - 按需實現
SettingProvider
並添加到ISettingConfiguration
實例的Provider
屬性中。 - 調用
ISettingDefinitionManager
的實例的Initialize
方法,將所有的Provider
中定義的配置項加載到內存中。 - 注入
ISettingDefinitionManager
的實例,已完成配置項定義的讀取。 - 按需實現
ISettingStore
完成配置項值的持久化。 - 注入
ISettingManager
完成對配置項值的讀取。
在.NET Core上的Microsoft.Extensions.Configuration也實現了一套通用配置模塊,用於訪問基於Key/Value的配置,支持讀取命令行參數、環境變量、INI文件、JSON和XML文件,有興趣的不妨一探究竟。