Can Constructor Over-Injection be avoided in Windows Forms?
Asked Answered
C

2

6

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

Cheater answered 24/7, 2023 at 17:13 Comment(10)
I myself find it difficult to keep the number of dependencies to a maintainable level in my client applications (nowadays I mainly work on WPF with MVVM, but I have the same struggle). Here are two ideas you might want to try: split a form into smaller components, and build that form from those parts. This is still tricky in Win Forms, because controls require a default constructor. You therefore have to fallback to property injection or a similar mechanism for their initialization, and this again leads initializing those controls from within the startup path of your application.Eisen
Second option is to use Mediator-like patterns, such as described here and here where you, for instance, inject a single dependency (e.g. IQueryProcessor) that allows executing any query. This has as benefit (among other things) to reduce the number of dependencies, with the consequence of increasing the indirection.Eisen
@Eisen The mediator solution would probably work, but I'm eternally sceptical of anything that requires either reflection or dynamic. Regardless, what you've said already seems enough to form an answer. "You can do it but it's a damn pain" is a disappointment, but still an answer.Cheater
Your millage of course might vary and so do opinions, but I can say that I applied these techniques with much gain to VB Win Forms application, which brought much joy to my fellow developers. You might want to investigate these concepts, because -besides lowering the number of dependencies of your Forms- these concepts are very powerful and work well for a wide range of application types. But either way, good luck with your journey and I wish you much joy with programming.Eisen
I think this heavily depends on how do you implement DI. Are you e.g. using Microsoft.Extensions.DependencyInjection?Electrostriction
@Electrostriction See the last sentence of my question. My preference is to not use any DI container.Cheater
I didn't mean a DI container. The objects you are injecting into the constructor cannot appear themselves, there should be an implementation that delivers them, even if it doesn't use DI containers.Electrostriction
@Electrostriction Sure they can. You just call new in your composition root.Cheater
That's what I meant under "implementation". I'm sorry if that wasn't the right term.Electrostriction
@Eisen Given that the bounty has attracted many upvotes but not answers, I can only assume that your answer is the correct one. Feel free to post it as a full answer.Cheater
A
1

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.

Andrej answered 1/8, 2023 at 19:32 Comment(2)
None of this answer appears to be specific to Windows Forms, even though the question is clearly about them and the problems they cause.Cheater
@J.Mini My third point mentions the difficulties with splitting Windows Forms into smaller components, referrring to Steven's comment. Other than that my answer is indeed not specific to WinForms, but instead offers general solutions that are applicable to WinForms as well. If you have any other specific problems that make my suggested solutions difficult to apply in WinForms, could you please edit your question to elaborate?Andrej
L
0

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.

Lowis answered 26/7, 2023 at 21:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.