如何避免在单向 api 设计中传递 DI 容器?

How do you avoid passing a DI container in a unidirectional api design?

我有一个业务层,其中包含使用 Active Record 设计的业务实体,以及一个单向 api 表面。我有两个不同的问题:

Product 是包装容器并充当我们的应用程序外观的根,也是 BAL 其余部分的入口点。我要解决的问题是 Product.FindCustomerCustomer.FindDocument

public class Product
{
    private IUnityContainer container;

    public void RegisterType<T>() ...
    public void RegisterType<TFrom, TTo>() ...

    public Customer FindCustomer(string customerNumber)
    {
        var id = context.Customers
                        .Where(p => p.CustomerNumber == customerNumber)
                        .Select(p => p.Id)
                        .Single();

        var customer = container.Resolve<Customer>(...); // param override?

        customer.Load();

        return customer;
    }
}

public class Customer : BusinessEntity<Data.Customer, Guid>
{
    private readonly IDocumentFileProvider provider;

    public Customer(IDataContext context, IDocumentFileProvider provider) : base(context)
    {
        this.provider = provider;
    }

    public Customer(IDataContext context, IDocumentFileProvider provider, Guid id) : base(context, id)
    {
        this.provider = provider;
    }

    public Document FindDocument(string code)
    {
        var id = context.Documents
                        .Where(p => p.CustomerNumber == customerNumber)
                        .Select(p => p.Id)
                        .Single()

        var document = new Document(context, provider, id); // Here is the issue

        document.Load();

        return document;
    }
}

public class Document : BusinessEntity<Data.Document, Guid>
{
    public Document(IDataContext context, IDocumentFileProvider provider) : base(context)
    {
        this.provider = provider;
    }

    public Document(IDataContext context, IDocumentFileProvider provider, Guid id) : base(context, id)
    {
        this.provider = provider;
    }

    public IDocumentFile GetFile()
    {
        return provider.GetFile();
    }
}

这是其他 class 的简要说明。

public abstract class ActiveRecord<TEntity, TKey>
{
    protected ActiveRecord(IDataContext context)
    {
    }

    public virtual void Load() ...
    public virtual void Save() ...
    public virtual void Delete() ...
}

public abstract class BusinessEntity<TEntity, TKey> : ActiveRecord<TEntity, TKey>
{
    protected BusinessEntity(IDataContext context) : base(context)
    {
    }

    protected BusinessEntity(IDataContext context, TKey id) : this(context)
    {
    }

    ...
}

层次结构可能很深,但举个较短的例子:

var customer = product.FindCustomer("123");
var account  = customer.FindAccount("321");
var document = account.FindDocument("some_code");
var file     = document.GetFile();

我的目标之一是 A) 对领域建模,以及 B) 提供一个非常容易理解的 API。目前我们的 BAL 使用服务定位器,但我正在尝试用适当的 IoC/DI 和容器替换它。

API 越深,需要的依赖越多,所有更高层的 class 构造函数可能会很长,而且看起来可能不再具有内聚性。

虽然可以使用折衷措施将 DI 压缩到大多数应用程序设计中,但不幸的是,并非所有应用程序设计都特别适合 DI。就 API 设计而言,创建 "smart entities" 似乎很神奇,但事实是,从根本上讲,它们违反了 SRP(加载和保存是独立的职责,无论您如何划分它)。

你基本上有 4 个选项:

  1. 找到一个更有利于 DI 的设计,并将其对象模型用于您的 API
  2. 寻找更有利于 DI 的设计,并为您的 API
  3. 创建外观对象模型
  4. 使用 属性 注入来加载您的依赖项并让最终用户控制构造函数
  5. 使用服务定位器

我 运行 在尝试将 CSLA 与 DI 结合使用时遇到了类似的墙,经过多次尝试,最终决定是 CSLA 需要去寻找更好的设计方法.

有一段时间,我尝试使用选项 3。在这种情况下,您可以围绕 DI 容器创建外观包装器,并且仅通过静态访问器公开其 BuildUp() 方法。这可以防止将容器用作服务定位器。

[Dependency]
public ISomeDependency SomeDepenency { get; set; }

public Customer()
{
    Ioc.BuildUp(this);
}

一些 DI 容器可以使用流畅的配置而不是属性来注入属性(因此您的业务模型不需要引用容器),但这会使 DI 配置非常复杂。另一种选择是构建您自己的属性。

选项 1 和 2 类似。您基本上将每项责任都归入其自己的 class 并将您的 "entities" 分离到哑数据容器中。一种适用于此的方法是使用 Command Query Segregation.

public class FindCustomer : IDataQuery<Customer>
{
    public string CustomerNumber { get; set; }
}

public class FindCustomerHandler : IQueryHandler<FindCustomer, Customer>
{
    private readonly DbContext context;

    public FindCustomerHandler(DbContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");
        this.context = context;
    }

    public Customer Handle(GetCustomer query)
    {
        return (from customer in context.Customers
                where customer.CustomerNumber == query.CustomerNumber
                select new Customer
                {
                    Id = customer.Id,
                    Name = customer.Name,
                    Addresses = customer.Addresses.Select(a =>
                        new Address
                        {
                            Id = a.Id,
                            Line1 = a.Line1,
                            Line2 = a.Line2,
                            Line3 = a.Line3
                        })
                        .OrderBy(x => x.Id)
                }).FirstOrDefault(); 
    }
}

使用选项 1,最终用户将创建 FindCustomer 的实例并调用 queryProcessor.Handle(findCustomer)(注入 queryProcessor)。

使用选项 2,您需要创建一个包装器 API。您可以使用 fluent builder approach (more info here) 来提供逻辑默认依赖项,但允许最终用户调用方法来提供自己的依赖项。

var customer = new CustomerBuilder().Build(); // defaults

var customer = new CustomerBuilder(c => 
    c.WithSomeDependency(new SomeDependency()).Build(); // overridden dependency

不幸的是,这个的主要问题是对象生命周期的控制不再取决于 DI 容器,因此像 DbContext 这样的依赖项需要特殊处理。

另一种变体是将每个实体变成一个不起眼的对象,该对象在内部使用其他(松散耦合的)API 对象构建自己的 DI 容器。对于难以与 DI 一起使用的遗留框架(例如 Web 表单),这是推荐的方法。

最后,创建一个静态服务定位器,您的所有 API 对象都使用它来解决它们的依赖关系。虽然这最好地实现了目标,但它应该被视为最后的手段。最大的问题是您无法快速轻松地理解 class 需要哪些依赖项。因此,您要么被迫创建(和更新)文档来指示最终用户的依赖项,要么最终用户将不得不深入挖掘源代码以找出答案。使用服务定位器是否可以接受取决于您的目标受众以及您希望他们能够自定义超出默认值的依赖项的频率。如果自定义依赖项是千载难逢的事情,它可能会起作用,但如果 25% 的用户群需要添加自定义依赖项,服务定位器可能不是正确的方法。

底线是,如果可维护性是您的主要目标,那么选项 1 无疑是赢家。但是,如果您选择了这种特定的 API 设计,您将需要选择其他选项之一,并忍受支持这种 API.

所涉及的额外维护。

参考文献: