假设我正在为我的应用程序定义一个浏览器实现类:
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
做重物。
我会选择最后一个选项 - 传入一个符合的对象 IAppSettings
接口。事实上,我最近在工作中执行了该重构,以便对某些单元测试进行整理,并且它运行良好。但是,很少有类依赖于该项目中的设置。
我将创建一个设置类的单个实例,并将其传递给任何依赖于它的东西。我看不出任何根本问题。
但是,我认为你已经考虑过这个问题,并且看到如果你有很多依赖于设置的类,那将是多么痛苦。
如果这对您来说是一个问题,您可以通过使用依赖注入框架来解决它 ninject (对不起,如果你已经知道像ninject这样的项目 - 这可能听起来有点光顾 - 如果你不熟悉, 为什么要使用ninject github上的部分是一个学习的好地方)。
使用ninject,对于您的主项目,您可以声明您希望任何具有依赖关系的类 IAppSettings
使用你的单例实例 AppSettings
基于类而不必将其明确地传递给各地的构造函数。
然后,您可以通过声明要使用的实例来为您的单元测试设置不同的系统 MockAppSettings
哪里 IAppSettings
使用,或直接显式传递您的模拟对象。
我希望我的问题得到了正确的答案,并且我帮助过了 - 你已经听起来像你知道你在做什么:)