In short, yes, the mathematical features of your programming language will make a huge difference in how you use dependency injection. I'll try to motivate why language choices matter in making optimal use of dependency injection, and also motivate complex dependency injection with a basic look into how to implement a plug-in architecture, and how we can use
Software engineers mainly talk about design patterns like Resource Acquisition Is Initialization, Service Locator, and Abstract Factory.
Mathematicians use phrases like universal quantification and existential quantification. So, as I explain plug-in architecture, I'll try to tie back "mathy" concepts with DI.
I'll start by explaining what Service Locator and Factory Method are, structurally, by quoting Mark Seeman and describing their type signatures:
An Abstract Factory is a generic type, and the return type of the
Create method is determined by the type of the factory itself. In
other words, a constructed type can only return instances of a single
type.
A Service Locator, on the other hand, is a non-generic interface with
a generic method. The Create method of a single Service Locator can
return instances of an infinite number of types.
Service Locator
public interface IServiceLocator
{
T Create<T>(object context);
}
Abstract Factory
public interface IFactory<T>
{
T Create(object context);
}
Or, in the degenerative case of no parameters:
Service Locator
public interface IServiceLocator
{
T Create<T>();
}
Abstract Factory
public interface IFactory<T>
{
T Create();
}
In a language without generic types, like JavaScript, there is literally no difference between the two signatures. The only detail that then matters is the name given to the interface. This is a shame, because you can't describe at code write time exactly how you intend for your architecture to work.
OK, so why is it qualitatively bad to only use names? After all, many programmers prefer dynamically typed languages because they're lightweight and have very few rules. Why would that same group of programmers care about only using names for dependency injection instead of types? Why would not being able to tell the difference between Service Locator and Abstract Factory matter any more than whether a number is an integer or a decimal or a floating point number? After all, these DI design patterns are just extra types, how does using Javascript actually make programming harder?
The biggest problem with only using names can be seen in how Javascript frameworks like Angular.js implemented dependency injection: they use hierarchical search. The main reason Angular developers encourage this is because there is no other way to provide a scope for name resolution.
As an experienced engineer who understands DI already, I hate having to read all the non-mathematical descriptions in the Angular.js DI docs. It just wastes my time, takes a long time to read, and is full of idiosyncratic techniques. I'd rather just learn generic programming with types, once, and apply that knowledge to any language I have to use for work.
Even the example for Angular.js service isolation is contrived: because they don't have type safety, they require every view component to explicitly pre-define, by typing out "providers", in the component list. You have to type it out, because there are no types to save you from having to type it out.
ES7 gets JavaScript developers a bit closer thanks to TypeScript and ES7 Metadata. One example of an IoC library in JavaScript is Inversify. In effect, TypeScript (a typed language) compiles down to JavaScript and creates a mapping layer that looks at ES7 metadata in order to resolve types. This scraps some of the boilerplate associated with JavaScript frameworks relying upon lexical names rather than types.
There's more I want to write here, but please write in the comments if I'm speaking to you at your level of understanding. We have not even covered yet more functional approaches to dependency injection and common functional design patterns such as Continuation Passing Style, which is the functional way to invert dependencies to the user and allow the user to use your library in ways you never imagined. There is also the trick of encoding existentials, by describing existentials in terms of the universals which can be used to operate upon them. using crates: if your type system does not directly support saying "For all types x, such that…", how can you still say it? The trick is to avoid passing around the function directly. You let consumers pick the return type using their own universally quantified function. In some way, this is what C# DI frameworks like DryIoC allow you to achieve, just in an OO style.
I have one last point about OO DI. Most frameworks just expose interface composition, rather than functional composition. What I mean is that you can say this:
public interface IHas<T> { T MyT { get; set; } }
public class FooV1 {
T MyT { get; set; }
}
public class FooV2 : IHas<T> {
T MyT { get; set; }
FooV2(T myT) {
MyT = myT;
}
}
It would be ideal if I could just use FooV1 and automatically inject a T, to avoid having to type things 4 times in a row:
- In the interface definition
- In the class definition describing that it implements the interface IHas
- In the constructor describing that you need a T dependency to create this object
- Assigning the external dependency from the constructor argument myT to the class property MyT.
I would prefer DI frameworks that can automatically detect I want a T, and give it to me, to those that require I create extra boilerplate everywhere. I would say the boilerplate approach uses interface composition, whereas I prefer function composition.
I'll leave you with a simple quote of how many functional programmers go wrong, which is eerily similar to how Javascript programmers go wrong:
Eirik Tsarpalis (@eiriktsarpalis) tweeted at 2:48 PM on Thu, Feb 15,
2018: F# modules are essentially static classes that cannot be
parameterized on the module level. As such the tendency is to include
singletons or expose all dependencies as function parameters. Often
they combine both approaches. It makes for highly untestable code.
(https://twitter.com/eiriktsarpalis/status/964134272658485248?s=03)