【问题标题】:How to use dependency injection with inheritance in C#如何在 C# 中使用依赖注入和继承
【发布时间】:2018-06-11 07:13:57
【问题描述】:

简介

大家好,我目前正在使用 C# 开发一个持久性库。在那个库中,我实现了我面临 SOLID 问题的存储库模式。 这是一个简化的示例,我当前的实现重点关注基本要素:

包含在持久性库中的抽象存储库:

public abstract class Repository<T> 
{
    protected Repository(
        IServiceA serviceA,
        IServiceB serviceB) 
    {
        /* ... */
    }
}

库用户创建的具体存储库:

public class FooRepository : Repository<Foo> 
{
    protected FooRepository(
        IServiceA serviceA,
        IServiceB serviceB) :
        base(serviceA, serviceB)
    {
        /* ... */
    }
}

问题

好的,使用当前代码,派生类必须知道基类的每个依赖项都可以,但是如果我向基类添加依赖项怎么办?每个派生类都会中断,因为它们需要将新的依赖项传递给基类......所以目前,我仅限于永远不会更改基类构造函数,这是一个问题,因为我想要我的基类有进化的可能。 这个实现明显打破了Open/Closed Principle,但是我不知道如何在不打破SOLID的情况下解决这个问题......

要求

  • 该库应该易于用户使用
  • 具体的存储库应该能够通过 DI 构建
  • 应将一个或多个依赖项添加到抽象存储库,而不影响派生存储库
  • 应该可以使用命名约定在 DI 容器中注册每个存储库,就像 ASP.NET MVC 框架 使用控制器
  • 如果需要,用户应该能够在他的派生存储库中添加更多依赖项

已经设想的解决方案

1。服务聚合器模式

按照这个article,服务聚合器模型可以应用于这种情况,所以代码看起来像这样:

包含在持久性库中的抽象存储库:

public abstract class Repository<T> 
{

    public interface IRepositoryDependencies
    {
        IServiceA { get; }
        IServiceB { get; }
    }

    protected Repository(IRepositoryDependencies dependencies) 
    {
        /* ... */
    }
}

库用户创建的具体存储库:

public class FooRepository : Repository<Foo> 
{
    protected Repository(IRepositoryDependencies dependencies) :
        base(dependencies)
    {
        /* ... */
    }
}

优点

  • 如果将依赖项添加到基类中,派生类不会中断

缺点

  • 如果我们添加依赖项,IRepositoryDependencies 接口的实现必须修改
  • article 没有解释如何使用 Castle DynamicProxy2(这对我来说是一个未知的技术)来动态生成服务聚合器

2。建造者模式

也许,可以删除基础存储库构造函数并引入构建器模板来创建存储库,但是要使此解决方案起作用,构建器必须是可继承的,以允许用户输入他的存储库自己的依赖项。

优点

  • 如果将依赖项添加到基类中,派生类不会中断
  • 存储库构建由另一个类管理

缺点

  • 用户必须为他想要创建的每个存储库创建一个构建器
  • 使用命名约定通过 DI 注册每个存储库变得越来越困难

3。属性注入

也许删除基础存储库构造函数并将 DI 配置为使用属性注入可能是一种选择。

优点

  • 如果将依赖项添加到基类中,派生类不会中断

缺点

  • 我认为属性设置器必须是公共的?

结论

在 SOLID 世界中是否有任何上述解决方案可以接受?如果没有,你们有解决方案吗?非常感谢您的帮助!

【问题讨论】:

  • 基类的新依赖有什么作用?如果基类需要一个新的依赖项,这表明它将承担额外的责任。 IOW - 如果没有额外的依赖,它会做什么以前无法做到的事情?难道新行为根本不属于那个类?
  • @ScottHannen 例如,如果我想添加一个简单的记录器。另外,我目前有一个通过工作单元注册存储库的依赖项,但是如果将来我不再需要它怎么办?让派生类知道基类的每一个依赖真的可以吗?
  • “这个实现显然打破了开放/封闭原则”我不确定我是否同意你的看法。对我来说,如果您需要对基类进行更改,那么派生类应该传递实例化基类所需的内容。如果派生类不需要依赖并且这是一个完全有效的情况,那么也许你应该创建一个额外的构造函数而不使用依赖。或者创建另一个基类,该基类可以从原始基类派生,并为需要该依赖项的派生类附加依赖项
  • 如果你的基类有多个构造函数,那么我作为用户会决定使用哪一个。如果我需要记录器,那么我将使用记录器调用构造函数。您只需要确保实现工作正常,并且该类对每个构造函数版本都很有用。将来,如果您需要添加额外的职责,那么您不应该这样做,因为那样您会破坏 SRP。
  • @AnkitVijay 好的,但是如果构建基类的方式发生了变化呢?可以在不添加或删除其职责的情况下更改类的构建方式,如果发生这种情况,每个派生类都必须更改?如果我添加一个依赖项,我可以添加一个构造函数,我理解这一点,你是完全正确的,但是如果所需的依赖项发生了变化怎么办?我应该永远不要更改所需的依赖项吗?

标签: c# inheritance dependency-injection solid-principles


【解决方案1】:

“我仅限于永远不会更改基类构造函数”的结论纯粹是证明 IoC 容器“模式”的严重性是多么的有害。

假设您有一个 asp 应用程序,并且希望能够为特定用户启用日志记录,就像他的每个会话进入新文件时一样。使用 IoC 容器/服务定位器/ASP 控制器构造函数是不可能实现的。您可以做什么:在每次会话开始时,您应该创建这样的记录器并准确地将其传递给所有构造函数(服务实现等越来越深入)。没有其他方法。 IoC 容器中没有“每个会话”生命周期之类的东西 - 但这只是实例自然应该存在于 ASP 中的一种方式(意味着多用户/多任务应用程序)。

