去掉基础中的 ServiceLocator class

Getting rid of the ServiceLocator in the base class

我有 command/query 个处理程序的抽象基础 class:

public abstract class AbstractBusiness<TRequest, TResult> : IBusiness<TRequest, TResult>
    where TResult : BaseResponse
{
    public TResult Send(TRequest request)
    {
        TResult response = Activator.CreateInstance<TResult>();
        response.Success = true;

        try
        {
            IConnectionProvider connectionProvider =
                ServiceLocator.Current.GetInstance<IConnectionProvider>();

            using (DatabaseScope scope = DatabaseScopeManager.Instance.GetScope(
                connectionProvider, 
                DatabaseScopeType.UseExistingOrNewTX,
                IsolationLevel.ReadCommitted))
            {
                response = this.Handle(request);
                scope.Complete();
            }
        }
        catch (Exception exc)
        {
            response.Success = false;
            response.Message = exc.Message;
        }

        return response;
    }

    protected abstract TResult Handle(TRequest request);
}

它是这样使用的:

public sealed class AccountCreateBusiness
    : AbstractBusiness<AccountCreateRequest, AccountCreateResponse>
{
    private readonly IMessageProvider messageProvider;

    public AccountCreateBusiness(IMessageProvider messageProvider)
    {
        this.messageProvider = messageProvider;
    }

    protected override AccountCreateResponse Handle(AccountCreateRequest request)
    {
        //handle request
    }
}

我正试图摆脱基地 class 中的 ServiceLocator。有没有一种方法可以注入 IConnectionProvider 而不必更改所有派生 classes 以在其构造函数中包含 IConnectionProvider 并调用 : base(connectionProvider)?我无法更改 DatabaseScopeManager

还有一个替代方案:属性 注入

顺便说一句,属性 注入 没有定义强制依赖项,而构造函数依赖项定义了它。

正如 Matías Fidemraizer 所提到的,您可以使用 属性 注入,这是避免破坏性更改的一种非常安全的方法。缺点是 optional 中的 属性 依赖项,但在您的代码中 IConnectionProvidera must.

从长远的 运行 角度来看,我会选择两个构造函数:

  1. 默认的 仍然 使用服务定位器(为了向后兼容)但被标记为 [Obsolete] 以指示它将在下一个中删除主要版本。
  2. IConnectionProvider 作为 must-have 依赖项的构造函数。应该推荐使用这个,一旦所有默认构造函数的用法都被淘汰,应该会成为唯一一个。

这里问题的核心是你的基地的存在class。 你应该摆脱底座 class。 移除底座 class 将完全解决你的问题。

基数 class 有问题,因为:

  • 它导致每个派生类型都依赖于此 class。
  • 它使测试复杂化,因为所有 cross-cutting 问题总是被执行。
  • 基地class将成为一个不断增长的大class;单一责任违规。

虽然您可以使用 属性 注入来摆脱服务定位器,但这并没有消除所述问题,甚至引入了一个新问题,即:Temporal Coupling.

相反,您应该简单地摆脱基础 class 并将这些 cross-cutting 问题移到装饰器中。对于您的情况,我建议创建两个装饰器。

一个TransactionBusinessDecorator:

public class TransactionBusinessDecorator<TRequest, TResult>
    : IBusiness<TRequest, TResult>
    where TResult : BaseResponse
{
    private readonly IConnectionProvider connectionProvider;
    private readonly IBusiness<TRequest, TResult> decoratee;

    public TransactionBusinessDecorator(
        IConnectionProvider connectionProvider,
        IBusiness<TRequest, TResult> decoratee)
    {
        this.connectionProvider = connectionProvider;
        this.decoratee = decoratee;
    }

    public TResult Send(TRequest request)
    {
        TResult response;

        using (DatabaseScope scope = DatabaseScopeManager.Instance.GetScope(
            this.connectionProvider, 
            DatabaseScopeType.UseExistingOrNewTX,
            IsolationLevel.ReadCommitted))
        {
            response = this.decoratee.Handle(request);
            scope.Complete();
        }

        return response;        
    }
}

还有一个ExceptionWrapperBusinessDecorator:

public class ExceptionWrapperBusinessDecorator<TRequest, TResult>
    : IBusiness<TRequest, TResult>
    where TResult : BaseResponse
{
    private readonly IBusiness<TRequest, TResult> decoratee;

    public ExceptionWrapperBusinessDecorator(
        IBusiness<TRequest, TResult> decoratee)
    {
        this.decoratee = decoratee;
    }

    public TResult Send(TRequest request)
    {
        TResult response = Activator.CreateInstance<TResult>();

        try
        {
            var response = this.decoratee.Handle(request);
            response.Success = true;
        }
        catch (Exception ex)
        {
            response.Success = false;
            response.Message = exc.Message;
        }

        return response
    }
}

这使得AccountCreateBusiness可以写成如下:

public sealed class AccountCreateBusiness 
    : IBusiness<AccountCreateRequest, AccountCreateResponse>
{
    private readonly IMessageProvider messageProvider;

    public AccountCreateBusiness(IMessageProvider messageProvider)
    {
        this.messageProvider = messageProvider;
    }

    public AccountCreateResponse Handle(AccountCreateRequest request)
    {
        //handle request
    }
}

用装饰器包装每个处理程序对于大多数 DI 容器来说是微不足道的,并且当手动执行此操作时(a.k.a。纯 DI)。

是最后的设计建议。捕获每个异常并将此信息作为消息的一部分 return 通常不是一个好主意,因为:

  • 这些错误代码正在泄漏到与其无关的系统基础(您的业务层)。不要在基本响应中提供此信息,而是使用包含此信息的信封。
  • 您将向客户提供技术信息。客户通常对这种详细程度不感兴趣。您只想 return 功能消息。
  • 安全敏感信息已 returned;黑客喜欢这种信息。
  • 如果您不记录此信息,就会丢失很多重要的错误详细信息。
  • 基本上 return 错误代码,而不是处理异常。错误代码很容易出错;客户端需要经常检查结果以查看响应是否成功。