如果子类不知道如何注入依赖项到基类中,该怎么办?

3

我有一个抽象基类Command,它依赖于ICommandLogger接口:

public abstract class Command
{
    public Command(ICommandLogger cmdLogger) { /* ... */ }
}

现在所有的继承者看起来都像这样:

public class ConcreteCommand : Command
{
     public ConcreteCommand(CommandLoggersNamespace.ICommandLogger cmdLogger)
         : base(cmdLogger)
     {
     }
}

我不喜欢让他们被迫了解他们不使用的。如何避免这种情况?或者完全重新设计的原因是什么?

你可以退而使用属性注入。但是在我的看法中,构造函数注入更加清晰,因此是更好的选择。 - Maarten
2
但是你的ConcreteCommand确实知道日志记录器。只是这种行为是继承而来的。 - dcastro
1
这只是一个观念问题。如果司机知道他们的车辆对燃料的依赖性,即使他们自己不使用它,这听起来会很奇怪吗? - Jon
@Jon 服务定位器!谢谢! - astef
1
@Steven 在你的回答之后,我放弃了这个想法。 - astef
显示剩余4条评论
5个回答

7
你的设计存在问题,导致你遇到了这些麻烦。首先,日志记录是一个横切关注点,你应该防止将其污染到类中。其次,如果让基类实现日志记录,那么下一个添加到日志记录器基类上的横切关注点是什么?一旦你开始添加另一个横切关注点,基类就会违反单一职责原则。你的基类最终会变成一个庞大且难以管理的类,有很多依赖和很多需要更改的原因。
相反,尝试将日志记录作为装饰器添加。但是,你的设计阻止你有效地这样做,因为你可能会有几十个具体命令,它们都需要自己的装饰器。但你的设计核心问题在于混合数据和行为。让命令只是包含一些数据(DTO)的类,将命令逻辑提取到其自己的类中,我们称之为命令处理程序
此外,让命令处理程序实现这个接口:
public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

这将会看起来像这样:
public class MoveCustomerCommand
{
    public Guid CustomerId;
    public Address NewAddress;
}

public class MoveCustomerCommmandHandler : ICommandHandler<MoveCustomerCommand>
{
    public void Handle(MoveCustomerCommand command)
    {
        // behavior here.
    }
}

这个设计的有趣之处在于,由于所有业务逻辑现在都隐藏在一个 简化接口 后面,而且这个接口是通用的,因此通过使用装饰器将处理程序包装起来,可以非常容易地 扩展系统的行为。例如,一个记录日志的装饰器:
public class LoggingCommandHandlerDecorator<TCommand> 
    : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decoratee;

    public LoggingCommandHandlerDecorator(
        ICommandHandler<TCommand> decoratee, ILog log)
    {
        this.decoratee = decoratee;
        this.log = log;
    }

    public void Handle(TCommand command)
    {
        this.log.Log("Executing " + typeof(TCommand).Name + ": " +
            JsonConvert.Serialize(command));

        try
        {
            this.decoratee.Handle(command);
        }
        catch (Exception ex)
        {
            this.log.Log(ex);
            throw;
        }
    }
}

由于这个LoggingCommandHandlerDecorator<TCommand>是通用的,因此可以包装在任何ICommandHandler<TCommand>周围。这允许您让消费者依赖于某些ICommandHandler<TCommand>(例如ICommandHandler<MoveCustomerCommand>),并且可以在不改变一行代码的情况下向所有业务逻辑添加横切关注点。
在大多数情况下,这将完全消除使用基类的需要。
您可以在这里阅读有关此类设计的更多信息。

2
非常深入地洞察了设计的真正问题。谢谢! - astef
您可能也会对这个相关的问答感兴趣。 - Steven

2

如果你正通过构造函数进行依赖注入,那么这是无法规避的。另一种选择是通过属性设置进行依赖注入。个人而言,我更喜欢构造函数方法,因为对我来说,它表明这个类需要这个依赖关系,而属性注入的依赖关系则表示可选依赖项。

如果您选择构造函数路线,并且您的基类需要许多依赖项,则可以通过创建聚合服务来缓解部分痛苦,以便基类只需要注入一个参数而不是许多参数。


1
+1 对于提及必需依赖项和可选依赖项。 - dcastro
那么,如果依赖项不是可选的(Command 真的需要日志记录器),你的意思是痛苦是不可避免的? - astef
如果您使用继承来模拟行为,那么这是不可避免的。由于在C#中构造函数不会被继承,因此您必须强制子类添加构造函数链。一个更“激进”的方法是使用组合而不是继承。这就是为什么我喜欢尽可能使用接口的原因。例如,您可以定义ICommand,然后ConcreteCommand可以实现它,但不能从Command继承。 - Jim Bolla

1
如果他们没有使用命令记录器,您可以尝试根本不设置任何内容,就像这样:

public ConcreteCommand()
    : base(null)
{
}

如果这个方法不起作用(抛出异常),您可以尝试实现一个phony命令记录器并将其实例化:
public ConcreteCommand()
    : base(new MyPhonyCommandLogger())
{
}

如果您不想使用虚假实例,请使用一个静态可用的实例:

public ConcreteCommand()
    : base(MyPhonyCommandLogger.Instance)
{
}

3
我认为OP的观点是继承者没有使用日志记录器,但基类确实使用了它。 - Maarten
记录器是基类的一个依赖项。在没有明确保证的情况下,子类如何知道是否会使用该依赖项? - Jon

1
另一个看待这个问题的方式是,ConcreteCommand 类确实依赖于 ICommandLogger。当它从 Command 派生时继承了它。
  • 因此,除了让ConcreteCommand代表其基类接受依赖项之外,没有其他合理的方法可以绕过您正在进行的操作——不要将其视为“ConcreteCommand有一个Command”,而应该更多地像“ConcreteCommand是一个Command

  • 考虑一下,如果你能够使基类依赖于ICommandLoggerConcreteCommand的构造中“悄悄溜过去”,那么你会如何处理想要覆盖基类日志记录行为的情况...

  • 如果您想提供“类似基类”的功能(base.Log("foo")等),并且绝对不想让ConcreteComand知道ICommandLogger,那么您总是可以切换到“有一个”类型的场景——其中Command只是ConcreteCommand的成员变量(在这种情况下,我认为这是一个愚蠢的方法!)


0
我不喜欢他们被迫了解 ICommandLogger(他们并没有使用它)。
好吧,实际上他们必须要知道;除非你在抽象类中实例化一个类型为 ICommandLogger 的对象并提供一个无参构造函数,否则你目前是在强制继承者了解它。

不,他们不使用它。我所说的“使用”是指调用它的成员。你所说的“使用”是什么意思? - astef

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接