Clifton 方法 - 第三部分





5.00/5 (6投票s)
使用模块管理器和服务管理器进行引导
系列文章
引言
在之前的两篇文章中,我描述了用于动态加载模块的模块管理器,以及用于使用接口类型实现对象实例化的服务管理器。在本文中,我们将稍作休息,看看如何组合一个可用于几乎任何应用程序的引导程序,无论是 WinForm 客户端应用程序、Web 服务器还是其他。
模块初始化
引导程序使用核心类 ServiceModuleManager
。该类派生自 ModuleManager
并协调模块的初始化。正如模块管理器一文中提到的,IModule
实际上需要实现 InitializeServices
public interface IModule
{
void InitializeServices(IServiceManager serviceManager);
}
我们可以在 ServiceModuleManager
中看到这一点,它重写了 InitializeRegistrants
using System;
using System.Collections.Generic;
using Clifton.Core.ServiceManagement;
namespace Clifton.Core.ModuleManagement
{
public class ServiceModuleManager : ModuleManager, IServiceModuleManager
{
public IServiceManager ServiceManager { get; set; }
public virtual void Initialize(IServiceManager svcMgr)
{
ServiceManager = svcMgr;
}
public virtual void FinishedInitialization()
{
}
/// <summary>
/// Initialize each registrant by passing in the service manager.
/// This allows the module to register the services it provides.
/// </summary>
protected override void InitializeRegistrants(List<IModule> registrants)
{
registrants.ForEach(r =>
{
try
{
r.InitializeServices(ServiceManager);
}
catch (System.Exception ex)
{
throw new ApplicationException("Error initializing " +
r.GetType().AssemblyQualifiedName + "\r\n:" + ex.Message);
}
});
}
}
}
这为每个实现 IModule
接口的模块提供了初始化其所提供服务的机会。
引导程序
引导程序实例化一个 ServiceModuleManager
而不是 ModuleManager
,并执行两步初始化
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Clifton.Core.Assertions;
using Clifton.Core.ExtensionMethods;
using Clifton.Core.Semantics;
using Clifton.Core.ModuleManagement;
using Clifton.Core.ServiceManagement;
namespace BootstrapDemo
{
static partial class Program
{
public static ServiceManager serviceManager;
public static void InitializeBootstrap()
{
serviceManager = new ServiceManager();
serviceManager.RegisterSingleton<IServiceModuleManager, ServiceModuleManager>();
}
public static void Bootstrap(Action<Exception> onBootstrapException)
{
try
{
IModuleManager moduleMgr =
(IModuleManager)serviceManager.Get<IServiceModuleManager>();
List<AssemblyFileName> modules = GetModuleList(XmlFileName.Create("modules.xml"));
moduleMgr.RegisterModules(modules);
serviceManager.FinishedInitialization();
}
catch (Exception ex)
{
onBootstrapException(ex);
}
}
/// <summary>
/// Return the list of assembly names specified in the XML file so that
/// we know what assemblies are considered modules as part of the application.
/// </summary>
private static List<AssemblyFileName> GetModuleList(XmlFileName filename)
{
Assert.That(File.Exists(filename.Value), "Module definition file " +
filename.Value + " does not exist.");
XDocument xdoc = XDocument.Load(filename.Value);
return GetModuleList(xdoc);
}
/// <summary>
/// Returns the list of modules specified in the XML document so we know what
/// modules to instantiate.
/// </summary>
private static List<AssemblyFileName> GetModuleList(XDocument xdoc)
{
List<AssemblyFileName> assemblies = new List<AssemblyFileName>();
(from module in xdoc.Element("Modules").Elements("Module")
select module.Attribute("AssemblyName").Value).ForEach
(s => assemblies.Add(AssemblyFileName.Create(s)));
return assemblies;
}
}
}
我通常将引导程序实现为部分 Program
类。
让我们稍微剖析一下这段代码。在引导程序初始化中...
serviceManager = new ServiceManager();
serviceManager.RegisterSingleton<IServiceModuleManager, ServiceModuleManager>();
...我们实例化一个 ServiceManager
并注册 ServiceModuleManager
的实现者。
因为
ServiceModuleManager
本身就是一项服务,所以您可以将其替换为自己的初始化过程。
然后,在引导程序本身中...
IModuleManager moduleMgr = (IModuleManager)serviceManager.Get<IServiceModuleManager>();
List<AssemblyFileName> modules = GetModuleList(XmlFileName.Create("modules.xml"));
moduleMgr.RegisterModules(modules);
serviceManager.FinishedInitialization();
- 我们获取初始化注册者(模块)的服务,这使得每个实现
IModule
的类都可以访问服务管理器。返回的单例被强制转换为
IModuleManager
,因为ServiceModuleManager
派生自实现IModuleManager
的ModuleManager
。 - 获取应用程序要加载的模块。在此示例中,它们在文件“modules.xml”中指定
- 模块已注册。这会调用虚方法
InitializeRegistrants
,该方法在ServiceModuleManager
中被重写,并实现初始化过程的第一步。这是实现服务的模块可以注册这些服务,以及本地保存服务管理器以供这些服务使用的地方。 - 一旦所有模块服务都已注册,引导程序会告诉服务管理器为模块中注册的所有单例调用
FinishInitialization
public override void FinishedInitialization()
{
singletons.ForEach(kvp => kvp.Value.FinishedInitialization());
}
只有注册为单例的服务才会收到
FinishedInitialization
调用。正如服务管理器文章中提到的,单例会立即实例化,并且由于它们存在,它们现在可以完成所需的任何初始化(包括调用其他服务)。非单例服务通常在其构造函数中初始化自身。
如果您的服务初始化调用了另一个尚未完成其初始化的服务,这会有点问题。在某个时候,我可能会实现一个初始化顺序过程,但从技术上讲,由于服务是按照模块列表的顺序初始化的,您可以将依赖项放在列表的更高位置。
六行高级代码还不错!
使用引导程序
使用引导程序非常简单
using System;
namespace BootstrapDemo
{
static partial class Program
{
static void Main(string[] args)
{
InitializeBootstrap();
Bootstrap((e) => Console.WriteLine(e.Message));
}
}
}
几个例子
让我们编写一些作为模块的服务来说明这一切是如何工作的。
Clifton.AppConfigService
这是一个我经常使用的非常简单的服务,它封装了 .NET 的 ConfigurationManager
以获取连接字符串和应用程序设置
using System.Configuration;
using Clifton.Core.ModuleManagement;
using Clifton.Core.ServiceInterfaces;
using Clifton.Core.ServiceManagement;
namespace Clifton.Cores.Services.AppConfigService
{
public class AppConfigModule : IModule
{
public void InitializeServices(IServiceManager serviceManager)
{
serviceManager.RegisterSingleton<IAppConfigService, ConfigService>();
}
}
public class ConfigService : ServiceBase, IAppConfigService
{
public virtual string GetConnectionString(string key)
{
return ConfigurationManager.ConnectionStrings[key].ConnectionString;
}
public virtual string GetValue(string key)
{
return ConfigurationManager.AppSettings[key];
}
}
}
总是有两个部分
- 一个实现
IModule
并注册服务的类 - 一个或多个实现服务的类
我们将使用一个示例 app.config 文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings>
<add name="myConnectionString" connectionString="some connection string"/>
</connectionStrings>
<appSettings>
<add key ="someKey" value="someKeyValue"/>
</appSettings>
</configuration>
在 modules.xml 中,我们将指定 Clifton.AppConfigService
<?xml version="1.0" encoding="utf-8" ?>
<Modules>
<Module AssemblyName='Clifton.AppConfigService.dll'/>
</Modules>
作弊
问题是,您如何将模块 DLL(在本例中为 Clifton.AppConfigService.dll)放入应用程序的 bin\Debug 文件夹中?我通过将其添加为应用程序引用的引用来作弊
这可能是一种不良做法,因为当然,这些类现在可以直接访问您的应用程序!
使用服务
这也很简单——从服务管理器中获取服务并开始使用它
static void Main(string[] args)
{
InitializeBootstrap();
Bootstrap((e) => Console.WriteLine(e.Message));
IConfigService cfgSvc = serviceManager.Get<IConfigService>();
Console.WriteLine(cfgSvc.GetConnectionString("myConnectionString"));
Console.WriteLine(cfgSvc.GetValue("someKey"));
}
请注意,我们正在请求一个 IConfigService
实现者。虽然我们可以指定一个 IAppConfigService
实现者,但我们将在下面看到为什么我们引用 abstract
接口。
Clifton.EncryptedAppConfigService
在此示例中,我们假设您的 app.config 要么完全加密,要么完全不加密——换句话说,为我们获取值的服务是排他性的,我们只使用其中一个。这使我们能够利用 IConfigService
接口,而不管具体实现是简单地返回未加密的值,还是首先为我们解密它们。如果我们需要同时支持两者,那么我们需要向服务管理器指定我们想要哪个:具体接口 IAppConfigService
或 IEncryptedAppConfigService
。
加密的应用程序配置服务如下所示
public class ConfigService : ServiceBase, IEncryptedAppConfigService
{
public virtual string GetConnectionString(string key)
{
string enc = ConfigurationManager.ConnectionStrings[key].ConnectionString;
return Decrypt(enc);
}
public virtual string GetValue(string key)
{
string enc = ConfigurationManager.AppSettings[key];
return Decrypt(enc);
}
protected string Decrypt(string enc)
{
return ServiceManager.Get<IAppConfigDecryption>().Decrypt(enc);
}
}
请注意,为了解密 app.config 中的 string
,我们使用了一个必须由应用程序提供的服务:IAppConfigDecryption
。我们可以在另一个模块中实现它,但在此示例中,我将其直接添加到应用程序中。
接口如下所示
public interface IAppConfigDecryption : IService
{
string Password { get; set; }
string Salt { get; set; }
string Decrypt(string text);
}
以及服务作为新模块的实现
using Clifton.Core.ExtensionMethods;
using Clifton.Core.ModuleManagement;
using Clifton.Core.ServiceInterfaces;
using Clifton.Core.ServiceManagement;
namespace AppConfigDecryptionService
{
public class AppConfigDecryptionModule : IModule
{
public void InitializeServices(IServiceManager serviceManager)
{
serviceManager.RegisterSingleton<IAppConfigDecryption, AppConfigDecryptionService>(d =>
{
d.Password = "somepassword";
d.Salt = "somesalt";
});
}
}
public class AppConfigDecryptionService : ServiceBase, IAppConfigDecryption
{
public string Password { get; set; }
public string Salt { get; set; }
public string Decrypt(string text)
{
return text.Decrypt(Password, Salt);
}
}
}
显然(我希望如此),您会希望从某个安全位置获取密码和盐,这样通过反编译 DLL 就无法轻易获取
string
,或者您可以在应用程序中初始化它们。
现在,我真的很喜欢扩展方法,所以真正的实现就在那里(顺便说一句,我不是说这是最好的实现,实际上这是我在 StackOverflow 上找到的,哈哈)
public static string Decrypt(this string base64, string password, string salt)
{
string decryptedBytes = null;
byte[] saltBytes = Encoding.ASCII.GetBytes(salt);
byte[] passwordBytes = Encoding.ASCII.GetBytes(password);
byte[] decryptBytes = base64.FromBase64();
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC; // Cipher Block Chaining.
using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(decryptBytes, 0, decryptBytes.Length);
cs.Close();
}
decryptedBytes = Encoding.Default.GetString(ms.ToArray());
}
}
return decryptedBytes;
}
我们现在可以在 app.config 文件中提供加密值
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings>
<add name="myConnectionString"
connectionString="D+560OKzdaeBle1VHcKc+JyAWgRkVNTQxu/t7K5jSUo="/>
</connectionStrings>
<appSettings>
<add key ="someKey" value="JggSd0i52WcOEERjBTQR+g=="/>
</appSettings>
</configuration>
我们还修改 modules.xml 文件以使用加密的应用程序配置服务,此外还指定我们想要的解密服务
<?xml version="1.0" encoding="utf-8" ?>
<Modules>
<Module AssemblyName='Clifton.EncryptedAppConfigService.dll'/>
<Module AssemblyName='AppConfigDecryptionService.dll'/>
</Modules>
现在我们运行应用程序,得到相同的结果,但现在 string
未加密
请注意,我们根本不需要更改应用程序代码。这之所以有效,是因为应用程序可以使用应用程序配置服务,该服务被视为独占服务(无论是纯文本还是加密的),具有抽象接口 IConfigService
。如果我们要同时支持纯文本和加密值,我们将不得不使用 IAppConfigService
(用于纯文本值)或 IEncryptedAppConfigService
(用于加密值)来获取正确的服务。
接口地狱
如果您认为 DLL 地狱很糟糕,那么基于模块/服务的实现开始出现的一个问题是潜在的接口地狱。每个服务都实现一个接口。您将它们保存在哪里?如何组织它们?实现服务的模块和使用服务的应用程序都需要对接口的引用。我通常将接口组织在两个单独的项目中
Clifton.Core.ServiceInterface
-- 用于我的核心库中提供的服务。[MyAppServiceInterfaces]
-- 用于特定于应用程序的服务。
结论
模块和服务是实现依赖反转的好方法,上面的示例希望能说明这种架构的强大之处。但是,我仍然认为它不够抽象,原因很简单,应用程序仍然需要引用抽象或具体接口才能获取实现服务。
这将在下一篇文章中解决,我将在其中介绍语义发布-订阅者。那不会是轻量级的!
历史
- 2016 年 8 月 25 日:初始版本