【问题标题】:MVVM, dependency injection and too many constructor parametersMVVM、依赖注入和太多的构造函数参数
【发布时间】:2015-06-01 10:22:41
【问题描述】:

几个月来,我一直在使用 MVVM 和依赖注入进行 iOS 开发,我对结果非常满意。代码非常清晰,更容易测试。但是我一直在纠结一个问题,我还没有找到让我感到非常满意的解决方案。

为了理解这个问题,我想给你一点上下文。我一直在工作的最后一个应用程序是按以下方式/层构建的:

  • 型号
  • 查看模型
  • 视图/视图控制器
  • 服务:知道如何处理 Twitter、Facebook 等外部服务的类。
  • 存储库:存储库是知道如何与应用程序的 REST API 资源交互的类。假设我们有一个博客应用程序,我们可以拥有用户资源和帖子资源。这些资源中的每一个都有几种方法。资源和存储库之间存在一对一的关系。

当应用程序启动时,我们有一个 Bootstrap 类来初始化应用程序并创建主视图模型。我们有一个限制,只有视图模型可以创建其他视图模型。例如,如果有一个包含元素列表的视图(在 iOS 中,它将用 UITableView 表示)和每个元素的详细视图,这些元素通过在点击元素后将其推送到导航堆栈来呈现在列表中。我们所做的是让附加到表视图控制器的视图模型创建详细视图模型。表视图控制器监听表视图模型,然后通过创建详细视图控制器并将其视图模型传递给它来呈现详细视图模型。所以视图控制器不知道如何创建视图模型,它只知道如何为该视图模型创建视图控制器。

父视图模型是否有责任将所有依赖项传递给子视图模型。

当视图层次结构非常深入的视图模型需要其父控制器不需要的依赖项时,就会出现问题。例如访问某些外部 Web 服务的服务。因为它的父级没有该依赖项,所以必须将其添加到其依赖项列表中,从而向构造函数添加一个新参数。想象一下,如果祖父母也没有依赖关系,会发生什么情况。

您认为什么是好的解决方案?可能的解决方案:

  • 单例:更难测试,它们基本上是全局状态
  • 工厂类:我们可以使用一组知道如何创建特定类型对象的工厂。例如 ServiceFactory 和 RepositoryFactory。服务工厂可以具有创建服务的方法,例如:TwitterService、FacebookService、GithubService。存储库工厂可以知道如何为每个 API 资源创建存储库。在有几个工厂(2 或 3 个)的情况下,所有视图模型都可能依赖于这些工厂。

目前我们选择了工厂类解决方案,因为我们不需要使用单例,我们可以将工厂视为任何其他依赖项,这使得测试相对容易。问题是它感觉像是一个很好的对象,并且通过拥有一个工厂,您实际上并不知道哪个是需要视图模型的真正依赖项,除非您查看构造函数的实现以检查正在调用哪些工厂方法。

【问题讨论】:

  • 我一直在为同样的问题苦苦挣扎,几个月来我一直在使用带有 RAC 的 MVVM,并且通常对此非常满意。但是臃肿的构造函数是一种很大的代码气味。到目前为止,我一直推迟解决这个问题,但如果我不尽快解决它,我相信它会适得其反。

标签: ios mvvm dependency-injection


【解决方案1】:

在我们的应用程序中,我们选择让我们的视图模型通过依赖查找而不是依赖注入来访问它们的依赖。这意味着视图模型只需传递一个包含必要依赖项的容器对象,然后从该容器对象“查找”每个依赖项。

这样做的主要优点是系统中的所有对象都可以在容器定义中预先声明,并且与可能需要的大约 78 个依赖项相比,传递容器非常简单。

任何依赖注入爱好者都会告诉你,依赖查找肯定是它的下级表亲,主要是因为依赖查找需要对象理解容器的概念(因此通常是提供它的框架),而依赖注入保持对象幸福地不知道它的依赖来自哪里。但是,在这种情况下,我相信权衡是值得的。请注意,在我们的架构中,只是视图模型做出了这种权衡——所有其他对象,例如“模型”和“服务”,仍然使用 DI。

还值得注意的是,依赖查找的许多基本实现都将容器作为单例,但不一定是这种情况。在我们的应用程序中,我们有多个容器,它们简单地将相关的依赖项“组合”在一起。如果不同的对象有不同的生命周期,这一点尤其重要——一些对象可能永远存在,而其他对象可能只需要在某个用户活动正在进行时才存在。这就是容器从视图模型传递到视图模型的原因——不同的视图模型可能有不同的容器。通过允许您将一个装满模拟对象的容器传递给正在测试的视图模型,这也有助于单元测试。

