用相同泛型类型的另一个依赖项注册装饰器

本文关键字:注册 依赖 另一个 泛型类型 | 更新日期: 2023-09-27 17:51:12

我想我偶然发现了简单注入器的RegisterDecorator()的一个怪癖。即使在最近的2.5.0中也会出现这种情况。我有一种情况,我想装饰一个封闭的泛型类型,例如ICommandHandler<MessageCommand>,装饰器(通过构造函数注入)采用ICommandHandler<MessageCommand>类型的内部处理程序,但也有另一种类型的处理程序,说ICommandHandler<LogCommand>。尽管这些命令处理程序类型是不同的,但当我在这样的装饰器类型上调用RegisterDecorator时,SimpleInjector似乎会混淆并抛出异常:

ArgumentException:对于能够使用MessageLogger作为装饰器的容器,其构造函数必须包含一个类型为ICommandHandler<MessageCommand>(或Func<ICommandHandler<MessageCommand>>)的参数-即正在装饰的实例的类型。参数类型ICommandHandler<MessageCommand>在MessageLogger类的构造函数中定义了多次。

…尽管装饰器显然只有一个ICommandHandler<MessageCommand>参数。

下面是抛出异常的完整工作示例:
public interface ICommandHandler<T>
{
    void Execute(T command);
}
public class LogCommand
{
    public string LogMessage { get; set; }
    public DateTime Time { get; set; }
}
public class Logger : ICommandHandler<LogCommand>
{
    public void Execute(LogCommand command)
    {
        Debug.WriteLine(string.Format("Message '"{0}'" sent at {1}",
            command.LogMessage, command.Time));
    }
}

public class MessageCommand
{
    public string Message { get; set; }
}
public class MessageSender : ICommandHandler<MessageCommand>
{
    public void Execute(MessageCommand command)
    {
        Debug.WriteLine(command.Message);
    }
}
// message command handler decorator that logs about messages being sent
public class MessageLogger : ICommandHandler<MessageCommand>
{
    private ICommandHandler<MessageCommand> innerHandler;
    private ICommandHandler<LogCommand> logger;
    // notice these dependencies are two distinct closed generic types
    public MessageLogger(ICommandHandler<MessageCommand> innerHandler,
        ICommandHandler<LogCommand> logger)
    {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }
    public void Execute(MessageCommand command)
    {
        innerHandler.Execute(command);
        var logCommand = new LogCommand
            {
                LogMessage = command.Message,
                Time = DateTime.Now
            };
        logger.Execute(logCommand);
    }
}
// this works as intended, but is tedious in a real-world app
ICommandHandler<MessageCommand> ResolveManually()
{
    ICommandHandler<MessageCommand> sender = new MessageSender();
    ICommandHandler<LogCommand> logger = new Logger();
    ICommandHandler<MessageCommand> loggerSender =
        new MessageLogger(sender, logger);
    return loggerSender;
}
// this is what I want to work - seems simple?
ICommandHandler<MessageCommand> ResolveWithSimpleInjector()
{
    var container = new Container();
    container.Register<ICommandHandler<LogCommand>, Logger>();
    container.Register<ICommandHandler<MessageCommand>, MessageSender>();
    // this next line throws the exception
    container.RegisterDecorator(typeof(ICommandHandler<MessageCommand>),
        typeof(MessageLogger));
    return container.GetInstance<ICommandHandler<MessageCommand>>();
}
void Main()
{
    //ICommandHandler<MessageCommand> sender = ResolveManually();
    ICommandHandler<MessageCommand> sender = ResolveWithSimpleInjector();
    var command = new MessageCommand { Message = "Hello World!" };
    sender.Execute(command);
}

我找不到任何关于这种情况的信息。这是一个错误,还是我错过了什么?

编辑

我正在寻找来自SimpleInjector开发人员的反馈,以找出是否存在这种限制的技术原因,或者它是被忽视的东西……除非有人能让我相信这种设计存在逻辑缺陷,并且有一个很好的理由我不应该这样做,但迄今为止没有任何答案能做到这一点。我非常感谢你的反馈。

在我看来,核心问题是RegisterDecorator()将两个不同的封闭泛型类型视为同一类型。这可能是基于其内部工作原理的技术原因,但也可能不是?

用相同泛型类型的另一个依赖项注册装饰器

