MVVM,依赖注入和太多构造函数参数

时间:2015-06-01 10:22:42

标签: ios mvvm dependency-injection

我使用MVVM和依赖注入进行了几个月的iOS开发,我对结果非常满意。代码非常清晰,更容易测试。但是我一直在讨论一个我无法找到解决方案的问题,我觉得这个问题真的很舒服。

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

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

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

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

问题出现在视图层次非常深的视图模型需要其父控制器不需要的依赖项时。例如,访问某些外部Web服务的服务。因为它的父项没有那个依赖项,所以它必须将它添加到它的依赖列表中,从而为构造函数添加一个新参数。想象一下,如果祖父母也不具备这种依赖性,情况会怎样。

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

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

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

4 个答案:

答案 0 :(得分:0)

以下是一些建议。

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

答案 1 :(得分:0)

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

这样做的主要优点是系统中的所有对象都可以在容器定义中预先声明,并且与可能需要的大约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 :(得分:0)

您需要做的是将所有对象的实例化移动到Composition Root。而不是父母传递依赖关系,他们甚至不一定需要他们的孩子,你在程序开始时有一个单一的入口点,你的所有对象图都被创建(并清理,如果你有一次性依赖项)。

You can find a good example here,作者Dependency Injection in .NET一书(强烈建议理解像组合根这样的概念) - 请注意它是如何让你免于传递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);

执行此操作几乎是执行正确的依赖注入的先决条件,并且如果您愿意,它将允许您非常轻松地转换为使用容器。

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

答案 3 :(得分:-2)

您似乎需要使用托管扩展性框架(MEF),您可以找到更多信息here

基本上,这将允许您使用[Export][Import]属性。这将允许您的班级&#39;要注入的依赖项,没有不必担心父视图模型上的大量构造函数。