3

So, I'm binding an InMemoryCachedCarRepositoryDecorator to an ICarRepository, and obviously I should bind it as a singleton, because otherwise it wouldn't work (as intended).

So this got me thinking:

Shouldn't the implementation dictate the scope of the binding?

(It doesn't seem like major DI frameworks support this, and I'm wondering if there's a valid reason for them not to. It should be easy though: enforce any implementation to implement some Scope-like interface, like InMemoryCachedCarRepositoryDecorator : Singleton)

6
  • You're in control of both the DI container configuration and the implementation. Why would you have your classes depend on a library provided interface when you can achieve the same with a simple configuration?
    – devnull
    May 26, 2021 at 14:00
  • Because it eliminates a place for bugs to hide. If I'm not in control, it means I also can't mess it up. If I create a nuget package, one that's easy to use in an DI project, I wouldn't want users to set them up wrongly as well. May 26, 2021 at 14:21
  • 1
    I think it's easier to document the DOs and DON'Ts than binding your users to a DI framework.
    – devnull
    May 26, 2021 at 14:27
  • Good point, thanks! I'm off picking my brain about that one for a while... ;) May 26, 2021 at 14:38
  • 1
    If your class lib requires a specific setup, you could create other NuGet packages for certain DI containers which provide an easy point of integration. This could be as simple as an extension method to the container object. But keep this as a separate package from your main class lib and one package per DI container. May 26, 2021 at 22:50

4 Answers 4

4

The singleton design pattern limits a class to one and only one instance. The compiler and language constructs like access modifiers enforce this restriction. That shouldn't be confused with a singleton registered in a dependency injection container, which means for a given scope the container should return the same object instance.

To be clear, the interface should not indicate the scope or lifetime of an object. Indeed the scope of the binding in the DI framework should be determined by the implementing class, but not in the way you think. The composition root of your application has the broad knowledge of dependencies and is in the best position to determine scope. The composition root (think main() or Application_Start()) is the point where all these dependencies are wired together. You know concrete implementations and have the bigger idea of how all these objects work together.

So I would expand on your statement a little. The concrete implementation and composition root determine the scope of an object in a DI container.

From one of your comments:

If the composition root decides the scope, the composition root has to know about the inner workings of the implementation, and if I'm not the owner of it, I don't. So you'd depend on people writing and reading documentation.

This assumption is correct. Sometimes you must rely on documentation. The composition root must know a tremendous amount. Something, at some point, must know this stuff. The composition root must know the specifics of how dependencies are implemented, and therefore their lifetime and scope. The composition root must also know what scopes are even available. For instance, it should know whether it is a web application, desktop app, command line application, etc. And for each kind of application it should know that there is a scope that runs the entire lifetime of the application or just a single use case. The composition root must also have knowledge about the underlying framework as well.

So the implementation and the composition root determine how long a dependency lives, and how many instances of that dependency are allowed.

10
  • I'm trying to come up with an example where I would specifically want the composition root to not only dictate what implementation to use, but also how it should work (the scope), and I haven't found it yet. I know it can, and I know they usually go together, but I'm starting to think that may just not be the best way to do it. If the composition root decides the scope, the composition root has to know about the inner workings of the implementation, and if I'm not the owner of it, I don't. So you'd depend on people writing and reading documentation. May 26, 2021 at 14:35
  • @JoepGeevers: I updated my answer with more info. May 26, 2021 at 14:58
  • Thanks, @greg-burghardt! I'm still going to challenge you though ;) "Something, at some point, must know this stuff"; I agree, but I would argue that this something already exists: the implementation itself. And even better: it knows statically. "The composition root must also know what scopes are even available". Again, I completely agree, but say there's no Request scope available, and some implementation depends on it, I'd say that the composition root shouldn't be able to use that implementation at all, not choose some other scope that won't work... May 26, 2021 at 15:12
  • 2
    @JoepGeevers: "but I would argue that this something already exists: the implementation itself" The core point of IOC is to separate the composition from the implementation, specifically so that you don't get a conflict of interest between doing ABC and deciding that ABC should be done. Think of it like an employee: more often than not, the one doing the work is not the one hiring themselves, deciding what work they should do, or who they should work with. That decision comes from management above (= the composition root).
    – Flater
    May 26, 2021 at 17:00
  • 2
    @JoepGeevers: That being said, part of the composition root's prerogative gives it the freedom to choose to let the library define its own composition, but that freedom is a privilege, not a given right. At all times, the composition root retains control/consent over the library doing so, and can override or revoke it at any time.
    – Flater
    May 27, 2021 at 9:06
2

To dictate the lifetime, the implementation must be able to talk with DI framework(s). This is wrong on many levels.

Considering that DI framework landscape has no limits and only grows over time (new frameworks appear, existing frameworks evolve), you will be putting enormous burden on the implementation author. (Including 3rd party vendors, if that's unclear.) In practice this means that only handful of popular or author's favored DI frameworks will be supported, if any, causing stagnation. Now factor in the sheer amount of components that would have to implement this lifetime reporting feature — the amount of wasted effort is unfathomable.

All this trouble for what, to spare a few minutes of reading documentation?

Alternatively, you'll have to wrap every implementation to add the lifetime property, or annotate with attributes or tag interfaces. Which is same as normal configuration, but all over the place, and no easy way to override for specific context (e.g. testing).

The power of DI comes from decoupling, and the main recipe for decoupling is separation of concerns. Components should not be concerned with managing their lifetime.


In your specific example, the actual cache implementation may be extracted into a singleton, while the decorator could be a proxy that joins the cache and the repository together.

diagram


Also to address a misconception from the comments. Component's lifetime is not a component's implementation detail. If a component wants a specific lifetime scope, it should be stated in it's public contract. But most often (I'd say always), component should not care at all, because the composition root has better understanding of a system and therefore should always have higher authority in making that decision.

This is obvious when working with slightly nested IoC scopes. Imagine GUI multi-project application, each project having it's own database. There is one scope for entire application, and one scope per each project. The project-related cache goes into a nested scope, not a singleton.

2
  • first of all thank you so much for your extensive answer. Your picture captured the essence of my problem: Indeed I think I should inject a caching thingy into my caching decorator, to abstract that concern away, making it way clearer to the composer what the intended scopes should be. Thanks! May 28, 2021 at 20:59
  • So my CachedCarRepositoryDecorator would just say: Hello there, composition root: I need some application lifetime caching thing to work properly, now give it to me. thanks, bye. May 28, 2021 at 21:02
2

DI is no worse

The world before DI. First, let's consider the world before DI and before IoC. To get an instance of an object, the caller would have to do something like this:

var instance = new MyClass();

In this case, it is the caller that determines the lifespan of the object, not the implementation of the object itself. The caller has to decide whether to store the instance in a local variable, member field, or static field, or in some other holding area like HttpContext.Item.

So the same problem existed before DI.

But... implementation could still control instancing. If you wanted the implementation to control the instancing of its objects, you could write a static factory, that may or may not always return the same instance. If you wanted "same instance" behavior, you'd actually implement a singleton (remember those?):

class MyClass
{
    static MyClass _instance = new MyClass();

    public MyClass GetInstance() => _instance;
}

Fast forward to the DI world. It's no different. In most cases, it's the caller that determines the rules for instantiation. After all, it's the caller that has to decide where to store the reference (even when the caller is an IoC container). So we have not made anything worse.

And the implementation can still control instancing. If you happen to have a class that absolutely requires a particular type of instancing behavior, you can always implement and inject a factory that controls the instancing. For example, if you absolutely need singleton behavior, you could write this:

class MyClassFactory : IMyClassFactory
{
    static MyClass _instance = new MyClass();

    public MyClass GetInstance() => _instance;
}

The caller can then register this factory any way they want and you will still end up with one and only one instance of MyClass.

//Startup
container.RegisterInstance<IMyClassFactory,MyClassFactory>();

//Some object in your program
class Foo
{
    protected readonly MyClass _instance;

    public Foo(IMyClasssFactory factory)
    {
        _instance = factory.GetInstance();
    }
}
       

This way the implementation controls the instantiation rules via the factory, just as before DI. And this fact is made obvious to the caller, and impossible to ignore.

4
  • And most DI containers allow you to specify that the container should create one and only one instance, eliminating the need for a factory. It becomes a singleton within the object graph created and managed by the container. May 26, 2021 at 22:39
  • @GregBurghardt The way I understand the question, OP is hoping to avoid requiring special container configuration, and would like the implementation to take care of it. Ergo the factory. Obviously it is not needed if you're willing to let the container manage the lifetime.
    – John Wu
    May 26, 2021 at 22:45
  • That's how I interpreted the question as well. Basically the thing the OP didn't want to do is the thing they should do. May 26, 2021 at 22:46
  • @johnwu it's not that I'm hoping to avoid configuration, I'm trying to find a way to avoid an obvious wrong configuration. Or, more probably, a missing configuration that defaults to transient in most cases, that wouldn't work with the example that I gave. But I'm definitely starting to change my opinion based on all the answers. It does take me some time to properly read and internalize them May 27, 2021 at 9:02
0

If your implementation won’t work without something that does X, whatever X is, then it has to says so. If the best way you think of to describe X is multiple interfaces or a concrete class, then that’s what your class needs to say it wants. The implementation says what it needs, it is then up to something else to provide it. That is the whole point of DI.

That said, just because you are using a DI and interfaces, that doesn’t mean you’ll necessarily be getting different concrete classes at runtime. Quite often even though it’s an interface, in practice there’s just one class that implements it, except for a fake class created during testing. If you need something, and you know that you are going to get it, don’t stress over the fact that there isn’t really a good way to ask for what you need (happens all the time with non-functional requirements, if you are going to add a million items to an ISortedCollection then it better not use a bubble sort). Make a note of it, and move on.

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.