C# 在应用更好的设计时采取初步步骤
为了获得一些良好OO设计的实践经验,我决定尝试在遗留应用程序上应用关注点分离 我觉得这些调用分散在代码库中让我感到不舒服C# 在应用更好的设计时采取初步步骤,c#,configuration,refactoring,dependency-injection,separation-of-concerns,C#,Configuration,Refactoring,Dependency Injection,Separation Of Concerns,为了获得一些良好OO设计的实践经验,我决定尝试在遗留应用程序上应用关注点分离 我觉得这些调用分散在代码库中让我感到不舒服 ConfigurationManager.AppSettings["key"] 虽然我之前已经通过编写一个helper类将这些调用封装到静态方法中解决了这一问题,但我认为这可能是一个更进一步的机会 我意识到,最终我的目标应该是使用依赖注入,并始终“编码到接口”。但我不想迈出看起来太大的一步。与此同时,我想朝着最终目标迈出一小步 有人能列举他们会推荐的步骤吗 我想到了以下几点
ConfigurationManager.AppSettings["key"]
虽然我之前已经通过编写一个helper类将这些调用封装到静态方法中解决了这一问题,但我认为这可能是一个更进一步的机会
我意识到,最终我的目标应该是使用依赖注入,并始终“编码到接口”。但我不想迈出看起来太大的一步。与此同时,我想朝着最终目标迈出一小步
有人能列举他们会推荐的步骤吗
我想到了以下几点:
让客户端代码依赖于接口而不是具体实现
手动将依赖项注入到
通过构造函数或属性的接口
在去努力之前
选择和应用国际奥委会
容器如何保存代码
跑步
为了满足依赖关系,默认设置为
任何需要
配置值可以使用工厂
使用静态CreateObject方法
当然我仍然会对工厂有一个具体的依赖
我已经深入了解了,所以我知道我需要引入接缝,但我很难知道什么时候我已经引入了足够或太多的接缝
更新
假设客户机在WidgetLoader上调用方法,并将所需的依赖项(如IConfigReader)传递给它
WidgetLoader读取配置以找出要加载的小部件,并要求WidgetFactory依次创建每个小部件
WidgetFactory读取配置以了解在默认情况下将小部件置于什么状态
WidgetFactory委托WidgetRepository执行数据访问,该操作读取配置以决定应记录哪些诊断
在上述每种情况下,IConfigReader是否应该像烫手山芋一样在调用链中的每个成员之间传递
工厂就是答案吗
澄清以下一些意见:
我的主要目标是逐渐将一些应用程序设置从配置文件迁移到其他形式的持久性中。虽然我意识到,通过注入依赖性,我可以获得一些单元测试的好处,但我主要关心的不是测试太多,而是封装得足够多,以至于开始不知道设置实际在哪里持久化。通常清理遗留应用程序非常困难,只需一小步,因为它们的设计不是为了以这种方式改变。如果代码完全混合在一起,并且你没有SoC,那么你很难在不被迫改变其他一切的情况下改变某件事。。。而且,通常很难对任何东西进行单元测试 但一般来说,你必须: 我发现最简单的最小类尚未重构 2为这个类编写单元测试,这样您就有信心重构不会破坏任何东西 3.做尽可能小的改变这取决于项目和你的常识 4确保所有测试都通过 5提交并转到1
我想推荐Martin Fowler的重构,让您有更多的想法:通常清理遗留应用程序非常困难,因为它们不是设计为以这种方式更改的。如果代码完全混合在一起,并且你没有SoC,那么你很难在不被迫改变其他一切的情况下改变某件事。。。而且,通常很难对任何东西进行单元测试 但一般来说,你必须: 我发现最简单的最小类尚未重构 2为这个类编写单元测试,这样您就有信心重构不会破坏任何东西 3.做尽可能小的改变这取决于项目和你的常识 4确保所有测试都通过 5提交并转到1
我想推荐Martin Fowler的重构,让您有更多的想法:当重构遗留代码库时,您希望随着时间的推移迭代地进行一些小的更改。以下是一种方法: 创建一个新的静态类,即MyConfigManager,使用一个方法获取应用程序设置,即GetAppSettingString键
执行ConfigurationManager.AppSettings[key]的全局搜索和替换,并用MyConfigManager.GetAppSettingsStringkey替换实例 测试和登记 现在,您对ConfigurationManager的依赖性就在一个地方。您可以将设置存储在数据库或任何地方,而无需更改大量代码。不利的一面是,您仍然有一个静态依赖项 下一步是将MyConfigManager更改为常规实例类,并将其注入到使用它的类中。这里最好的方法是以增量方式进行 在静态类旁边创建实例类和接口 既然两者都有了,就可以慢慢地重构使用类,直到它们都使用实例类。将实例注入到 使用接口的构造函数。如果有很多用法,不要尝试大爆炸签入。随着时间的推移,慢慢地、小心地做 然后删除静态类
当重构遗留代码库时,您希望随着时间的推移迭代地进行小的更改。以下是一种方法: 创建一个新的静态类,即MyConfigManager,使用一个方法获取应用程序设置,即GetAppSettingString键
执行ConfigurationManager.AppSettings[key]的全局搜索和替换,并用MyConfigManager.GetAppSettingsStringkey替换实例 测试和登记 现在,您对ConfigurationManager的依赖性就在一个地方。您可以将设置存储在数据库或任何地方,而无需更改大量代码。不利的一面是,您仍然有一个静态依赖项 下一步是将MyConfigManager更改为常规实例类,并将其注入到使用它的类中。这里最好的方法是以增量方式进行 在静态类旁边创建实例类和接口 既然两者都有了,就可以慢慢地重构使用类,直到它们都使用实例类。使用接口将实例注入构造函数。如果有很多用法,不要尝试大爆炸签入。随着时间的推移,慢慢地、小心地做 然后删除静态类
对于您的示例,我要做的第一件事是创建一个接口,公开您需要读取config的功能
public interface IConfigReader
{
string GetAppSetting(string key);
...
}
然后创建一个委托给静态ConfigurationManager类的实现:
public class StaticConfigReader : IConfigReader
{
public string Get(string key)
{
return ConfigurationManager.AppSetting[key];
}
}
然后,对于依赖于配置的特定类,您可以创建一个seam,该seam最初只返回静态配置读取器的一个实例:
public class ClassRequiringConfig
{
public void MethodUsingConfig()
{
string setting = this.GetConfigReader().GetAppSetting("key");
}
protected virtual IConfigReader GetConfigReader()
{
return new StaticConfigReader();
}
}
并用接口的用法替换对ConfigManager的所有引用。然后,出于测试目的,您可以对该类进行子类化,并重写GetConfigReader方法以注入伪代码,这样您就不需要任何实际的配置文件:
public class TestClassRequiringConfig : ClassRequiringConfig
{
public IConfigReader ConfigReader { get; set; }
protected override IConfigReader GetConfigReader()
{
return this.ConfigReader;
}
}
[Test]
public void TestMethodUsingConfig()
{
ClassRequiringConfig sut = new TestClassRequiringConfig { ConfigReader = fakeConfigReader };
sut.MethodUsingConfig();
//Assertions
}
最后,当您添加IoC容器时,您将能够用属性/构造函数注入来替换它
编辑:
如果您不喜欢将实例注入到单独的类中(如果许多类依赖于配置,这将非常繁琐),那么您可以创建一个静态配置类,然后允许对配置读取器进行临时更改以进行测试:
public static class Configuration
{
private static Func<IConfigReader> _configReaderFunc = () => new StaticConfigReader;
public static Func<IConfigReader> GetConfiguration
{
get { return _configReaderFunc; }
}
public static IDisposable CreateConfigScope(IConfigReader reader)
{
return new ConfigReaderScope(() => reader);
}
private class ConfigReaderScope : IDisposable
{
private readonly Func<IConfigReader> _oldReaderFunc;
public ConfigReaderScope(Func<IConfigReader> newReaderFunc)
{
this._oldReaderFunc = _configReaderFunc;
_configReaderFunc = newReaderFunc;
}
public void Dispose()
{
_configReaderFunc = this._oldReaderFunc;
}
}
}
您的测试可以在临时范围内使用假代码:
[Test]
public void TestMethodUsingConfig()
{
using(var scope = Configuration.CreateConfigScope(fakeReader))
{
new ClassUsingConfig().MethodUsingConfig();
//Assertions
}
}
对于您的示例,我要做的第一件事是创建一个接口,公开您需要读取config的功能
public interface IConfigReader
{
string GetAppSetting(string key);
...
}
然后创建一个委托给静态ConfigurationManager类的实现:
public class StaticConfigReader : IConfigReader
{
public string Get(string key)
{
return ConfigurationManager.AppSetting[key];
}
}
然后,对于依赖于配置的特定类,您可以创建一个seam,该seam最初只返回静态配置读取器的一个实例:
public class ClassRequiringConfig
{
public void MethodUsingConfig()
{
string setting = this.GetConfigReader().GetAppSetting("key");
}
protected virtual IConfigReader GetConfigReader()
{
return new StaticConfigReader();
}
}
并用接口的用法替换对ConfigManager的所有引用。然后,出于测试目的,您可以对该类进行子类化,并重写GetConfigReader方法以注入伪代码,这样您就不需要任何实际的配置文件:
public class TestClassRequiringConfig : ClassRequiringConfig
{
public IConfigReader ConfigReader { get; set; }
protected override IConfigReader GetConfigReader()
{
return this.ConfigReader;
}
}
[Test]
public void TestMethodUsingConfig()
{
ClassRequiringConfig sut = new TestClassRequiringConfig { ConfigReader = fakeConfigReader };
sut.MethodUsingConfig();
//Assertions
}
最后,当您添加IoC容器时,您将能够用属性/构造函数注入来替换它
编辑:
如果您不喜欢将实例注入到单独的类中(如果许多类依赖于配置,这将非常繁琐),那么您可以创建一个静态配置类,然后允许对配置读取器进行临时更改以进行测试:
public static class Configuration
{
private static Func<IConfigReader> _configReaderFunc = () => new StaticConfigReader;
public static Func<IConfigReader> GetConfiguration
{
get { return _configReaderFunc; }
}
public static IDisposable CreateConfigScope(IConfigReader reader)
{
return new ConfigReaderScope(() => reader);
}
private class ConfigReaderScope : IDisposable
{
private readonly Func<IConfigReader> _oldReaderFunc;
public ConfigReaderScope(Func<IConfigReader> newReaderFunc)
{
this._oldReaderFunc = _configReaderFunc;
_configReaderFunc = newReaderFunc;
}
public void Dispose()
{
_configReaderFunc = this._oldReaderFunc;
}
}
}
您的测试可以在临时范围内使用假代码:
[Test]
public void TestMethodUsingConfig()
{
using(var scope = Configuration.CreateConfigScope(fakeReader))
{
new ClassUsingConfig().MethodUsingConfig();
//Assertions
}
}
AppSettings[key]是.NET处理常量的方式。那不是我追求的第一件事。用一个静态方法替换它对我来说是多余的。您已经有了一个从配置文件中检索常量的静态方法。即使方法名称的更改与.NET 1.1->2.0中的更改相同,VisualStudio也使更改变得非常简单。我完全理解你所做的,但这对一个新锤子的人来说有点异味,每个问题都是一颗钉子。当试图重构遗留应用程序时,我通常先寻找真正糟糕的、不枯燥的代码,并将其转化为函数。正如Brian Frantz所暗示的,但我没有明确说明,我想将这些配置移出.config并移到DB表中。是的,你是对的,我已经开始担心appSettings并不是解决依赖注入问题的最佳人选……ConfigurationManager.appSettings[key]是.NET处理常量的方式。那不是我追求的第一件事。用一个静态方法替换它对我来说是多余的。您已经有了一个从配置文件中检索常量的静态方法。即使方法名称的更改与.NET 1.1->2.0中的更改相同,VisualStudio也使更改变得非常简单。我非常感激你所做的一切,但这对一个新锤子的人来说有点异味,每个问题都是钉子
为了重构遗留应用程序,我通常首先寻找真正糟糕的、非干性的代码,然后将这些代码放入函数中。正如Brian Frantz所提到的,但我没有明确说明,我想将这些配置移出.config并放入DB表中。但你是对的,我已经开始担心appSettings并不是我在依赖注入方面的最佳人选……谢谢你的回答。我已经有了一份重构和重竖琴的副本,但我仍然觉得陷入了困境。我可能会尝试一下我提到的工厂,以及一些你很好地信任我的常识:-谢谢你的回答。我已经有了一份重构和重竖琴的副本,但我仍然觉得陷入了困境。我可能会尝试一下我提到的工厂,还有一些你很好地信任我的常识:-+1我很喜欢这家工厂的声音。我没有想到的关键是让静态和实例彼此并排一段时间。。。谢谢。经过深思熟虑。。。谁打针?当我有嵌套的依赖关系时,父母会把依赖关系推到孩子身上吗?这就是我提到工厂的原因。但是工厂仍然会有静电,所以我觉得这是一个陷阱22:@rohancragg-你的顶级应用程序或测试线束会进行注射。另请参阅这篇优秀的文章:。注入由调用构造函数的人完成。然后可以注入到该类中,依此类推到对象树的顶部。只要随着时间的推移逐渐地做,你就会没事的。无论你做什么,不要试图一次做完。或者,您可以在构造函数中插入和实例化实例之前执行中间步骤。然后再担心注射。@Jeff Sternal Misko Hervey和他引用的其他人的文章绝对是黄金-谢谢:-+1我喜欢这篇文章的声音。我没有想到的关键是让静态和实例彼此并排一段时间。。。谢谢。经过深思熟虑。。。谁打针?当我有嵌套的依赖关系时,父母会把依赖关系推到孩子身上吗?这就是我提到工厂的原因。但是工厂仍然会有静电,所以我觉得这是一个陷阱22:@rohancragg-你的顶级应用程序或测试线束会进行注射。另请参阅这篇优秀的文章:。注入由调用构造函数的人完成。然后可以注入到该类中,依此类推到对象树的顶部。只要随着时间的推移逐渐地做,你就会没事的。无论你做什么,不要试图一次做完。或者,您可以在构造函数中插入和实例化实例之前执行中间步骤。然后再担心注射。@Jeff Sternal Misko Hervey和他引用的其他人的文章是绝对可靠的-谢谢:-在命名类StaticConfigReader时,你是说它是Stistic还是只是命名中的一个共同事件?我仍然不喜欢在每个需要的类中“更新”实例…谢谢你的回答。正如我现在在原问题中澄清的那样;我更关心的是封装以支持对功能需求的更改,而不仅仅是支持测试。我真的很感激在我做任何改变之前我应该努力测试:-再次感谢编辑,这是一个真正的大开眼界,我需要一段时间来消化它。我希望我能接受不止一个答案,因为到目前为止我喜欢不止一个答案的元素。在命名类StaticConfigReader时,你是指它是Stistic还是只是命名中的一个共同关联?我仍然不喜欢在每个需要的类中“更新”实例…谢谢你的回答。正如我现在在原问题中澄清的那样;我更关心的是封装以支持对功能需求的更改,而不仅仅是支持测试。我真的很感激在我做任何改变之前我应该努力测试:-再次感谢编辑,这是一个真正的大开眼界,我需要一段时间来消化它。我希望我能接受不止一个答案,因为到目前为止我喜欢不止一个答案的要素。