假设我正在为应用程序定义浏览器实现类:
class InternetExplorerBrowser : IBrowser {
private readonly string executablePath = @"C:\Program Files\...\...\ie.exe";
...code that uses executablePath
}
乍一看,这可能是个好主意,因为
executablePath
数据接近使用它的代码。当我试图在我的另一台计算机上运行同一个应用程序时,问题就来了,它有一个外语操作系统:
executablePath
将有一个不同的值。我可以通过一个appsettings单例类(或者它的一个等价类)来解决这个问题,但是没有人知道我的类实际上依赖于这个appsettings类(这违背了di ideias)。它也可能给单元测试带来困难。
我可以通过构造函数传入
executablePath
来解决这两个问题:class InternetExplorerBrowser : IBrowser {
private readonly string executablePath;
public InternetExplorerBrowser(string executablePath) {
this.executablePath = executablePath;
}
}
但是这会在我的
Composition Root
(这个启动方法将完成所有需要的类连接)中引起问题,因为这个方法必须知道如何连接,并且必须知道所有这些小的设置数据:class CompositionRoot {
public void Run() {
ClassA classA = new ClassA();
string ieSetting1 = "C:\asdapo\poka\poskdaposka.exe";
string ieSetting2 = "IE_SETTING_ABC";
string ieSetting3 = "lol.bmp";
ClassB classB = new ClassB(ieSetting1);
ClassC classC = new ClassC(B, ieSetting2, ieSetting3);
...
}
}
很容易变成一个大混乱。
我可以通过传递表单的接口来解决这个问题
interface IAppSettings {
object GetData(string name);
}
所有需要某种设置的类。然后,我可以将其实现为一个包含所有设置的常规类,或者是一个从XML文件中读取数据的类。如果这样做,我应该为整个系统提供一个通用的appsettings类实例,还是将appsettings类与可能需要的每个类关联起来?这似乎有点过分了。另外,把所有的应用程序设置放在同一个位置,这样可以很容易地查看和查看当我试图将程序移动到不同平台时可能需要做的所有更改。
处理这种常见情况的最佳方法是什么?
编辑:
使用
IAppSettings
并将其所有设置硬编码在其中如何?interface IAppSettings {
string IE_ExecutablePath { get; }
int IE_Version { get; }
...
}
这将允许编译时类型安全。如果我看到接口/具体类增长过多,我可以创建其他更小的
IMyClassXAppSettings
接口。在医疗/大型项目中,这是否是一个负担太重而无法承受的负担?我也读过关于aop及其处理交叉关注点的优势(我想这就是其中之一)。它不能也提供这个问题的解决方案吗?可能会像这样标记变量:
class InternetExplorerBrowser : IBrowser {
[AppSetting] string executablePath;
[AppSetting] int ieVersion;
...code that uses executablePath
}
然后,在编译项目时,我们还将拥有编译时安全性(让编译器检查我们实际实现的代码是否会编织在数据中)。当然,这会将我们的api绑定到这个特定的方面。
最佳答案
各个类应该尽可能不受基础结构的影响-像IAppSettings
、IMyClassXAppSettings
和[AppSetting]
这样的结构将合成细节渗透到类中,最简单的说,这些类实际上只依赖于executablePath
这样的原始值。依赖注入的艺术在于关注点的分解。
我已经使用Autofac实现了这个确切的模式,它有类似于ninject的模块,应该会产生类似的代码(我意识到这个问题没有提到ninject,但是op在注释中提到)。
模块按子系统组织应用程序。模块公开子系统的可配置元素:
public class BrowserModule : Module
{
private readonly string _executablePath;
public BrowserModule(string executablePath)
{
_executablePath = executablePath;
}
public override void Load(ContainerBuilder builder)
{
builder
.Register(c => new InternetExplorerBrowser(_executablePath))
.As<IBrowser>()
.InstancePerDependency();
}
}
这就给构图根留下了同样的问题:它必须提供
executablePath
的值。为了避免配置汤,我们可以编写一个独立的模块,读取配置设置并将其传递给BrowserModule
public class ConfiguredBrowserModule : Module
{
public override void Load(ContainerBuilder builder)
{
var executablePath = ConfigurationManager.AppSettings["ExecutablePath"];
builder.RegisterModule(new BrowserModule(executablePath));
}
}
您可以考虑使用自定义配置节而不是
AppSettings
;更改将本地化到模块:public class BrowserSection : ConfigurationSection
{
[ConfigurationProperty("executablePath")]
public string ExecutablePath
{
get { return (string) this["executablePath"]; }
set { this["executablePath"] = value; }
}
}
public class ConfiguredBrowserModule : Module
{
public override void Load(ContainerBuilder builder)
{
var section = (BrowserSection) ConfigurationManager.GetSection("myApp.browser");
if(section == null)
{
section = new BrowserSection();
}
builder.RegisterModule(new BrowserModule(section.ExecutablePath));
}
}
这是一个很好的模式,因为每个子系统都有一个独立的配置,可以在一个地方读取。这里唯一的好处是更明显的意图。对于非
string
值或复杂模式,我们可以让System.Configuration
做重启。