【问题标题】:Constructor injection overuse构造函数注入过度使用
【发布时间】:2012-06-19 12:02:17
【问题描述】:

我正在寻找避免过度使用构造函数注入的最佳实践。例如,我有会议实体,它有几个子实体,如下所示:

  • 会议
    1. 会议联系人
    2. 会议与会者
    3. 会议类型
    4. 地址
    5. 会议公司
    6. 会议记录

MeetingService 类如下所示:

public class MeetingService
{
    private readonly IMeetingContactRepository _meetingContactRepository;
    private readonly IMeetingAttendeeRepository _meetingAttendeeRepository;
    private readonly IMeetingTypeRepository _meetingTypeRepository;
    private readonly IAddressRepository _addressRepository;
    private readonly IMeetingCompanyRepository _meetingCompanyRepository;
    private readonly IMeetingNoteRepository _meetingNoteRepository;
    private readonly IMeetingRepositoy _meetingReposity;

    public MeetingService(IMeetingRepositoy meetingReposity, IMeetingContactRepository meetingContactRepository, IMeetingAttendeeRepository meetingAttendeeRepository, 
        IMeetingTypeRepository meetingTypeRepository, IAddressRepository addressRepository, 
        IMeetingCompanyRepository meetingCompanyRepository, IMeetingNoteRepository meetingNoteRepository)
    {
        _meetingReposity = meetingReposity;
        _meetingContactRepository = meetingContactRepository;
        _meetingAttendeeRepository = meetingAttendeeRepository;
        _meetingTypeRepository = meetingTypeRepository;
        _addressRepository = addressRepository;
        _meetingCompanyRepository = meetingCompanyRepository;
        _meetingNoteRepository = meetingNoteRepository;
    }

    public void SaveMeeting(Meeting meeting)
    {
        meetingReposity.Save();
        if(Condition1())
            _meetingContactRepository.Save();
        if(Condition2())
            _meetingAttendeeRepository.Save();
        if(Condition3())
            _meetingTypeRepository.Save();
        if(Condition4())
            _addressRepository.Save();
        if(Condition5())
            _meetingCompanyRepository.Save();
        if(Condition6())
            _meetingNoteRepository.Save();
    }
    //... other methods
}

这里只是七个依赖项,但真正的代码包含更多。我使用了"Dependency Injection Constructor Madness" 中描述的不同技术,但我还没有找到如何处理存储库依赖项。

有什么方法可以减少依赖的数量并保持代码可测试?

【问题讨论】:

  • 创建一个 MeetingConfiguration 类,其中构造函数为您“放牧猫”。然后,您可以将 MeetingConfiguration 类传递给您正在初始化的任何内容。你不会解决有多个重载的问题,但至少所有的重载都在一个地方。
  • 如果你真的写信给MeetingService 中的所有这些存储库,它一定做了很多。将MeetingService 拆分为仅共享密钥的其他服务怎么样?我的意思是,你在这里的工作单位是什么?如果会议在meetingReposity.Save(); 之后完成,则触发一个事件并让所有其他人订阅。
  • 我认为@dtryon 的建议要好得多。我不喜欢创建一个“配置”类只是为了隐藏你实际上有很多依赖项的想法。这只会使代码变得不那么清晰,但组织起来并没有更好。
  • 也同意@dtryon(发布答案!)如此大量的构造函数参数通常是一个明确的迹象,表明一个类做的太多并违反了 SRP
  • 我认为存储库的存储库是有序的……实际上,您可以使用 MEF 和 compose。

标签: c# .net unit-testing dependency-injection repository-pattern


【解决方案1】:

您真的需要将存储库功能拆分为这么多接口吗?你需要单独模拟它们吗?如果没有,您可以使用更少的接口,使用更多的方法。

但是让我们假设您的类确实需要那么多依赖项。在这种情况下,您可以:

  • 创建一个提供所有依赖项的配置对象 (MeetingServiceBindings)。每个模块可以有一个配置对象,而不仅仅是单个服务。我认为这个解决方案没有任何问题。
  • 使用依赖注入工具,例如NInject。这很简单,您可以在一个地方在代码中配置您的依赖项,并且不需要任何疯狂的 XML 文件。

