I have been doing iOS development using MVVM and dependency injection for a couple of months and I am really happy with the results. The code is so much clear and easier to test. But I have been stragling with a problem which I haven't found a solution that I felt really confortable with.
In order to understand the problem I want to give you a little bit of context. The last app that I have been working was architectured in the following way / layers:
When the applications starts we have a Bootstrap class that initializes the app and creates the main view model. We have a restriction that only view models can create other view models. For example in the case of having a view that contains a list of elements (in iOS it will be represented with a UITableView) and the detail view for each of thoses elements that is presented by pushing it to the navigation stack after tapping on the element in the list. What we do is make the view model that is attached to the table view controller create the detail view model. The table view controller listens to the table view model and then presents the detail view model by creating the detail view controller and passing it its view model. So the view controller does not know how to create a view model it only knows how to create a view controller for that view model.
Is the responsability of the parent view model to the pass all the dependecies to the child view model.
The problem comes when a view model that is very deep in the view hierachy needs dependencies that its parent controllers does not require. For example a service to access some external web service. Because its parent does not have that dependency it will have to add it to its dependecy list, thus adding a new parameter to the constructor. Imagine how this goes if the grand parent does not have the dependecy either.
What do you think is a good solution? Possible solutions:
For now we have chosen the factory class solution because we don't need to use singletons and we can treat the factory as any other dependecy which makes it relatively easy to test. The problem is that it kind of feels like a good object and by having a factory you don't actually know which is the real dependecy that needs the view model, unless you look inside the constructor's implementation to check which factory methods are being called.
In our application, we have chosen to have our view models access their dependencies via dependency lookup rather than dependency injection. This means the view models are simply passed a container object which contains the necessary dependencies, and then "looks up" each dependency from this container object.
The major advantage of this is that all objects in the system can be declared up front in a container definition, and it is very simple to pass around the container, compared to the seventy-eight or so dependencies that might be needed.
As any dependency injection fan will tell you, dependency lookup is certainly its inferior cousin, largely because dependency lookup requires the object to understand the idea of a container (and therefore usually the framework that provides it), whereas dependency injection keeps the object blissfully unaware of where its dependencies came from. However, in this case I believe the tradeoff is worth it. Note that in our architecture, it's just the view models that make this tradeoff - all other objects such as your "models" and "services" still use DI.
It's also worth noting that many basic implementations of dependency lookup have the container as a singleton, but that does not have to be the case. In our application, we have multiple containers that simply "group" related dependencies together. This is particularly important if different objects have different lifecycles - some objects may live forever, while others may only need to live while a certain user activity is in progress. This is why the container is passed from view model to view model - different view models may have different containers. This also facilitates unit testing by allowing you to pass a container full of mock objects to the view model under test.
To provide some concreteness to an otherwise abstract answer, here's 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
}
What you need to do is move the instantiation of all your objects to a Composition Root. Instead of parents passing down dependencies they don't even necessarily need to their children, you have a single point of entry at the start of your program where all of your object graph is created (and cleaned up, should you have Disposable dependencies).
You can find a good example here, by the author of the Dependency Injection in .NET book (highly recommended to understand concepts like the Composition Root) - notice how it frees you from having to pass dependencies 5 or 6 levels deep 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 to do proper Dependency Injection, and it will allow you to very easily transition to use a container if you want to.
For the instantiation of objects that must be created after startup, or depend on data available long after startup, what you want to do is create Abstract Factories that know how to create these objects and take as constructor parameters all needed stable dependencies. These factories are injected as normal dependencies in the composition root, and are then called upon as needed with the variable/unstable arguments passed in as method parameters.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With