代码之家  ›  专栏  ›  技术社区  ›  Maxime Gélinas

在C语言中如何将依赖注入与继承结合使用#

  •  19
  • Maxime Gélinas  · 技术社区  · 6 年前

    介绍

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

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

    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)
        {
            /* ... */
        }
    }
    

    问题

    好的,对于当前代码,派生类必须知道基类的每个依赖项,这些依赖项都可以,但是如果我向基类添加一个依赖项呢?每个派生类都将中断,因为它们需要将新的依赖关系传递给基类。。。所以现在, 我仅限于从不更改基类构造函数 这是一个问题,因为我希望我的基类有可能进化。 这种实现显然打破了开放/封闭原则 ,但我不知道如何在不破坏实体的情况下解决这个问题。。。

    要求

    • 该库应便于用户使用
    • 混凝土存储库应该能够通过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 如果添加依赖项,则必须修改接口
    • 这个 文章 没有解释如何使用 Castle DynamicProxy 2 (对我来说这是一种未知的技术)动态生成服务聚合器

    2、建设者模式

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

    赞成的意见

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

    欺骗

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

    3、物业注入

    也许可以选择删除基本存储库构造函数并配置DI以使用属性注入。

    赞成的意见

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

    欺骗

    • 我认为财产设定者必须公开?

    结论

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

    4 回复  |  直到 3 年前
        1
  •  7
  •   lopezbertoni    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());
    

    这种模式非常棒,因为您可以将行为封装在单独的类中,而无需中断更改,并且只在真正需要时使用它们。

        2
  •  4
  •   Ankit Vijay    6 年前

    正如您所问的,这里有一个通过组合而不是继承来解决这个问题的非常基本和粗糙的示例。

    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 或任何其他必需的依赖项(请参阅 ISomeOtherService 在里面 ChildRepository2 ).

    这样,您的子存储库只提供 IRepositoryService 依赖于基类,并且不需要提供 IRepositoryService 在任何地方

        3
  •  0
  •   Sanabalis Dragon    2 年前

    可能有点晚了,但您可以在构造函数中使用IServiceProvider:

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

    public abstract class Repository<T> 
    {
        protected Repository(IServiceProvider serviceProvider) 
        {
            serviceA = serviceProvider.GetRequiredService<IServiceA>();
            serviceB = serviceProvider.GetRequiredService<IServiceB>();
            /* ... */
        }
    }
    

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

    public class FooRepository : Repository<Foo> 
    {
        protected FooRepository(IServiceProvider serviceProvider)
            :base(serviceProvider)
        {
            serviceC = serviceProvider.GetRequiredService<IServiceC>();
            serviceD = serviceProvider.GetRequiredService<IServiceD>();
            /* ... */
        }
    }
    

    这样,每个类都可以利用自己的服务,而不需要任何交叉依赖。

    这种方法有缺点。首先,这是一种带有抽象定位器的服务定位器(anti)模式,但这并不意味着它不应该在任何地方使用。公正地对待它,确保没有更好的解决方案。 其次,没有编译时强制为任何存储库类提供必要的服务实现。也就是说,如果您不将IServiceA的具体实现放到服务提供者中,这仍然会编译。然后,它将在运行时失败。然而,在这种情况下,这是您的要求之一。

        4
  •  -3
  •   Roman Pokrovskij Peterdk    6 年前

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

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

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

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