正确使用依赖注入

时间:2010-04-04 02:34:41

标签: c# dependency-injection inversion-of-control

我和另外两位同事正试图了解如何最好地设计一个程序。例如,我有一个接口ISoda和多个实现该接口的类,如CokePepsiDrPepper等....

我的同事说,最好将这些项目放入数据库,如键/值对。例如:

Key       |  Name
--------------------------------------
Coke      |  my.namespace.Coke, MyAssembly
Pepsi     |  my.namespace.Pepsi, MyAssembly
DrPepper  |  my.namespace.DrPepper, MyAssembly

...然后有XML配置文件,将输入映射到正确的密钥,查询数据库中的密钥,然后创建对象。

我没有任何具体原因,但我觉得这是一个糟糕的设计,但我不知道该说些什么或如何正确反对它。

我的第二位同事建议我们对这些课程进行微观管理。所以基本上输入将通过switch语句,类似于此:

ISoda soda;

switch (input)
{
   case "Coke":
      soda = new Coke();
      break;       
   case "Pepsi":
      soda = new Pepsi();
      break;
   case "DrPepper":
      soda = new DrPepper();
      break;
}

这对我来说似乎有点好,但我仍然认为有更好的方法。过去几天我一直在阅读IoC容器,这似乎是一个很好的解决方案。但是,我对依赖注入和IoC容器仍然很新,所以我不知道如何正确地争论它。或许我错了,有更好的方法吗?如果是这样,有人可以提出更好的方法吗?

我可以提出什么样的论据来说服我的同事尝试另一种方法?有哪些优点/缺点?我们为什么要这样做呢?

不幸的是,我的同事非常不愿意改变,所以我想弄清楚如何说服他们。

修改

似乎我的问题有点难以理解。我们要弄清楚的是如何最好地设计一个利用多个接口的应用程序,并包含许多实现这些接口的具体类型。

是的,我知道将这些内容存储在数据库中是一个坏主意,但我们根本不知道更好的方法。这就是为什么我要求如何我们做得更好。

public void DrinkSoda(string input)
{
   ISoda soda = GetSoda(input);
   soda.Drink();
}

我们如何正确实施GetSoda?我们应该重新考虑整个设计吗?如果传递这样的魔法字符是个坏主意,那么我们应该怎么做呢?

用户输入,例如“Diet Coke”,“Coke”,“Coke Lime”或任何会实例化Coke类型的内容,因此多个关键字会映射到单一类型。

很多人都说上面发布的代码很糟糕。我知道这很糟糕。我正在寻找理由向同事们展示,并说明为什么这很糟糕以及为什么我们需要改变。

如果这个解释仍然难以理解,我道歉。我很难描述这种情况,因为我真的不明白如何用文字表达。

6 个答案:

答案 0 :(得分:7)

作为一般规则,传递包装概念的对象比传递字符串更加面向对象。也就是说,当您需要将稀疏数据(如字符串)转换为正确的域对象时,有很多情况(特别是在UI方面)。

例如,您需要将字符串形式的用户输入转换为ISoda实例。

标准的,松散耦合的方式是使用Abstract Factory。像这样定义:

public interface ISodaFactory
{
    ISoda Create(string input);
}

您的消费者现在可以依赖ISodaFactory并使用它,如下所示:

public class SodaConsumer
{
    private readonly ISodaFactory sodaFactory;

    public SodaConsumer(ISodaFactory sodaFactory)
    {
        if (sodaFactory == null)
        {
            throw new ArgumentNullException(sodaFactory);
        }

        this.sodaFactory = sodaFactory;
    }

    public void DrinkSoda(string input)   
    {   
        ISoda soda = GetSoda(input);   
        soda.Drink();   
    }

    private ISoda GetSoda(input)
    {
        return this.sodaFactory.Create(input);
    }
}

注意Guard子句和readonly关键字的组合如何保证SodaConsumer类的不变量,它允许您使用依赖项而不必担心它可能为null。

答案 1 :(得分:4)

我遇到的依赖注入的最佳介绍实际上是quick series of articles that form a walkthrough of the concepts wiki网站上的Ninject

你还没有真正给出你想要实现的目标的背景,但在我看来,你正试图设计一个“插件”架构。如果是这种情况 - 你的感觉是正确的 - 这些设计都是一句话,可怕。第一个是相对较慢的(数据库反射?),对数据库层有大量不必要的依赖关系,并且不会随程序扩展,因为每个新类都必须输入到数据库中。使用反射有什么问题?

第二种方法是关于不可扩展的。每次引入新类时,都必须更新旧代码,并且必须“了解”您将要插入的每个对象 - 使第三方无法开发插件。您添加的耦合,即使是像Locator模式这样的耦合,也意味着您的代码不够灵活。

这可能是使用依赖注入的好机会。阅读我链接的文章。关键思想是依赖注入框架将使用配置文件来动态加载您指定的类。这使得交换程序组件变得非常简单和直接。

答案 2 :(得分:4)

如果我需要根据名称或其他序列化数据实例化正确的类,这是我通常使用的模式:

public interface ISodaCreator
{
    string Name { get; }
    ISoda CreateSoda();
}

public class SodaFactory
{
    private readonly IEnumerable<ISodaCreator> sodaCreators;

    public SodaFactory(IEnumerable<ISodaCreator> sodaCreators)
    {
        this.sodaCreators = sodaCreators;
    }