【讨论】:

  • OP已经使用依赖注入。这些辅助类的问题在于您最终会重用它们,正如您所建议的那样!这听起来不错,除非您最终在没有辅助类所隐含的所有依赖项的类中重用它们。这会让你真正的依赖关系变得非常模糊。
  • 关于模糊依赖关系的好点。然后,您应该为每个类而不是整个模块创建一个配置对象。请注意,“DI 工具”与“DI”不同。
  • 如果类确实有那么多依赖项并且首选构造函数注入,则另一种解决方案是保持代码不变。
  • "如果没有,您可以使用更少的接口,使用更多的方法。"更少的接口和更多的方法导致违反Interface Segregation Principle,所以我投反对票。
【解决方案2】:

稍微扩展一下我上面的评论:

由于这个问题是针对如何管理存储库依赖关系的,我不得不假设MeetingService 正在管理某种持久提交。过去,当我看到像 MeetingService 这样的类具有这么多依赖项时,很明显它们做得太多了。所以,你必须问自己,“我的交易边界是什么”。换句话说,您可以做出的最小提交是多少,这意味着会议已成功保存。

如果答案是在调用meetingReposity.Save(); 后成功保存了会议,那么这就是MeetingService 应该管理的全部内容(对于提交)。

本质上,其他所有内容都是会议已保存这一事实的副作用(请注意,现在我们用过去时态说话)。此时,为每个其他存储库订阅事件更有意义。

这还具有将所有条件中的逻辑分离到遵循 SRP 以处理该逻辑的订阅者类的良好效果。例如,当联系人存储库提交的逻辑何时发生更改时,这一点变得很重要。

希望这会有所帮助。

【讨论】:

    【解决方案3】:

    构造函数过度使用只是一种症状 - 似乎您通过拥有一个了解消息持久性的各种元素并将它们插入到整体保存中的“主”类来接近 unit of work

    缺点是每个存储库都通过公开一个专用的Save 方法来传达其与其他存储库的独立性;但这是不正确的,因为 SaveMeeting 明确指出存储库不是独立的。

    我建议识别或创建存储库使用的类型;这集中了您的更改,并允许您从一个地方保存它们。示例包括 DataContext (LINQ to SQL)ISession (NHibernate)ObjectContext (Entity Framework)

    您可以在我之前的回答中找到有关存储库如何工作的更多信息:

    Advantage of creating a generic repository vs. specific repository for each object?

    一旦您拥有了存储库,您就可以确定它们将在其中运行的上下文。这通常映射到单个 Web 请求:在请求开始时创建公共工作单元的实例并将其交给所有存储库。在请求结束时,保存工作单元中的更改,让存储库无需担心访问哪些数据。

    这巧妙地将所有内容捕获并保存为一个单元。这与源代码控制系统的工作副本非常相似:将系统的当前状态拉入本地上下文,使用它,并在完成后保存更改。您不会单独保存每个文件 - 您可以同时保存所有文件作为离散修订。

    【讨论】:

    • 感谢您的帮助。我发现你的回答非常有用。至于我的设计,它是这样构建的,因为我试图从 ActiverRecord 迁移到 Repository。因此,实施工作单元将是我的下一步。
    【解决方案4】:

    前三个答案中的每一个都为处理摘要中的问题提供了重要的建议和想法。但是,我可能对您上面的示例阅读过多,但这看起来是聚合根太多的问题,per se 没有太多依赖项。这与缺少存储库注入基础架构下的持久性机制有关,或者与相同的配置错误有关。

    简单地说,就是联系人、与会者、笔记等。应该是会议本身的复合属性(如果仅作为链接到单独管理的联系人等对象/数据);因此,您的持久性机制应该自动保存它们。

    听从 Bryan Watts 的格言“构造函数过度使用只是一种症状”,还有其他几种可能性:

    • 您的持久性机制应该自动处理会议图的持久性,并且配置错误或缺乏执行此操作的能力(Bryan 建议执行此操作的所有三个,我会添加DbContext (EF 4.1+))。在这种情况下,实际上应该只有一个依赖项--IMeetingRepositoy--它可以处理会议及其组合本身的原子保存。
    • SaveMeeting() 不仅保存指向其他对象(联系人、与会者等)的链接,而且还保存这些对象,在这种情况下,我必须同意 dtryon 的观点,即 MeetingServiceSaveMeeting() 做得更多比名字所暗示的,他的机制可以缓解它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-12-12
      • 1970-01-01
      • 2011-01-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-01
      相关资源
      最近更新 更多