5

After some googling I found some debates about whether constructor injection or property/field injection is better, but there is yet another alternative that strikes me as more beneficial.

In most programming environments, there is something called "the call context", which is information implicitly passed from caller to callee.

Sometimes this context is simply thread-local storage, sometimes, in the case of cross-thread asynchronous programming, it's more complex than that.

In any case, such contexts store information such as culture, user, or, for web applications, the current request.

These types of information are rarely carried around explicitly but accessed through global methods that are accessing said contexts.

In the use case of dependency injection, this means a static/global form of service locator (my example is in C#):

public static Sl
{
    public static Dependency Get<Dependency>();

    public static IDisposable Push<Dependency>(Dependency dependency);
}

The usage would be:

public void SomeFunction()
{
    Sl.Get<Dependency>().UseItSomehow();
}

To call SomeFunction, the dependency can be set or changed for the callees like this:

using (Sl.Push<SomeDependency>(someImplementation)
{
    SomeFunction();
}

I think this pattern superior to other types of dependency injection in those cases where a dependency is uniformly required for a large part of a larger call subtree.

For example, a repository, logger or configuration interface is rarely required only for a single function, but also for all its subsequent nested calls.

Especially with contructor injection this leads to some clutter that can be completely avoided: SomeFunction above can call other functions that will get the dependency automatically. It can also change the dependency in those cases where it actually is necessary.

I want to address some of the concerns I think I read somewhere, and my question would then be if anyone can think of others.

  • There's a dependency on Sl

    This strikes me as a fundamentalist's objection. Most software also depend on integers, strings and lists, without such things being properly abstracted away and passed around with proper respect to the pure Church of Dependency Injection. Clearly some low-level things one needs to depend on directly - hopefully there are well-designed and ideally they reside in the runtime library of the respective language. If not, they may still be the lesser evil.

  • Required dependencies should be explicit

    The only really explicit form of DI is contructor injection or passing dependencies directy in method calls. Unfortunately, this is also the most verbose and unflexible way of doing it. Dependencies that are added later change constructor or method signatures, requiring a refactoring of potentially several layers of function calls that do nothing but passing down objects verbatim. In static languages, this is tedious. In dynamic ones, it's even a source of bugs.

    Furthermore, if the dependency is required, the respective feature will fail whenever it's used anyway, making it very likely that a missing-dependency bug won't accidentally go to production.

    So ideally, yes, required dependencies should be explicit. But there's always a tradeoff, and am I really the only one who thinks that the constructor injection folk don't have their priorities straight?

  • One shouldn't rely on magic

    Call contexts and thread-statics are not magic, they are merely advanced and perhaps more technical than what is covered in your typical programming course at university.

    Also, the implementation is hidden behind the static service locator. Users don't have to know how it works to use it.

So my question: Are there any other objections? Are there any legitimate, real-world reasons why DI with a static/global service locator would be bad?

  • To which class does SomeFunction() belong? Why can't that class provide the context of SomeFunction() and provide the dependencies in member variables (injected, for example, through its constructor)? – Doc Brown Apr 29 '16 at 17:34
  • "For example, a repository, logger or configuration interface is rarely required only for a single function, but also for all its subsequent nested calls." <- I think this says a lot about the layering and boundaries (or lack thereof) in your application – sara Apr 29 '16 at 18:22
  • @kai I indeed use logging and configuration in many parts of my applications. I think that's normal. Also: insults, really? – John Apr 29 '16 at 18:28
  • @John it was not meant as an insult. I think that passing a repository accross several layers and down several levels of nested method calls is a code smell. It sounds like there are some issues with SRP and staying at an appropriate level of abstraction. I think a config shouldn't be passed from top to bottom, it should be read at the top and the results then govern what implementations and strategies are used at the bottom. Rarely should you actually let a class wander through your entire object graph. – sara Apr 29 '16 at 18:42
  • @Kai Repositories are the weakest of the three examples in this case, but what's wrong with a piece of specialized code requesting a configuration provider that is set somewhere at the application root or in the unit test that tests the respective component? – John Apr 29 '16 at 18:48
4

Are there any other objections? Are there any legitimate, real-world reasons why DI with a static/global service locator would be bad?

Ugh, yes.

Statics/globals are horrible. They assume that the runtime of your application is homogenous - all of your instances require all the same sort of instances all over. That is naive. They interfere with concurrency, since any state in any of these things is inherently shared state. They interfere with testing, since you can't effectively mock different instances running concurrently or even different instances down the callstack. And then there's the usual debugging issues with global state.

Hiding your problems is not fixing your problems. If you have something that everything needs, then you have widespread coupling. If you have objects that need a bunch of dependencies, then you have widespread coupling. If you have a huge object hierarchy that makes it difficult to pass things around where they need to go, then you probably have a poor design. DI does not fix these things! All you're doing is hiding it behind some magic that passes dependencies around. You're not removing the dependencies, just making them less visible and a runtime error rather than a compile time error.

  • 1
    I'm not talking about a global state, I'm talking about call contexts. Those are not shared accrosss threads (at most they are passed in the case of asynchronous programming), so there is no problem with concurrency. Providing mockups for testing is trivial, I've even given an example of how to do it (the code excerpt containing the using). – John Apr 29 '16 at 18:13
  • Further, I'm not talking about the case where everything needs something, but the case where something that is deeply nested in function calls need something. – John Apr 29 '16 at 18:15
  • Also, the removing of dependencies itself is indeed not explicitly talked about in my question, as I thought that's self-explanatory: You have Dependency be an interface. You can do that with the proposed method as well as contructor injection, so that's not really the issue here. – John Apr 29 '16 at 18:16
  • @john - Except that Call Contexts don't always flow along async calls. Either your dependencies go along to other threads (and you have shared state problems) or they don't (and you have missing dependency problems). – Telastyn Apr 29 '16 at 18:17
  • 2
    @John Call contexts are goddamn terrible. HttpContext.Current in ASP.NET is a blemish on the face of humanity that should be seared off with a laser beam. It's a hideously bad, lazy design that causes nothing but problems. HttpContext.Current is also not completely reliable (what a shocker!) we've had some cases in our controllers where we've had to hack around it missing once asynchronous controllers are involved. Not to mention that disgusting shit about ConfigureAwait(false). It's just terrible and broken and wrong. – DeadMG Apr 29 '16 at 19:10
3

I think you are missing the benefit of dependency injection--that anything the class depends on is given to it, rather than assumed. What you're proposing is the service locator anti-pattern. There are a few cases where I might be tempted to use it (identity), but I'd prefer a clean contract for one reason--I never know how someone is going to use my code. I've seen this in my workplace, where someone designs code with ambient references to web concepts (like HttpContext) deep inside of a call chain instead of injecting it. A year or two later, and the code can't be reused in a console application because of the static dependencies. The same is true in trying to write unit tests.

  • There's a dependency on S1
    • This isn't a fundamentalist objection, it's a failure to understand the point of dependency injection (which is different from preferring interfaces, which I think you're conflating with DI). Strings, integers, and lists should be passed in the constructor where they are needed. Your constructor should always expose the contract of the class--what requirements it has should be made clear and confirmed before the object is created. DI does not always use a DI container.
  • Required dependencies should be explicit
    • See my response above about never knowing how the code will be used, especially in a library. Although it is verbose, that's the purpose--to accurately describe the contract. Adding dependencies shouldn't change signatures; new methods/classes should be introduced to obsolete the old ones. This is a core principle of semantic versioning and is a good way to do library development. As far as catching implicit dependencies, I'd rather catch something at compile time rather than receive an angry bug from a user.
  • One shouldn't rely on magic
    • It's magic in the sense that your dependencies just appear, rather than be asked for. That's problematic when the dependency you get has special constraints on it (like HttpContext).

Here are some links to other discussions about the server locator anti-pattern and reasons to avoid it:

http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/

https://www.youtube.com/watch?v=RlfLCWKxHJ0&feature=youtu.be

https://stackoverflow.com/questions/22795459/is-servicelocator-anti-pattern

  • You're missing my point about integers, strings and lists: Not even the most fanatical DI-enthusiasts are refraining from using concrete implementations of integers, strings and lists in their code - rather than just depending on interfaces that create and use them. – John Apr 29 '16 at 18:06
  • 1
    "Adding dependencies shouldn't change signatures" Adding a constructor or method parameter obviously changes the signature. Supplying the old one as well just means the dependency is optional, which isn't the case we're discussing. – John Apr 29 '16 at 18:07
  • Nothing about dependency injection says that you can't use concrete implementations. You're conflating it with the maxim prefer interfaces. Those are two separate ideas. You prefer interfaces where there is logic or an external resource (for the most part). Strings and integers are primitives--they have no logic. DTOs fall in the same boat. Lists can go either way--and Visual Studio will warn you to use IEnumerable<T> instead of List<T> when possible. – mgw854 Apr 29 '16 at 18:10
  • If the dependency isn't optional, then you should probably be rethinking your class. Chances are, it's doing too much. Introducing new dependencies to existing classes isn't supposed to be easy because you will have an impact on everything upstream. That being said, there's nothing wrong with creating a new class and marking the old one obsolete. Once that's done, it's legacy code. All bets about maintainability are off, so I could condone instantiation inside of it. – mgw854 Apr 29 '16 at 18:14
  • 1
    @John A dependency is a type that a class needs to refer to in order to do it's work. This can be a purely abstract interface, a concrete class, a simple struct or a primitive datatype. It is 100 % irrelevant and has exactly nothing at all to do with DI. DI is about HOW the class gains access to the dependency (by having it INJECTED by someone else, as opposed to grabbing it out of thin air without the knowledge of the client). You are mixing different SOLID principles together. – sara May 28 '16 at 16:42
0

I think this is a great idea partly because I had exactly the same idea myself. I know it is workable at least because I've done something very similar. I'd be very interested to see what other objections are raised. As to the ones so far, I don't think they are substantial. The most interesting is within the http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ link. In short it says that a third-party consumer of

SomeFunction()

may fail to set up the service locator as its not forced to do so whereas in the case

SomeFunction(dependency)

the compiler will force it to supply the dependency. However I think having to supply the dependency in 3rd party code using SomeFunction is liable to be just as much of a problem as it is in the first party code.

In the case of a logger, you will have the classic chain reaction problem of introducing a parameter in a commonly used function. The caller of the function has to find that parameter, commonly causing it to need the parameter itself, and so on, causing a large ripple effect up the call tree. In the recommended context of using DI for a logger, the code change where you need to add logging to some function causes this need to add the logger service interface to the constructor of all classes up the chain from whereever the function is used.

This is a significant maintenance cost for DI in this use case, and potentially very difficult to manage.

This cost is not isolated to the codebase where the logger is required. In the example where SomeFunction is being used by a third party, it will also be more efficient to supply the logging service once to the service locator than to write it into the constructors of all the third party code that needs it too.

What this comes down to is does it ever make sense for a global context to exist? The answer to that has to be yes as it makes data objects which are widely used at all levels of code much easier to access without creating large numbers of parameters to pass them around.

  • The fallacy about dependencies rippling all the way through the object graph just because one leaf node right at the bottom needed it is just plain wrong. If that happens, it's because you have no layering in your application. If a top-level controller needs to know about the dependencies of a small logic helper for some strategy object 20 levels down, then you have an ARCHITECTURAL problem, not a problem with DI. as has been pointed out many times: DI does not solve your dependency problems, it makes them VISIBLE (so you can fix them). – sara May 28 '16 at 16:45
  • @kai I'm thinking of a code timer utility object which needs a logger service. Anywhere I use that code timer I am supposed to introduce constructor parameters for the logger until I reach an object initialised at startup according to 'proper' DI, am I right? It doesn't matter if it's 3 layers each time, that's a lot of maintenance. But also I fail to see that its a design error to have a deep object tree if, say, I've got a complex domain model. Are you of the view that in this case, the intermediate objects have some kind of dependency on the logger which DI is exposing? – James Ellis-Jones May 28 '16 at 17:43
  • There's nothing inherently wrong with a deep object graph, but there IS something inherently wrong with a rigid object graph (where local changes propagate through the whole system). Given a dependency graph A -> B -> C where (->) denotes a dependency (A is top-level), then A should be totally oblivious to the existence of C. This is an age-old topic and the very core of dependency management. en.wikipedia.org/wiki/Dependency_inversion_principle. – sara May 28 '16 at 17:51
  • 1
    @kai ...even through layers of code that don't log themselves? Having dependencies explicit is sensible in those cases where they are part of business logic. If it's something extremely fundamental, such as logging, or .NETs ambient transactions or ASP.NET HttpContext.Current, it's not that sensible. In many cases, the the dependency is obvious. If I do databases, I know I have transactions. If I do web development, I know I have requests. That shouldn't surprise anybody. Clutter has it's downsides too, and I'm not only talking about the bleeding fingers. – John May 28 '16 at 19:17
  • 2
    the dependency inversion principle isn't some "new fad" that came up in the "latest books a bunch of graduates read", the term was coined in the 90's by Robert C Martin, and the whole notion of dependency management and dealing with abstractions is as old as the field of computer science itself. that's not the point of the article though, my point is that it seems to me like you have a skewed or wrong image of what dependency inversion IS, so I linked to a source where one can learn more about the subject, because it's more fit for a uni course than for a P.SE comment. – sara May 28 '16 at 19:26

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.