    public ISoda CreateSodaFromName(string name)
    {
        var creator = this.sodaCreators.FirstOrDefault(x => x.Name == name);
        // TODO: throw "unknown soda" exception if creator null here

        return creator.CreateSoda();
    }
}

请注意,您仍然需要进行大量ISodaCreator实现,并以某种方式收集要传递给SodaFactory构造函数的所有实现。为了减少相关工作,您可以根据反射创建一个通用的苏打创建者:

public ReflectionSodaCreator : ISodaCreator
{
    private readonly ConstructorInfo constructor;

    public string Name { get; private set; }

    public ReflectionSodaCreator(string name, ConstructorInfo constructor)
    {
        this.Name = name;
        this.constructor = constructor;
    }

    public ISoda CreateSoda()
    {
        return (ISoda)this.constructor.Invoke(new object[0])
    }
}

然后只扫描执行程序集(或其他一些程序集)来查找所有苏打类型并构建SodaFactory,如下所示:

IEnumerable<ISodaCreator> sodaCreators =
    from type in Assembly.GetExecutingAssembly().GetTypes()
    where type.GetInterface(typeof(ISoda).FullName) != null
    from constructor in type.GetConstructors()
    where constructor.GetParameters().Length == 0
    select new ReflectionSodaCreator(type.Name, constructor);
sodaCreators = new List<ISodaCreator>(sodaCreators); // evaluate only once
var sodaFactory = new SodaFactory(sodaCreators);

请注意,我的答案补充了Mark Seemann的答案:他告诉您让客户使用抽象的ISodaFactory,这样他们就不必关心 苏打工厂的运作方式。我的答案是这样一个苏打工厂的示例实施。

答案 3 :(得分:3)

我会说实话,我知道其他人不同意,但我发现将类实例化为XML或数据库是一个可怕的想法。即使它牺牲了一些灵活性,我也很擅长使代码变得清晰。我看到的缺点是:

  • 很难跟踪代码并找到对象的创建位置。
  • 其他从事该项目的人员需要了解您用于实例化对象的专有格式。
  • 在小项目中,您将花费更多时间实现依赖注入,实际上您将切换类。
  • 在运行时之前,您将无法分辨出正在创建的内容。这是一个调试噩梦,你的调试器也不会喜欢你。

我实际上有点厌倦了听到这种特殊的节目时尚。它感觉和看起来很丑,它解决了一个问题,很少有项目需要解决。说真的,你多久会从该代码中添加和删除苏打水。除非它几乎每次你编译,否则这么多的额外complexexity就没那么有优势了。

为了澄清,我认为控制反转可能是一个很好的设计理念。我只是不喜欢外部嵌入代码只是为了实例化,用XML或其他方式。

交换机案例解决方案很好,因为这是实现工厂模式的一种方法。大多数程序员都不会很难理解它。它也比在代码中实例化子类更清晰。

答案 4 :(得分:3)

每个DI库都有自己的语法。 Ninject如下所示:

public class DrinkModule : NinjectModule
{
    public override void Load()
    {
        Bind<IDrinkable>()
            .To<Coke>()
            .Only(When.ContextVariable("Name").EqualTo("Coke"));
        Bind<IDrinkable>()
            .To<Pepsi>()
            .Only(When.ContextVariable("Name").EqualTo("Pepsi"));
        // And so on
    }
}

当然,维护映射变得非常繁琐,所以如果我有这种系统,那么我通常最终将基于反射的注册放在一起,类似于this one

public class DrinkReflectionModule : NinjectModule
{
    private IEnumerable<Assembly> assemblies;

    public DrinkReflectionModule() : this(null) { }

    public DrinkReflectionModule(IEnumerable<Assembly> assemblies)
    {
        this.assemblies = assemblies ??
            AppDomain.CurrentDomain.GetAssemblies();
    }

    public override void Load()
    {
        foreach (Assembly assembly in assemblies)
            RegisterDrinkables(assembly);
    }

    private void RegisterDrinkables(Assembly assembly)
    {
        foreach (Type t in assembly.GetExportedTypes())
        {
            if (!t.IsPublic || t.IsAbstract || t.IsInterface || t.IsValueType)
                continue;
            if (typeof(IDrinkable).IsAssignableFrom(t))
                RegisterDrinkable(t);
        }
    }

    private void RegisterDrinkable(Type t)
    {
        Bind<IDrinkable>().To(t)
            .Only(When.ContextVariable("Name").EqualTo(t.Name));
    }
}

稍微多一点代码,但你永远不必更新它,它会变成自我注册。

哦,你这样使用它:

var pop = kernel.Get<IDrinkable>(With.Parameters.ContextVariable("Name", "Coke"));

显然这是特定于单个DI库(Ninject),但它们都支持变量或参数的概念,并且所有这些都有相同的模式(Autofac,Windsor,Unity等)

所以对你的问题的简短回答基本上是是的,DI可以做到这一点,这是一个非常好的地方有这种逻辑,虽然你的原始代码示例都不是问题实际上与DI有关(第二个是工厂方法,第一个是......好吧,企业)。

答案 5 :(得分:3)

我认为问题甚至不是工厂或其他什么;这是你的班级结构。

DrPepper的行为与Pepsi的行为有何不同? Coke?对我而言,听起来你正在使用不正确的接口。

根据具体情况,我会创建一个Soda类,其属性为BrandName或类似。 BrandName将通过DrPepperCoke设置为enumstring(前者似乎是您案例的更好解决方案)。

解决工厂问题很容易。事实上,你根本不需要工厂。只需使用Soda类的构造函数。