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

12

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

为了理解这个问题,我想给你一些背景。我最近正在工作的应用程序的架构如下所示/层:

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

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

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

问题出在一个非常深的视图模型需要其父控制器不需要的依赖项,例如访问某些外部Web服务的服务。因为它的父级没有这个依赖性,所以它将不得不将其添加到其依赖项列表中,从而向构造函数添加一个新参数。如果祖先也没有该依赖项,想象一下这将如何进行。
你认为什么是好的解决方案?可能的解决方案:
- 单例:更难测试,基本上是全局状态。 - 工厂类:我们可以创建一组知道如何创建某些对象类型的工厂。例如ServiceFactory和RepositoryFactory。Service factory可以有用于创建服务的方法,例如TwitterService、FacebookService和GithubService。存储库工厂可以知道如何为每个API资源创建存储库。如果只有几个工厂(2或3),则所有视图模型都可以依赖于这些工厂。
目前,我们选择了工厂类解决方案,因为我们不需要使用单例,并且我们可以将工厂视为任何其他依赖关系,这使得它相对容易测试。问题在于它感觉像一个好的对象,通过拥有一个工厂,你实际上不知道哪个是需要视图模型的真正依赖性,除非你查看构造函数的实现以检查调用了哪些工厂方法。

我一直在苦恼着同样的问题,已经使用MVVM和RAC几个月了,总体上非常满意。但是臃肿的构造函数是一个很大的代码异味。到目前为止,我一直推迟解决这个问题,但我确信如果不尽快解决它,它会适得其反。 - ynnckcmprnl
4个回答

3
在我们的应用程序中,我们选择让视图模型通过“依赖查找”而不是依赖注入来访问它们的依赖项。这意味着视图模型只需传递一个包含必要依赖项的容器对象,然后从该容器对象“查找”每个依赖项。
其中的主要优点是,系统中的所有对象都可以在容器定义中预先声明,并且与可能需要的七十八个左右的依赖项相比,传递容器非常简单。
正如任何依赖注入的粉丝所说的那样,依赖查找确实是其劣等的表亲,主要是因为依赖查找需要对象理解容器的概念(因此通常需要提供它的框架),而依赖注入使对象不知道其依赖项来自哪里。但是,在这种情况下,我认为这种权衡是值得的。请注意,在我们的架构中,只有视图模型才做出了这种权衡 - 所有其他对象,如您的“模型”和“服务”,仍然使用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
}

3
你需要做的是将所有对象实例化的过程移到组合根(Composition Root)中。不再让父级向他们的子级传递他们甚至不需要的依赖项,你有一个程序开始时的单个入口点,在这里创建了你的整个对象图(如果你有可释放资源的依赖项,则进行清理)。
你可以在这里找到一个很好的示例,这个示例来自于.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);

这样做基本上是实现正确的依赖注入的先决条件,它将使您能够非常轻松地转换为使用容器。

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


0

以下是一些建议。

  • 最佳编码实践建议,如果您使用的参数超过3个,则应使用类来托管参数。
  • 另一种方法是将数据服务[存储库]分离出来,使其与基于任务的服务相一致。主要是为了与ViewModel(或Controller)保持一致。因此,如果您的ViewModel使用CustomersOrders,大多数人会使用两个服务-一个用于对客户进行CRUD操作,另一个用于对订单进行CRUD操作。但是,您可以使用一个服务来处理ViewModel所需的所有操作。这是在设计Windows Communication Foundation Services和Web Services时使用的基于任务的方法。

-2

看起来你需要使用托管可扩展性框架(MEF),你可以在这里找到更多信息。

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


我正在进行iOS开发,无法使用那个。除此之外,我希望不必使用框架来解决这个问题。 - GuidoMB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接