如果您没有通过构造函数进行 DI,那么您肯定做错了什么(对于 ASP.CORE 和 EF.CORE 来说确实如此 - 不可能看到它们如何通过抽象泄漏来折磨每个人和他们自己:您能想象添加自定义记录器破坏了 DbContext https://github.com/aspnet/EntityFrameworkCore/issues/10420 这正常吗?

仅从 DI 获取配置或动态插件(但如果您没有动态插件,请不要考虑任何依赖项“因为它可能是动态插件”),然后执行所有 DI 标准经典方式 - 通过构造函数。

【讨论】:

  • 对不起,我不太明白你的提议是什么(也许是我的英语不好)?你告诉我不要考虑基类中可能发生的演变?您还在谈论 EF Core。 EF Core 做得好吗?我想知道在不调用基本构造函数时如何构建 EF Core DbContexts?例如如何收集连接字符串或 EF 提供程序?
  • ef core 显示错误的方式。我所说的只是通过构造函数传递所有 DI。如果你做不到——你肯定做错了什么。
  • 我当然可以,但是这个示例会打破你想象中的规则“图书馆应该易于用户使用”——你决定什么容易什么不容易(而且它不是中立的)。我尝试通过在您的脑海中播种不同的想法来回答:想象您的所有对象(记录器和依赖于记录器的所有对象)都需要“每个会话”生存。你会怎么做?
  • 我希望用户从基本存储库继承,也许还有一些简单的配置,就是这样。用户不应该编写太多代码来创建存储库。如果我提到 EF,则可以在选项中更改记录器工厂和内部服务提供者。因此,如果我们愿意,可以将一些“每个会话”服务传递给 DbContext 吗?为什么做这样的事情不好?我的意思是这些选项是完全可配置的?
  • 在 EF6 中记录“每个会话”是正常的(您可以为每个会话创建记录器并将其订阅到 Messages 事件),并且在 EF Core 中是可能的但极其复杂:stackoverflow.com/questions/43680174/…(阅读我的答案)。将一行事件订阅与 ef 核心中使用超级 IoC 容器的数百行池化记录器进行比较。
【解决方案2】:

如您所问,这是一个非常基本且粗略的示例,通过组合而不是继承来解决此问题。

public class RepositoryService : IRepositoryService
{

    public RepositoryService (IServiceA serviceA, IServiceB serviceB) 
    {
        /* ... */
    }

    public void SomeMethod()
    {
    }     
}

public abstract class Repository
{
    protected IRepositoryService repositoryService;

    public (IRepositoryService repositoryService)   
    {
      this.repositoryService= repositoryService;
    }

    public virtual void SomeMethod()
    {
          this.repositoryService.SomeMethod()

          .
          .
    }
}

public class ChildRepository1 : Repository
{

    public (IRepositoryService repositoryService)  : base (repositoryService)
    {
    }

    public override void SomeMethod()
    {
          .
          .
    }
}

public class ChildRepository2 : Repository
{

    public (IRepositoryService repositoryService, ISomeOtherService someotherService)   : base (repositoryService)
    {
          .
          .
    }

    public override void SomeMethod()
    {
          .
          .
    }
}

现在,这里的抽象基类和每个子存储库类将仅依赖于IRepositoryService 或任何其他必需的依赖项(请参阅ChildRepository2 中的ISomeOtherService)。

这样,您的子存储库仅向您的基类提供 IRepositoryService 依赖项,而您无需在任何地方提供 IRepositoryService 的依赖项。

【讨论】:

  • 这个实现打破了我的要求之一(“库应该易于用户使用”)。对于RepositoryService 的每个方法,用户都必须这样做,这很烦人。我正在寻找更像 EF Core 中的 DbContext 类的东西(无参数构造函数和更改默认选项的方法)
  • 但是,您将拥有 RepositoryServiceRepository 类,而不是您的用户。所以,我不确定您的用户使用起来有多么困难。
  • 哦,我没有正确阅读代码。我可以这样做!谢谢!
  • 如果这有助于您解决您的问题,请您将其标记为答案
【解决方案3】:

经过多年的经验,我发现装饰器模式非常适合。

实施:

// Abstract type
public interface IRepository<T>
{
    Add(T obj);
}

// Concete type
public class UserRepository : IRepository<User>
{
    public UserRepository(/* Specific dependencies */) {}

    Add(User obj) { /* [...] */ }
}

// Decorator
public class LoggingRepository<T> : IRepository<T>
{
    private readonly IRepository<T> _inner;

    public LoggingRepository<T>(IRepository<T> inner) => _inner = inner;

    Add(T obj) 
    {
        Console.Log($"Adding {obj}...");
        _inner.Add(obj);
        Console.Log($"{obj} addded.");
    }
}

用法:

// Done using the DI.
IRepository<User> repository = 
    // Add as many decorators as you want.
    new LoggingRepository<User>(
        new UserRepository(/* [...] */));

// And here is your add method wrapped with some logging :)
repository.Add(new User());

这种模式很棒,因为您可以将行为封装在单独的类中,而不会破坏更改,并且仅在您真正需要时才使用它们。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-28
    • 2011-12-24
    • 1970-01-01
    • 2015-06-18
    • 2021-04-16
    • 1970-01-01
    • 2016-06-03
    相关资源
    最近更新 更多