How to you use DI without DI framework in a reasonably complex project

dependency-injectioninversion-of-control

edit

I asked this question not because this is my opinion, but because I am trying to understand someone else's. Personally I prefer IoC via DI (in my specific case Simple Injector) but not everybody shares this opinion and I am trying to get some insight as to how maintainable code would be without DI.

end edit

I've been wondering about this for a few weeks now, but I can't seem to find a decent answer.

How can a developer write maintainable code while adhering to SOLID principles, without using a DI framework?

Some example code to make this somewhat less abstract:

class ImageProcessor: IImageProcessor
{
    public ImageProcessor() { ... }
}

class ImageUploader: IImageUploader
{
    public ImageUploader(IImageProcessor processor) { ... }
}

class ImageService
{
    public ImageService(IImageUploader uploader) { ... }
}

class ImageController
{
    public ImageController()
    {
        var imageService = new ImageService(new ImageUploader(new ImageProcessor()));
    }
}

Each class uses DI to get his dependencies injected via the constructor except the endpoint, in this case the ImageController which creates everything.

Suppose that the ImageProcessor now requires a new dependency, for example a IConfig to determine image dimensions.

class ImageProcessor
{
    public ImageProcessor(IConfig config) { ... }
}

class ImageController
{
    public ImageController()
    {
        var imageService = new ImageService(new ImageUploader(new ImageProcessor(new Config())));
    }
}

The main problem that I see is that all complexity and dependencies bubble up to every endpoint, every controller that uses this ImageProcessor and every consumer (e.g. batch processing).

The coupling of those inner dependencies would skyrocket because everyone knows about them.

Am I missing something?

How can I write maintainable code without a DI framework, while still trying to use DI?

Best Answer

The main problem that I see is that all complexity and dependencies bubble up to every endpoint, every controller that uses this ImageProcessor and every consumer (e.g. batch processing).

And how does a DI framework solve that? It reduces the amount of code you need to write, but the complexity is still there. You still have all of the dependencies, and you still have all of the coupling. The top layer is using the lower layers and something has to know how to make them. Using a DI framework just hides it away and makes it harder to debug.

But if you need to make a change, you're still at the mercy of the coupling.

As others have mentioned, I wouldn't do that sort of constructor chaining, but the general approach is fine. Classes take interfaces, whose implementation is supplied by some external actor. Having the controller know about that is maybe forcing it to have multiple responsibilities, but a DI framework is just one way to separate the "knows how to build dependencies" responsibility from the usual controller work.