4

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?

  • I had an impression that the difference, for you, between IoC and DI wasn't clear, making your question unclear as well. I edited it to emphasize the difference between DI itself and DI framework. – Arseni Mourzenko Sep 6 '18 at 17:58
  • @ArseniMourzenko Thanks for the edit. The difference between IoC and DI should be clear to me though. The biggest issue is that DI is considered "unnecessarily complex" to use in our solution so we want to avoid it as much as possible. – Benjamin Diele Sep 6 '18 at 18:01
  • 1
    All the DI frameworks are just compiled code that somebody else wrote to help you implement IOC, don't like them? Write your own: kenegozi.com/blog/2008/01/17/… – RandomUs1r Sep 6 '18 at 18:44
  • 4
    Have you heard of pure dependency injection. You’re still doing dependency injection, just without the assistance of an IoC framework. The reason your examples aren’t solid is directly because they’re not injecting dependencies. – RubberDuck Sep 6 '18 at 22:21
  • 1
    Is it possible that by "framework" here you mean "library", or simply "Dependency Injection Container"? A whole framework based around managing dependency injection for you sounds... very crappy. – Félix Gagnon-Grenier Sep 7 '18 at 2:03
9

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.

  • The complexity is still there yes, but it is less tightly coupled using DI. Changes in an implementation somewhere don't bubble up to every controller that might use it in its dependency chain. – Benjamin Diele Sep 6 '18 at 18:53
  • 3
    @BenjaminDiele, whether you use pure DI, or achieve DI through using an IoC framework, there level of coupling shouldn't change. Each layer has dependencies and those dependencies are injected. With pure DI, you write your own dependency mapping code. With a framework, you give it a set of rules and it resolves the dependencies for you. – David Arno Sep 7 '18 at 7:43
5

It all depends on what you're trying to achieve with the dependency injection.

From what I've seen, the most current usage of a DI framework is to be able to swap real implementation with stubs/mocks when unit testing the code:

1. When DI is used to replace dependencies by stubs/mocks in unit tests

If you don't want to use a DI framework but you want to get the benefit of DI to mock the dependencies of a class under test, you can use default values. Instead of:

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

you'll have:

class ImageUploader
{
    public ImageUploader(IImageProcessor processor = null)
    {
        this.processor = processor || new ImageProcessor();
    }
}

This way, in application code, you don't have to pass anything to a constructor: default dependencies will be used. When doing unit tests, however, you'll use the constructor to inject the stubs and mocks you want.

Obviously, this works only in situations where there is a default implementation for an interface. If there are no defaults (such as the case where the app has to make a choice between PostgreSqlProvider and OracleProvider based on the application configuration), then you still have to pass an object to a constructor.

Sometimes, you may encounter situations where you have the defaults nearly everywhere in the chain, but then, somewhere, there is a class which needs a custom dependency. For instance, ImageProcessor may need a storage mechanism, which is either InMemoryStorage or FileStorage. In this case:

  • You can pass the dependency through the chain, so it would belong to the controller, in your example, to do the choice. The drawback is that you end up with the chains as the one you used as an illustration in your question.

  • You may use a singleton which will store the instance of a storage mechanism, freeing the chain and ensuring that only the class which needs it (in our case, ImageProcessor) would access it. I would advise against it, since it would make it difficult to impossible to test ImageProcessor.

  • Or you have an object such as ApplicationConfiguration which feeds the classes which need a dependency with the corresponding instance. In your case, it would mean having:

    interface IApplicationConfiguration
    {
        IStorage getImageStorage();
    }
    
    class ApplicationConfiguration: IApplicationConfiguration
    {
        IStorage getImageStorage()
        {
            ... // Based on a config file, return either `InMemoryStorage` or
                //`FileStorage`.
        }
    }
    
    class ImageProcessor: IImageProcessor
    {
        ... // All logic which should be unit tested goes here.
    }
    
    class DefaultImageProcessor: ImageProcessor
    {
        // Contains no business logic; simply uses
        // `ApplicationConfiguration.getImageStorage` to retrieve the instance
        // of the storage.
    }
    

    This third solution gives you the benefit of being able to unit test all of your code, with the exception of DefaultImageProcessor, which has its role limited to passing the correct dependency to the base ImageProcessor class.

Note that while it works for the purpose of being able to swap dependencies between implementation and stubs/mocks, this is pretty much the only benefit of this approach, and all other benefits of dependency injection are lost.

Drawbacks

One of the drawbacks you need to consider is that at the level of packages/assemblies, it links the classes with their dependencies in the exact same order it would do if no DI was used.

For instance, with DI, I can create an assembly/package which contains ImageService and IImageUploader. This assembly/package knows nothing about ImageUploader. The benefit of this—and this is one of the most important benefits of dependency injection—is that I can modify the assembly/package containing one of the implementations of IImageUploader without affecting in any way the assembly/package which contains ImageService. I don't need to rebuild it. I don't need to publish a new version of a package to the package repository. This makes perfect sense, because ImageService was not changed.

With default parameters in the constructor, this benefit is wasted. I have no other choices as having the assembly/package-level dependency from the package containing ImageService to the one which contains ImageUploader. Now, when I change something in the implementation of ImageUploader, the package containing ImageService has to be recompiled as well.

In other words, if the default parameters approach looks like DI, it's not a IoC (and therefore not a DI, since DI is just a form of IoC), since there is no inversion.

Does it matter?

  • If your only goal is to be able to swap dependencies during tests, it shouldn't matter much.

  • If your goal is to have a clean architecture, then default parameters won't work for you.

2. When DI is used to do the actual IoC

