MVVM, dependency injection and too many constructor options

I have been developing iOS using MVVM and dependency injection for several months and am really happy with the results. The code is so clear and easier to test. But all the time I was faced with a problem that did not find solutions with which I felt really comfortable.

To understand the problem, I want to give you a little context. The last application I worked on was built as follows / layer:

  • Model
  • Browse Models
  • View / View Controllers
  • Services Classes that know how to handle external services such as Twitter, Facebook, etc.
  • Repositories . A repository is a class that knows how to interact with an application's REST API resource. Suppose we have a blog application, we can have user resources and message resources. Each of the thoses resources has several methods. There is a 1 to 1 relationship between resources and repositories.

When starting applications, we have a Bootstrap class that initializes the application and creates the main presentation model. We have a limitation that only view models can create other view models. For example, if you have a view containing a list of elements (in iOS it will be presented using a UITableView) and a detailed view for each of the thoses elements, which is presented by clicking on the navigation stack after clicking on an element in the list. We make the view model attached to the table view controller create the detail view model. The table view controller listens for the table view model, and then presents the detailed view model, creating a detailed view controller and passing it its view model. Therefore, the view manager does not know how to create a view model; it knows how to create a view controller for this view model.

Is the responsibility of the parent view model the transfer of all the dependencies of the child view model.

The problem arises when a view model that is very deep in the view hierarchy requires dependencies that are not required by its parent controllers. For example, a service for accessing some external web services. Since his parent does not have such a dependency, he will have to add it to his dependecy list by adding a new parameter to the constructor. Imagine how this happens if the grand parent also has no addiction.

Do you think this is a good solution? Possible solutions:

  • Singleton : harder to test, and they are mostly sinister.
  • Class factory . We could create a factory set that knows how to create certain types of objects. For example, ServiceFactory and RepositoryFactory. A factory service may have a method for creating services such as: TwitterService, FacebookService, GithubService. The factory repository might know how to create a repository for each of the API resources. If there are several factories (2 or 3), all viewing models may depend on these plants.

Now we have chosen the solution of the factory class, because we do not need to use singletones, and we can consider the factory like any other dependency, which makes it relatively easy to test. The problem is that it looks like a good object and with the help of factory you really don’t know what real dependency depends on the model of the presentation unless you look inside the constructor implementation to check which factory methods are called.

+6
source share
4 answers

Here are some suggestions.

  • The best coding methods suggest that if you use more than three parameters, you should use a class to place the parameters.
  • Another approach is to separate the data services [repositories] so that they are consistent with the task-based service. Basically, according to the ViewModel (or controller). Therefore, if the ViewModel uses Clients and Orders , most of them will use two services - one for CRUD operations for Clients, and for CRUD operations on orders. However, you can use a service that will handle all the operations necessary for your ViewModel. This is a task-based approach used in developing Windows Communication Foundation and Web services.
0
source

In our application, we decided that our view models access their dependencies through a dependency search, rather than dependency injection. This means that view models simply pass in a container object that contains the necessary dependencies, and then “looks through” each dependency on this container object.

The main advantage of this is that all objects in the system can be declared in advance in the container definition, and it is very easy to bypass the container compared to dependencies of 70 or so dependencies that may be required.

Like any injection dependency scanner will tell you that dependency search is certainly its incomplete cousin, mainly because dependency search requires the object to understand the idea of ​​the container (and usually it’s usually the framework that provides it), then how injection of dependencies keeps the object blissfully unaware of where its dependencies came from. However, in this case, I think the compromise is worth it. Note that in our architecture, these are just presentation models that make this compromise - all other objects, such as your “models” and “services”, still use DI.

It is also worth noting that many basic dependency search implementations have a container as a singleton, but this is not necessarily the case. In our application, we have several containers that simply group related dependencies with each other. This is especially important if different objects have different life cycles - some objects can live forever, while others may only need to live while a certain user activity is being performed. This is why the container is transferred from the view model to view the model - different view models can have different containers. It also facilitates unit testing, allowing you to transfer a container full of mock objects to the test representation model.

To provide some concreteness to the abstract answer otherwise, here is how one of our view models might look. We use the Swinject framework.

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 } 
0
source

What you need to do is move an instance of all your objects into the Composition Trash. Instead of having parents pass dependencies that they don’t even need their children, you have a single entry point at the beginning of your program where your entire graph of objects is created (and cleared if you have one-time dependencies).

Here you can find a good example by the author. Injecting dependencies in .NET (it is strongly recommended to understand concepts such as Root Composition) - pay attention to how this frees you from having to pass dependencies to 5 or 6 levels for no reason:

 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); 

Doing this is pretty much a prerequisite for injecting dependencies correctly, and this will allow you to very easily switch to using the container if you want.

To create objects that must be created after startup or depend on the data available after startup, you need to create abstract factories that know how to create these objects and take all necessary stable dependencies as constructor parameters. These factories are introduced as normal dependencies at the root of the composition, and then called as necessary with variable / unstable arguments passed as method parameters.

0
source

It looks like you need to use the Managed Extensibility Framework (MEF) , you can find more information here .

Essentially, this will allow you to use the [Export] and [Import] attributes. This will allow you to introduce dependencies of your class, without having to worry about massive constructors in the models of your parent views.

-2
source

Source: https://habr.com/ru/post/988308/


All Articles