Vanilla DI is one of the implementation approaches for Dependency Injection. And also, Vanilla DI is an approach for design.
This approach has the following pros:
- Pros
- Extremely simple
- Extremely easy to use
- Dependency-free
- Good debuggability
- Good readability for beginners
- Good portability for most languages
- Good way to layered architecture
But it is not a silver bullet, so this approach has the following cons:
- Cons
- Maintainability highly depends on design skill
So, Vanilla DI may not work well if you work on a huge project. But using Vanilla DI is still a good choice for many situations.
We provide examples for the languages:
However, we always welcome to add examples for other languages above. Feel free to send a pull request.
This approach is extremely simple; Inject depended components via constructors (this approach is known as Constructor Injection).
You can achieve it by writing code like the following for each dependent components:
// These are depended components.
class X {}
class Y {}
// This is a dependent component.
class Z {
constructor(dependency) {
const {x, y} = dependency;
this.x = x;
this.y = y;
}
doSomething() {
// Do something with this.x and this.y.
}
}
// All dependency should be injected when
// the dependent component is constructing.
const z = new Z({
x: new X(),
y: new Y(),
});
z.doSomething();
That's all.
If you felt something hard to write constructors or tests, Vanilla DIer should find several design smells.
Because the most important part of Vanilla DI is keen sensitivity of design smells. Vanilla DI does not hide any design smells, but DI containers can hides often them.
Number of arguments of Vanilla DI component should be less as possible.
Ideally, all components should know only few others (in other words, direct dependencies should be few as possible). You can see a huge arguments list if your code violates the principle. The following code is an example of huge arguments lists:
// PROBLEM: This is huge arguments lists.
class SomethingGreatService {
constructor(foo, bar, fooBar, baz, qux, quux) {
this.foo = foo
this.bar = bar
this.fooBar = fooBar
this.baz = baz
this.qux = qux
this.quux = quux
}
// ...
}
Understanding the component is hard but also understanding an user of the component is hard (typically you can face it when writing a setup phase of test code):
const service = new SomethingGreatService(
new Foo(),
new Bar(new Corge()),
new FooBar(new Grault(new Garply())),
new Baz(new Waldo()),
new Qux(),
new Quux(new Plugh())
);
The huge arguments list says that; some abstraction is needed for depended-on components.
Vanill DIers should try to encapsulute several depended-on components into an abstraction. The abstraction may be a Dependency Bag describing the next section.
Arguments of Vanilla DI components should not be affected by dependent components changes.
One of a reason to avoid Consructor-Injection is parameters modifications upward call hierarchy. For example:
// NOTE: Child class needs to depend on GreetingService.
class Child {
constructor(fooService, /* TODO: greetingService */) {
this.fooService = fooService;
// TODO: this.greetingService = greetingService;
}
// ...
}
// PROBLEM: But Parent class does not have GreetingService.
// Should we add GreetingService to the constructor?
class Parent {
constructor(fooService) {
this.child = new Child(fooService);
}
// ...
}
// PROBLEM: But also GrandParent class does not have GreetingService.
// Should we add GreetingService to the constructor too?
class GrandParent {
constructor(fooService) {
this.child = new Parent(fooService);
}
// ...
}
// ...
You can handle the situation by parameter modifications, but it is boring.
Instead, you can achieve it by adding a dependency bag object:
// APPROACH: Wrapping several components necessary to instantiate Child into PersionFactoryBag.
class Child {
constructor(personFactoryBag) {
const {fooService, greetingService} = personFactoryBag;
this.fooService = fooService;
this.greetingService = greetingService;
}
// ...
}
// GOOD: The arguments list of Parent does not affect.
class Parent {
constructor(personFactoryBag) {
this.child = new Child(personFactoryBag);
}
// ...
}
// GOOD: The arguments list of Parent does not affect.
class GrandParent {
constructor(personFactoryBag) {
this.child = new Parent(personFactoryBag);
}
// ...
}
// ...
But should not abuse dependency bags. Abuse of dependency bags lead to violate Interface Segregation Principle.
Yes and no. It completely depends to your situation.
For example, you should use a DI container if it is already used in your project. For another example, you should not use any DI container if the project is across many languages and project members are not expert for each languages.
It means, using a DI container is not always the best solution. Don't be afraid to be away from DI container.
Vanilla DI = Constructor Injection + Phillosophy
The phillosophy is emphasizing design smells; Facing something hard to inject means a design smell.
Any suggetions are welcome. Feel free to open issues to discuss about Vanilla DI.