9

我目前正在尝试了解使用 IoC 容器的好处并熟悉 DI。我已经开始使用 StructureMap,因为它看起来相当简单但功能强大。

我想验证我对这些概念的理解是否正确。让我们假设应用程序中有以下基本类(为简洁起见省略了细节):

public class OrderService : IOrderService
{
    private IOrderRepository _repository;
    private ITaxCalculator _taxCalculator;
    private IShippingCalculator _shippingCalculator;

    public OrderService(IOrderRepository repository, 
        ITaxCalculator taxCalculator, 
        IShippingCalculator shippingCalculator)
    {
        this._repository = repository;
        this._shippingCalculator = shippingCalculator;
        this._taxCalculator = taxCalculator;
    }

    public IOrder Find(int id) { return this._repository.Find(id); }
}

public class OrderRepository : IOrderRepository
{
    public IOrder Find(int id) { // ... }
}

public class StandardShippingCalculator : IShippingCalculator
{
    // ...
}

public class StandardTaxCalculator : ITaxCalculator
{
    private ITaxSpecification _specification;

    public StandardTaxCalculator(ITaxSpecification specification)
    {
        this._specification = specification;
    }
}

首先,依赖倒置原则指出,由于 OrderService 是一个“高级”模块,它不应该依赖于任何较低级别的实现细节,它应该只引用这些类并能够要求它们做他们的事情而不必知道在做什么,消费代码应该负责创建这些预配置的模块并将其交给它。是对的吗?因此,DI 使这些类保持松散耦合,因此它们不必知道调用该依赖项的方法的确切方式,只需知道它被调用并做它需要做的任何事情——OrderService 不关心存储库是否正在查询 XML,或者使用 NHibernate 或 EF 甚至原始数据集;它只知道它可以调用存储库,告诉它找到一个 ID 为 42 的订单,存储库就会知道该做什么。

我的理解也是,一个 IoC 容器,在这种情况下是 StructureMap,它提供了一个好处,它不会强迫我们确保我们手动创建所有这些依赖项并将它们传递进来。例如,一个普通应用程序的 Main 方法使用上面的代码可能有:

static void Main(string[] args)
{
    IOrderService service = new OrderService(
        new OrderRepository(), new StandardShippingService(), 
        new StandardTaxService(new StandardTaxSpecification()));

    IOrder order = service.Find(42);

    // Do something with order...
}

加上所有的新闻来设置它,这真是令人作呕;即使我创建了变量,它仍然很丑。使用 IoC 容器让我避免了所有这些,在 StructureMap 的情况下,它会变成:

static void Main(string[] args)
{
    ObjectFactory.Initialize(x =>
    {
        x.For<IOrderRepository>().Use<OrderRepository>();
        x.For<IOrderService>().Use<OrderService>();
        x.For<IOrder>().Use<Order>();
        x.For<IShippingCalculator>()
                        .Use<StandardShippingCalculator>();
        x.For<ITaxCalculator>().Use<StandardTaxCalculator>();
        x.For<ITaxSpecification>()
                        .Use<StandardTaxSpecification>();
    });

    IOrderService service =
                ObjectFactory.GetInstance<IOrderService>();
    IOrder order = service.Find(42);
    // do stuff with order...
}

这更清洁,更易于维护,如果我正在编写单元测试,我只需将具体类替换为 Mock 即可。简而言之,它的好处是它把所有东西都解耦到我什至不需要关心的地方(在调用代码中,即)一个特定的类依赖什么,我可以使用容器创建一个并让它做确保消费代码只知道它需要什么——例如,在一个真实的应用程序中,如果控制器正在调用服务,它不需要知道存储库、计算器或规范,它只需要知道使用OrderService 对订单做一些事情。

这个理解正确吗?在那张纸条上,有几件事我还不确定:

