Can Constructor Over-Injection be avoided in Windows Forms?

247 views Asked by At

I have a Windows Form. It contains many controllers: Grids, buttons, and even a TreeView. Events are defined on most of them. For example, there are multiple independent events that query or command my SQL server in independent ways. For almost all cases, the Interface Segregation Principle forces me to not merge these interfaces.

Because I'm doing dependency injection, the constructor for my Windows Form is massively over-injected. I have something like four parameters in the form's constructor just for a single grid. Example include: initial population of the grid, querying the list of values for a combobox in one cell of it, repopulating certain types of cell when one other type changes, etc. I reckon that it won't be long before my constructor has something absurd like 20 arguments, all interfaces that query the server.

Is this avoidable in Windows Forms? At a guess, I think I'd be better off if I could somehow build each component and then feed them in to my constructor rather than letting my form know about the dependencies of each component. That is, I think I'd rather replace

MyForm(IQueryGridTimes TimeQueryRepo, ICommandGridTimeCells TimeCommandRepo, IQueryTheWholeGrid GridInitialPopulator, ..., IQueryForTheTreeView TreeViewPopulator)

with

var grid = new WhateverGrid(IQueryGridTimes TimeQueryRepo, ICommandGridTimeCells TimeCommandRepo, IQueryTheWholeGrid GridInitialPopulator)
var tress = new WhateverTreeview(QueryForTheTreeView TreeViewPopulator)
MyForm(grid, ..., trees,)

Is this both wise and possible? I'm open to other approaches and do not need to assume any dependency injection container (I'd rather not use any).

2

There are 2 answers

0
Blindy On

There are many different ways to achieve something similar to what you want.

One quick way to minimize the number of dependencies is to aggregate some of them. Four or more dependencies just to populate one grid seems quite excessive. Instead you could have a repository dependency of some kind that exposes all the necessary data coming from your cache or permanent storage. An interface can hold more than one function.

Alternatively, you can just not use constructors. Most DI frameworks use them out of convention, but that's all it is. You can easily write a DI framework that injects into properties instead, and you then need to only provide properties for your engine to fill.

As a follow up to the previous suggestion, it's relatively trivial to write a source generator handle the creation of DI objects for you, without needing to resort to reflection. Not that reflection is that bad when you have just a few dozen objects in the first place, but source genning them is definitely better.

2
Jonathan Feenstra On

Mark Seemann describes some possible ways of dealing with constructor over-injection in his blog:

  • If the constructor arguments form natural clusters of behaviour, they can be refactored to Facade Services to create more coarse-grained interfaces.
  • If the dependencies represent cross-cutting concerns, it would be better to address them using Decorators (also described in @Steven's blog post) or the Chain of Responsibility pattern instead of injecting them separately into all consumers that require them.
  • If the number of constructor arguments cannot sufficiently be reduced by the above suggestions, this is often a symptom of a class doing too much and violating the Single Responsibility Principle. At this point it would be worth considering splitting the form into smaller components (user controls) as Steven suggested, although the required default constructors can be difficult to deal with as explained in his comment.

Seemann also notes that there could be valid cases for having constructors with a lot of dependencies where that is simply the most maintainable solution or genuinely not a problem.

Steven also suggested using Mediator-like patterns as an alternative solution as detailed in his blog post. There are libraries that can help implement this pattern, such as MediatR (using reflection) and Mediator (using source generators). However, this will increase the indirection and make it more difficult to keep track of which dependencies the form is using. As a result, unit tests can become more fragile and you need to verify yourself that all of the required handlers are registered.

Note that switching to Property Injection does not reduce the number of dependencies, but only implies they are optional instead of mandatory.