我必须在代码库中做一些调查,看看发生了什么。你可能会称这是Simple Injector实现中的一个小故障,但在我看来这是一个公平的权衡。Simple Injector的装饰器子系统是基于使用开放泛型类型和开放泛型装饰器的思想。它在装饰器注册时所做的检查是看一个装饰器的构造函数是否只有一个装饰者。这种检查是使用必须应用装饰器的开放泛型抽象来完成的;在你的例子中是ICommandHandler<T>。由于此时只有泛型ICommandHandler<T>可用,因此有两个构造函数参数匹配该类型。

有可能改进这些前置条件检查,但这实际上是相当讨厌的,而这个特性的有用性非常有限。它是有限的,因为它只对非泛型装饰器有用。看一下下面的装饰器:

public class GenericDecorator<TCommand> : ICommandHandler<TCommand> {
    public GenericDecorator(
        ICommandHandler<TCommand> decoratee,
        ICommandHandler<LoggingCommand> dependency)
    {
    }
}

这个装饰器是通用的,允许你将它应用到任何装饰器上,这更有用。但是当您解析ICommandHandler<LoggingCommand>时会发生什么?这将导致循环依赖图,而简单注入器(显然)将无法创建该图,并将抛出异常。它必须抛出,因为在这种情况下装饰器将有两个ICommandHandler<LoggingCommand>参数。第一个是被装饰的,将被注入你的Logger,第二个是一个普通的依赖,将被注入一个GenericDecorator<LoggingCommand>,这当然是递归的。

所以我认为问题出在你的设计上。一般来说,我建议不要将命令处理程序与其他命令处理程序组合在一起。ICommandHandler<T>应该是位于业务层之上的抽象,它定义了表示层如何与业务层通信。它不是供业务层内部使用的机制。如果您开始这样做,您的依赖项配置将变得非常复杂。下面是一个使用DeadlockRetryCommandHandlerDecorator<T>TransactionCommandHandlerDecorator<T>的图表示例:
new DeadlockRetryCommandHandlerDecorator<MessageCommand>(
    new TransactionCommandHandlerDecorator<MessageCommand>(
        new MessageSender()))

在本例中,DeadlockRetryCommandHandlerDecorator<T>TransactionCommandHandlerDecorator<T>应用于MessageSender命令处理程序。但是看看发生了什么,我们也应用了你的MessageLogger装饰器:

new DeadlockRetryCommandHandlerDecorator<MessageCommand>(
    new TransactionCommandHandlerDecorator<MessageCommand>(
        new MessageLogger(
            new MessageSender(),
            new DeadlockRetryCommandHandlerDecorator<MessageLogger>(
                new TransactionCommandHandlerDecorator<MessageLogger>(
                    new Logger())))))

注意对象图中有第二个DeadlockRetryCommandHandlerDecorator<T>和第二个TransactionCommandHandlerDecorator<T>。在事务中拥有事务并(在事务中)拥有嵌套死锁重试是什么意思?这可能会导致应用程序出现严重的可靠性问题(因为数据库死锁将导致您的操作在无事务的连接中继续)。

尽管可以创建这样的装饰器,它们能够检测到它们是嵌套的,以便在它们嵌套的情况下使它们正确工作,但这使得实现它们更加困难和脆弱。在我看来,这是在浪费你的时间。

因此,不允许命令处理程序被嵌套,而是让命令处理程序和命令处理程序装饰符依赖于其他抽象。在您的情况下,这个问题可以很容易地通过改变您的装饰器来解决,让它使用某种ILogger接口:

public class MessageLogger : ICommandHandler<MessageCommand> {
    private ICommandHandler<MessageCommand> innerHandler;
    private ILogger logger;
    public MessageLogger(
        ICommandHandler<MessageCommand> innerHandler, ILogger logger) {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }
    public void Execute(MessageCommand command) {
        innerHandler.Execute(command);
        logger.Log(command.Message);
    }
}

你仍然可以有一个ICommandHandler<LogCommand>实现的情况下,表示层需要直接记录,但在这种情况下,实现可以简单地依赖于ILogger,以及:

public class LogCommandHandler : ICommandHandler<LogCommand> {
    private ILogger logger;
    public LogCommandHandler(ILogger logger) {
        this.logger = logger;
    }
    public void Execute(LogCommand command) {
        logger(string.Format("Message '"{0}'" sent at {1}",
            command.LogMessage, DateTime.Now));
    }
}

这是一种极端情况,你可以用任何一种方式来争论,但事实是Simple Injector明确地不支持你想要做的事情。