为了给原本抽象的答案提供一些具体性,下面是我们的一个视图模型的外观。我们使用Swinject 框架。

class SomeViewModel: NSObject {
    private let fooModel: FooModel
    private let barModel: BarModel

    init(container: Container) {
        fooModel = container.resolve(FooModel.self)!
        barModel = container.resolve(BarModel.self)!
    }

    // variety of code here that uses fooModel and barModel
}

【讨论】:

    【解决方案2】:

    这里有几个建议。

    • 最佳编码实践建议,如果您使用 3 个以上的参数,则应使用类来托管参数。
    • 另一种方法是将数据服务 [存储库] 分开,以便它们与基于任务的服务对齐。主要是为了符合 ViewModel(或 Controller) 所以如果你的 ViewModel 使用 CustomersOrders,大​​多数会使用两种服务 - 一种用于对客户的 CRUD 操作,另一种用于对订单的 CRUD 操作。但是,您可以使用一个服务来处理您的 ViewModel 所需的所有操作。这是一种基于任务的方法,用于设计 Windows Communication Foundation 服务和 Web 服务。

    【讨论】:

      【解决方案3】:

      您需要做的是将所有对象的实例化移动到合成根。而不是父母将他们甚至不一定需要的依赖项传递给他们的孩子,您在程序开始时有一个单一的入口点,您的所有对象图都在其中创建(如果您有一次性依赖项,则进行清理)。

      You can find a good example here,作者 Dependency Injection in .NET 书的作者(强烈建议您理解诸如 Composition Root 之类的概念)——注意它如何让您不必无缘无故地传递 5 或 6 层的依赖关系:

      var queueDirectory = 
          new DirectoryInfo(@"..\..\..\BookingWebUI\Queue").CreateIfAbsent();
      var singleSourceOfTruthDirectory = 
          new DirectoryInfo(@"..\..\..\BookingWebUI\SSoT").CreateIfAbsent();
      var viewStoreDirectory = 
          new DirectoryInfo(@"..\..\..\BookingWebUI\ViewStore").CreateIfAbsent();
      
      var extension = "txt";
      
      var fileDateStore = new FileDateStore(
          singleSourceOfTruthDirectory,
          extension);
      
      var quickenings = new IQuickening[]
      {
          new RequestReservationCommand.Quickening(),
          new ReservationAcceptedEvent.Quickening(),
          new ReservationRejectedEvent.Quickening(),
          new CapacityReservedEvent.Quickening(),
          new SoldOutEvent.Quickening()
      };
      
      var disposable = new CompositeDisposable();
      var messageDispatcher = new Subject<object>();
      disposable.Add(
          messageDispatcher.Subscribe(
              new Dispatcher<RequestReservationCommand>(
                  new CapacityGate(
                      new JsonCapacityRepository(
                          fileDateStore,
                          fileDateStore,
                          quickenings),
                      new JsonChannel<ReservationAcceptedEvent>(
                          new FileQueueWriter<ReservationAcceptedEvent>(
                              queueDirectory,
                              extension)),
                      new JsonChannel<ReservationRejectedEvent>(
                          new FileQueueWriter<ReservationRejectedEvent>(
                              queueDirectory,
                              extension)),
                      new JsonChannel<SoldOutEvent>(
                          new FileQueueWriter<SoldOutEvent>(
                              queueDirectory,
                              extension))))));
      disposable.Add(
          messageDispatcher.Subscribe(
              new Dispatcher<SoldOutEvent>(
                  new MonthViewUpdater(
                      new FileMonthViewStore(
                          viewStoreDirectory,
                          extension)))));
      
      var q = new QueueConsumer(
          new FileQueue(
              queueDirectory,
              extension),
          new JsonStreamObserver(
              quickenings,
              messageDispatcher));
      
      RunUntilStopped(q);
      

      这样做几乎是进行适当的依赖注入的先决条件,如果您愿意,它可以让您非常轻松地过渡到使用容器。

      对于必须在启动后创建的对象的实例化,或者依赖于启动后很长时间可用的数据,您要做的是创建知道如何创建这些对象并将所有需要的稳定依赖项作为构造函数参数的抽象工厂。这些工厂作为普通依赖注入到组合根中,然后根据需要使用作为方法参数传入的变量/不稳定参数进行调用。

      【讨论】:

        【解决方案4】:

        看来您需要使用Managed Extensibility Framework (MEF),您可以找到更多信息here

        本质上,这将允许您使用[Export][Import] 属性。这将允许注入您的类的依赖项,无需担心您的父视图模型上的大量构造函数。

        【讨论】:

        • 我在做iOS开发不能用那个。除此之外,我不想使用框架来解决这个问题
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多