首页 > 解决方案 > 装饰多个实现的接口之一

问题描述

我想知道是否可以在 C# 中为多个已实现接口中的 1 个提供装饰器。我倾向于不,但也许。

这就是我的意思

public abstract class Auditable
{
    public string CreatedBy { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime ModifiedAt { get; set; }
    public string ModifiedBy { get; set; }
}

public class MyClass : Auditable
{
  // <...> properties
}


public interface IWriteRepository<T> : where T : Auditable
{
    T Create(T entity);
    T Update(T entity);
}

public class AuditRepositoryDecorator<T> : IWriteRepository<T> where T : Auditable
{
    private readonly IWriteRepository<T> _decorated;

    // <...> ctor with injects

    public T Create(T entity)
    {
        entity.ModifiedAt = time;
        entity.CreatedAt = time;
        entity.CreatedBy = invoker;
        entity.ModifiedBy = invoker;

        return _decorated.Create(entity);
    }

    public T Update(T entity)
    {
        entity.ModifiedAt = time;
        entity.ModifiedBy = invoker;

        return _decorated.Update(entity);
    }
}


public interface IMyClassRepository : IWriteRepository<MyClass>
{
     MyClass Get(int id);
}

所以我希望能够依赖IMyClassRepository存储库,并且无论何时CreateUpdate将被调用它都会通过AuditRepositoryDecorator. 这是一段被执行很多次的逻辑,我认为作为装饰器而不是与某些执行相同操作的接口建立组合关系会简单得多。

IAuditableRepository永远不会直接实例化,因为它总是由另一个接口实现,所以我认为可能无法做到我想要实现的目标。

我正在使用带有Scrutor的默认 dnc2.1 DI 框架进行装饰。

标签: c#design-patternsdependency-injectiondecorator

解决方案


你想要达到的目标是不可能的。这不是使用的 DI 容器的限制,而是 .NET 类型系统的限制。我经常建议遇到 DI 麻烦的开发人员,为了便于理解,从等式中删除 DI 容器,而是手动构建对象图。正如我将在下面演示的那样,这在您的情况下效果很好。

假设您有一个IMyClassRepository消费者:

public class RepoConsumer
{
    RepoConsumer(IMyClassRepository repo) ...
}

和一个IMyClassRepository实现:

public class MyClassRepositoryImpl : IMyClassRepository
{
    ...
}

现在让我们为RepoConsumer它创建对象图AuditRepositoryDecorator<MyClass>

var repo = new MyClassRepositoryImpl();
var decoratedRepo = new AuditRepositoryDecorator<MyClass>(repo);
var consumer = new RepoConsumer(decoratedRepo); // <-- COMPILE ERROR

When you compile this code, you'll notice that the C# compiler will generate an error on the new RepoConsumer line. This is because RepoConsumer expects an IMyClassRepository. Although MyClassRepositoryImpl implements IMyClassRepository, AuditRepositoryDecorator<MyClass> does not implement IMyClassRepository.

To solve this, you might try letting AuditRepositoryDecorator<T> implement IMyClassRepository, but that will obviously be ugly, because the decorator will have to implement a dozen of interfaces, for each entity in your system.

But what this exercise proves, is that the problem is not so much with the DI Container, but rather that the type system simply not permits you to build an object graph of this. And since the type system doesn't allow you to, the DI Container certainly won't allow it. It can't work around the type checks of the type system. Fortunately.

But the solution to your problem is actually really straightforward: remove the specific IMyClassRepository and let consumers depend on IWriteRepository<MyClass> instead. This might seem a disappointing solution, but there is a myriad of problems surrounding deriving from generic interfaces. Just accept the fact that consumers depend on such generic abstraction. It takes some time, but eventually, you will start to love and appreciate this style of programming.

But, of course, this still leaves us with the question of how to add new methods, such as MyClass Get(string). There are multiple solutions, such as:

  • Implement it as extension method (only possible when the method itself requires access to the interface itself, not to the class's internals)
  • Define a separate interface, which might be a good idea in general, according to the Interface Segregation Principle

推荐阅读