1

为了获得一些良好 OO 设计的实践经验,我决定尝试在遗留应用程序上应用关注点分离。

我决定我对这些分散在代码库中的调用感到不舒服。

ConfigurationManager.AppSettings["key"]

虽然我之前已经通过编写一个帮助类将这些调用封装到静态方法中来解决这个问题,但我认为这可能是一个更进一步的机会。

我意识到最终我应该以使用依赖注入为目标,并且始终是“对接口进行编码”。但我不想迈出看起来太大的一步。与此同时,我想朝着这个最终目标迈出更小的步伐。

谁能列举他们推荐的步骤?

以下是一些我想到的:

  • 让客户端代码依赖于接口而不是具体的实现

  • 通过构造函数或属性手动将依赖项注入接口?

  • 在开始选择和应用 IoC 容器之前,我如何保持代码运行?

  • 为了实现依赖关系,任何需要配置值的类的默认构造函数都可以使用工厂(使用静态 CreateObject() 方法)?

当然,我仍然会对工厂有具体的依赖吗?...

我已经查阅了Michael Feathers 的书,所以我知道我需要介绍接缝,但我很难知道我什么时候介绍了足够多或太多!

更新

想象一下,客户端调用 WidgetLoader 上的方法,向其传递所需的依赖项(例如 IConfigReader)

WidgetLoader 读取配置以找出要加载的 Widgets 并要求 WidgetFactory 依次创建每个

WidgetFactory 读取配置以了解默认情况下将 Widget 置于什么状态

WidgetFactory 委托 WidgetRepository 进行数据访问,读取配置以决定应该记录哪些诊断

在上述每种情况下,IConfigReader 是否应该像烫手山芋一样在调用链中的每个成员之间传递?

工厂是答案吗?

为了澄清以下一些评论:

我的主要目标是逐渐将一些应用程序设置从配置文件中迁移到其他形式的持久性中。虽然我意识到通过注入的依赖项,我可以提取和覆盖以获得一些单元测试的优点,但我主要关心的不是测试太多,而是封装足够多,以至于开始不知道设置实际在哪里持续存在。

4

3 回答 3

3

在重构遗留代码库时,您希望随着时间的推移迭代地进行小的更改。这是一种方法:

  • 使用获取应用程序设置的方法(即 GetAppSettingString(string key))创建一个新的静态类(即 MyConfigManager)

  • 对“ConfigurationManager.AppSettings["key"] 进行全局搜索和替换,并将实例替换为 "MyConfigManager.GetAppSettingsString("key")"

  • 测试和签到

现在您对 ConfigurationManager 的依赖在一个地方。您可以将设置存储在数据库或任何地方,而无需更改大量代码。不利的一面是你仍然有一个静态依赖。

下一步是将 MyConfigManager 更改为常规实例类并将其注入到使用它的类中。这里最好的方法是逐步进行。

  • 在静态类旁边创建一个实例类(和一个接口)。

  • 现在您已经拥有了两者,您可以慢慢重构 using 类,直到它们都使用实例类。将实例注入构造函数(使用接口)。如果有很多用途,请不要尝试大爆炸签入。随着时间的推移,慢慢地、小心地做。

  • 然后只需删除静态类。

于 2009-12-11T15:34:45.503 回答
1

通常很难清理遗留应用程序是小步骤,因为它们并非旨在以这种方式进行更改。如果代码完全混合并且您没有 SoC,则很难在不被迫更改其他所有内容的情况下进行更改……而且通常很难对任何内容进行单元测试。

但总的来说,您必须: 1) 找到尚未重构的最简单(最小)的类 2) 为此类编写单元测试,以便您确信重构没有破坏任何东西 3) 做尽可能小的更改(这取决于关于项目和你的常识)4)确保所有测试都通过5)提交并转到1

我想推荐 Martin Fowler 的“重构”给你更多的想法:http ://www.amazon.com/exec/obidos/ASIN/0201485672

于 2009-12-11T15:28:18.833 回答
1

对于您的示例,我要做的第一件事是创建一个接口,公开您需要读取配置的功能,例如

public interface IConfigReader
{
    string GetAppSetting(string key);
    ...
}

然后创建一个委托给静态 ConfigurationManager 类的实现:

public class StaticConfigReader : IConfigReader
{
    public string Get(string key)
    {
        return ConfigurationManager.AppSetting[key];
    }
}

然后对于依赖于配置的特定类,您可以创建一个接缝,该接缝最初只返回静态配置读取器的实例:

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;
        }
    }
}

然后您的类只需通过静态类访问配置:

public void MethodUsingConfig()
{
    string value = Configuration.GetConfigReader().GetAppSetting("key");
}

并且您的测试可以通过临时范围使用假货:

[Test]
public void TestMethodUsingConfig()
{
    using(var scope = Configuration.CreateConfigScope(fakeReader))
    {
        new ClassUsingConfig().MethodUsingConfig();
        //Assertions
    }
}
于 2009-12-11T15:49:46.550 回答