装饰器通常需要在所有(或部分)特定抽象中应用公共逻辑,在您的示例中是ICommandHandler。换句话说,MessageLogger被设计用来装饰ICommandHandler,因为它是ICommandHandler的装饰器,它只能在它的构造函数中使用一个ICommandHandler。此外,允许这样的事情将需要大量可怕的循环检查,而这最好通过更干净的设计来避免!

因此,您通常会定义一个具有与它所装饰的类型相同接口(和泛型参数)的装饰器
public class MessageLogger<TCommand> : ICommandHandler<TCommand>
    where TCommand : <some criteria e.g. MessageCommand>
{
    //....
}

我能想到的缓解问题的第一个解决方案是创建一个中介来消除直接依赖:

public class LoggerMediator
{
    private readonly ICommandHandler<LogCommand> logger;
    public LoggerMediator(ICommandHandler<LogCommand> logger)
    {
        this.logger = logger;
    }
    public void Execute(LogCommand command)
    {
        this.logger.Execute(command);
    }
}

并更改您的MessageLogger以使用中介。

public class MessageLogger<TCommand> : ICommandHandler<TCommand>
    where TCommand : MessageCommand
{
    private ICommandHandler<TCommand> innerHandler;
    private LoggerMediator logger;
    public MessageLogger(
        ICommandHandler<TCommand> innerHandler,
        LoggerMediator logger)
    {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }
    public void Execute(TCommand command)
    {
        innerHandler.Execute(command);
        var logCommand = new LogCommand
        {
            LogMessage = command.Message,
            Time = DateTime.Now
        };
        logger.Execute(logCommand);
    }
}

顺便说一句,你可以像这样简化你的注册

var container = new Container();
container.RegisterManyForOpenGeneric(
    typeof(ICommandHandler<>), 
    typeof(ICommandHandler<>).Assembly);
container.Register<LoggerMediator>();
container.RegisterDecorator(typeof(ICommandHandler<>), typeof(MessageLogger<>));
container.Verify();

查看我的代码库,我发现我有一个类似的需求,我用一个额外的类解决了它——一个通用的命令中介:

public class CommandHandlerMediator<TCommand>
{
    private readonly ICommandHandler<TCommand> handler;
    public CommandHandlerMediator(ICommandHandler<TCommand> handler)
    {
        this.handler = handler;
    }
    public void Execute(TCommand command)
    {
        this.handler.Execute(command);
    }
}

注册如下:

container.RegisterOpenGeneric(
    typeof(CommandHandlerMediator<>), 
    typeof(CommandHandlerMediator<>));

和如下引用:

public class MessageLogger<TCommand> : ICommandHandler<TCommand>
    where TCommand : <some criteria e.g. MessageCommand>
{
    private ICommandHandler<TCommand> decorated;
    private CommandHandlerMediator<LogCommand> logger;
    public MessageLogger(
        ICommandHandler<TCommand> decorated,
        CommandHandlerMediator<LogCommand> logger)
    {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }
    //....
}

一个新的类,你为所有的处理程序排序。

你可以把你的Decorator变量改成

public MessageLogger(ICommandHandler<MessageCommand> innerHandler)
{
    this.innerHandler = innerHandler;
}

与"ICommandHandler(或Func>)类型的单个参数"匹配所需的tor签名。注入logger作为属性,而不是作为参数。我没有使用简单注入器,但看看你的异常消息,这是最明显的解决方案,因为装饰器构造函数签名的限制。

你的解决方案似乎有点尴尬,因为它是一个装饰器和构造器注入/组合(什么的)的组合。虽然这不是你问题的确切答案,但它可能会解决你的问题(我认为是以一种更好的方式):

public class LoggingHandler : ICommandHandler<MessageCommand>
{
    private ICommandHandler<MessageCommand> innerHandler;
    public LoggingHandler(ICommandHandler<MessageCommand> innerHandler)
    {
        this.innerHandler = innerHandler;
    }
    public void Execute(MessageCommand command)
    {
        innerHandler.Execute(command);
        Debug.WriteLine(string.Format("Message '"{0}'" sent at {1}",
        command.Message, DateTime.Now));
    }
}

我不认为需要一个单独的CommandHandler为LogMessage。您可以登录到装饰实际命令处理程序的对象中。否则它的目的是什么?

使用这种方法,你有一个纯装饰器,这是一个更好的解决方案,因为它节省了你两个额外的类。