  1. 如果您决定使用 IoC 容器,它是否意味着在应用程序中的任何地方都可以使用,仅在您需要解决许多反向依赖项的地方,还是仅在消费者中使用?例如,在 OrderRepository 中,如果我使用具体的实现并新建一个 Order;此类是否也会使用 StructureMap 来获取订单?这可能是一个有点傻的问题,但我看到的所有 DI/IoC 示例都只关注在消费客户端(例如网页)中使用它,而从未涉及在其他地方使用它。这似乎是一种全有或全无的方法:如果您要使用 IoC 容器,那么它会在任何地方使用;new SomeObject();在这种情况下,您实际上是将任何调用替换为ObjectFactory.GetInstance<ISomeObject>();

  2. 无论是否需要使用 DI/IoC 或类似模拟它的东西,让每个类(当然,在可能的情况下)都从接口派生是好还是坏?我见过许多代码示例,其中每个不是内置类的类背后都有一个接口,虽然我可以看到这样做的好处和可能的未来证明,但我认为遵循 TDD 或 BDD 可能是一个使用这些方法的因素通常会告诉您是否需要类的接口,但是我见过并与许多人交谈过,无论是否使用 TDD,都认为您永远不应该将对象的类型定义为具体的类;它应该总是是作为底层类型的接口或抽象类。这似乎是“不必要的复杂性”代码气味的一个案例,更不用说违反 YAGNI 了。

4

2 回答 2

5

您的两个问题都涉及有争议的话题,但我会站在辩论的一边。

如果您决定使用 IoC 容器,它是否意味着在应用程序中的任何地方都可以使用,仅在您需要解决许多反向依赖项的地方,还是仅在消费者中使用?

您的顶级应用程序(消费者)是唯一应该了解您的依赖注入框架的组件。您不需要在new整个代码库中进行替换,因为每个对象都应该具有完成其工作所需的所有依赖实例(Miško Hevery 的“依赖注入神话:引用传递”最终让我明白了这一点)。

无论是否需要使用 DI/IoC 或类似模拟它,让每个类(当然,在可能的情况下)都从接口派生是好还是坏?

这个问题的其余部分表明您已经知道这个问题的答案:只构建接口以创建更合适的抽象(比所讨论的具体类)或提供一些其他值。

于 2011-01-05T20:16:36.203 回答
2

我目前正在 VisualStudio2010/12 上的 C#/WinForm 中深入使用 DI/IoC。我的选择落在 Castle Windsor 上,也落在 StructureMap 上,但你使用的 IoCC 并不重要。

要获得非常详细的答案,我建议您阅读 Mark Seemann 的“.NET 中的依赖注入”。即使您不使用 .NET 进行开发,这也是一本好书。

关于你的问题:

  1. DI CONTAINER 是一个库,你可以在任何你想使用的地方使用它——但这并不意味着你应该这样做。尽管您可以分散容器的使用以使其渗透到大部分类中,但您应该将其集中到应用程序的单个区域中。

    这个地方叫做 COMPOSITION ROOT,你应该只使用那个地方的 DI CONTAINER。应用程序的 COMPOSITION ROOT 应该位于应用程序的根目录中,以便它可以正确地组合应用程序。您不应尝试在任何模块中编写类,因为这种方法会限制您的选择。应用程序模块中的所有类都应使用 CONSTRUCTOR INJECTION(或者,在极少数情况下,使用其他模式之一,如属性注入)并将其留给 COMPOSITION ROOT 来组成应用程序的对象图。任何 DI CONTAINER 调用都应限于 COMPOSITION ROOT。

    COMPOSITION ROOT 可以分布在几个类中。这是意料之中的——重要的是所有类都包含在同一个模块中,最好是应用程序根。

  2. 您不需要在任何地方都使用接口。您还可以使用具体类。当然,接口提供了更高级别的抽象,但是您必须考虑项目中是否需要。例如,在应用程序的业务逻辑中使用接口是一种很好的做法。

于 2012-08-31T09:30:37.213 回答