In a case of IoC, you'll end up at some level passing instances to constructors anyway. The only way to avoid writing the code yourself is to delegate the job to a library—this is exactly what DI framework does. Its goal is not to make the relationships simpler or anything like that. The goal of a DI framework is to hide the code related to DI from you (usually by using Reflection, which causes its own problems).

Obviously, you're free to use different approaches at different levels. For instance, you may decide that it makes perfect sense to have a strong relation between ImageUploader and ImageProcessor; they would find themselves side by side, within the same assembly/package, and in this case, using a default parameter in a constructor would be a wise choice.

At the same time, you may want ImageProcessor to rely on a storage mechanism which depends on the configuration of the application. In this case, the solution with DefaultImageProcessor I described above may work.

And then, you may have a case of a log class that you'll like to access from any class within the application. Here, a static factory may work well, as soon as you're able to replace the real loggers by stubs for unit testing.

Finally, you may find that ImageService is completely independent from ImageUploader and ImageProcessor. You'll put it in a separate package, and use IoC. The ImageController will then pass an instance of IImageUploader when initializing the service.

  • Agreed that objects would be reused, but for the example I only showed a single instance. Those chains would be duplicated across Controllers though, because they share no BaseController. – Benjamin Diele Sep 6 '18 at 17:57
  • How would the default implementation work where e.g. ImageProcessor has dependencies? Suppose that the ImageProcessor requires a Config or a FileSystem. It still needs to get them from somewhere, doesn't it? – Benjamin Diele Sep 6 '18 at 17:59
  • @BenjaminDiele: Good point. Let me edit my answer to address this specific aspect. – Arseni Mourzenko Sep 6 '18 at 18:00
  • 3
    This "default implementation" is a code smell. You're effectively negating the benefits of DI. – MetaFight Sep 6 '18 at 18:32
  • 1
    @MetaFight: it depends on the context and the usage. If DI is used exclusively to be able to unit test classes independently of their dependencies, the default implementation works rather well. The real problem of this approach is when you try to properly decouple the code at packages/assemblies level. Most of the projects I've seen which use DI framework don't do that anyway. – Arseni Mourzenko Sep 6 '18 at 18:50
3

The first thing to note is that you are confusing dependency injection (DI) with DI using a framework. Both are DI. The former is often referred to as "pure DI" to highlight the fact that no third party library/framework is used to achieve the injections. It's all handled in the user's code.

Taking your code example:

var imageService = new ImageService(new ImageUploader(new ImageProcessor(new Config())));

That is DI in action: ImageProcessor is dependent on an IConfig, so you provide it via the constructor. ImageUploader is dependent on IImageUploader. So you supply that dependency. ImageUploader has no knowledge of Config as it doesn't need to. By using DI, you've decoupled those two.

In this code example, you've used pure DI: you've manually mapped the dependencies to each other. All a framework does for you is let you describe rules for how things map together and it then generates the above constructor chain for you.

The main problem that I see is that all complexity and dependencies bubble up to every endpoint

There should only be one endpoint. Your class, ImageController, should therefore look like:

class ImageController
{
    public ImageController(IImageService imageService)
    {
        ...
    }
}

and the root of your application then has responsibility for resolving the dependencies of an IImageService implementation, creating it and injecting it into ImageController.

Parts of your code have to be dependent on each other. The purpose of DI is to abstract those dependencies and to resolve them at runtime. Yes, this creates complexity; all abstractions create complexity. It's a trade-off between the two.

Your comment though makes me suspect that you aren't really using DI with your DI framework. I may just be misunderstanding you, but I suspect your current implementation of ``, looks something like:

class ImageController
{
    public ImageController()
    {
        var imageService = DIContainer.GetInstance<IImageService>();
    }
}

If that's the case, then you aren't using DI; you are using the container as a service locator. This has the advantage that it hugely reduces complexity. But it comes at the cost that, for example, theres nothing to stop Config grabbing a copy of ImageService too, so the code becomes a mass of coupled spaghetti.

As I previously said, it's all about trade-offs. DI adds complexity. But it helps decouple code, making it easier to test and maintain. Choose your poison...

2

TL;DR;

Something that brands itself as a Dependency Inejction Framework has no idea how to let you, as a programmer, work.

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

By actually using dependency injection, and type hinting clearly what dependencies there are for their types. One does not need any framework, of any kind, to write maintainable, SOLID, code.


It seems there is a strange perception of what Dependency Injection means, or represents, so I'd like to state this:

Dependency Injections means type hinting what dependencies there are for a class to execute all actions that are demanded of it.

Really, that's all there is to it. Of course, that encompasses a lot of things, in terms of creating an architecture, and it has gigantic effects on testing, but dependency injection is really just that: defining clearly what dependencies a custom type has to be instantiated.

There exists dependency injection containers, that will mostly define how you, as a programmer, link together parts of your types, and how types are resolved. These containers allow you to decide how interface resolution will be decided, what concrete type should be used for this or that interface in this or that type, but that's all they are, containers. These should be used in an actual framework (either built by you or someone else), to create a relevant application.

These containers can use configuration files and/or reflection to inspect types, look at what they need, instantiate and feed types into instances (hopefully caching the instances that need be shared between everyone). And there is dependency injection, at its purest usable form.

1

Maybe there is a way to delegate the controller's creation or configuration even further. Many frameworks allow you to provide factories for that.

If you definitely reached a point where some framework will create instances by using a no-arg constructor, you can still manually call a factory to deliver the direct(!) dependencies you need there.

In any case the factory may use a configuration which has been composed at app startup time. This way you still have the complexity of dependency wiring in only one spot.

TLDR: you are violating the DRY principle, centralize your creation logic into central factories and reuse those where necessary.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.