Let me try to explain DI (manual and automatic) via example.
DI vs NO-DI is all about less coupling by injecting dependencies instead of.
It's often referred as a Hollywood prince - Don't call us we will call you!
Example
struct coupled_no_di {
void api() {
d.call();
}
depedency d{singleton::get()};
};
- Coupled design (hard to test)
struct not_coupled_di {
not_coupled_di(dependency& d) : d_{d} {}
void api() {
d.call();
}
depedency& d;
};
- Much better - separation of business logic and object creation.
But we can do even better by applying
- Dependency Inversion (relay on abstractions and not concrete
implementations)
- Abstractions can be done in many different ways (type erasure, templates,
abstract classes, etc.)
template<DependencyConcept TDependency>
struct not_coupled_di {
not_coupled_di(TDependency& d) : d_{d} {}
void api() {
d.call();
}
TDependency& d;
};
The above is much better because
- It's not coupled to any specific implementation
- Can be changed for testing (mocks/fakes/stubs)
Okay, now let's take at the main. A good design will apply a Composition
Root (unique place in the app when dependencies are being created)
int main() {
my_depedency dependency{...};
not_coupled_di di{dependency};
di.api();
}
- The above is an example of manual DI which is cool already but may lead
to what is called a Wiring Mess.
Let's imagine that we have a big dependency tree because we are following
SOLID principle and we especially apply the Single Responsibility Principle
(we will have a lot of dependencies).
int main() {
my_dependency_1 d1{};
my_dependency_2 d2{};
my_dependency_3 d2{d1, d2};
my_dependency_4 d3{d1, 42, d3};
app a{d3, ...};
...
// Order of the above is important and with bigger projects might be
easily 10k LOC+
}
- Well, we can maintain the above if we want, but Automatic DI will let us
actually focus on the business logic instead!
- Any change in the constructors (a reference to shared_pointer, the order
of parameters) will require us to change the above :(
Boost.DI library can help with removing the Wiring Mess for us, though!
int main() {
auto injector = make_injector();
app = create<app>(injector);
return app.api();
}
Right now the benefits of using the framework instead of manual DI
- If we change any constructor parameter or order of any dependency we
DON't have to change anything with DI framework
- Before
not_coupled_di(TDependency& d);
- Refactor
not_coupled_di(std::shared_ptr<TDependency>);
With manual DI the wiring has to be changed to pass the shared_ptr with
Boost.DI we don't have to change the wiring code at all.
Okay, but what about the polymorphism behaviour.
Boost.DI allows a different type of polymorphism. One can inject templates,
abstract classes, variant, type erasure etc.
More can be found here -
https://github.com/boost-ext/di/tree/cpp14/example/polymorphism.
Why that's important? Because it's a better design and makes testing easier
too.
How to do it with Boost.DI?
// configuration
auto module = make_injector(
bind<interface>.to<implementation> // I'm not going to go into details,
but Boost.DI allows to inject TEMPLATES, abstract classes, etc...
);
// production, release
int main() {
app = create<app>(module);
return app.api();
}
// end 2 end testing
int main() {
auto injector = di::make_injector(
module(), // production wiring
di::bind<interface>.to<mock> [override]
);
app = create<app>(module);
return app.api();
}
- Great, we can test end 2 end with overriding some dependencies such as
time, database, networking, logging etc...
- We have a loosely coupled design!
- We don't have to change ANYTHING in the wiring code when we change the
dependency tree and/or any constructor parameters/order
I hope that helps a bit?
I also really encourage to take a quick look at which summaries concepts
behind DI.
- https://www.youtube.com/watch?v=yVogS4NbL6U
Thanks, Kris
On Sat, Feb 20, 2021 at 4:10 PM Peter Dimov via Boost
Andrzej Krzemienski wrote: ...
So, I want to apply the Dependency Injection philosophy. I get:
class State { Rect area; Point location; // invariant: location is inside area; public: State(Rect a, Point loc) : area{a}, location{loc} {} };
int main() { State s{Rect{{0, 0}, {2, 2}}, Point{1, 1}}; } ```
Now, I choose to use the DI-library (this is where I have troubles with understanding: why would I want to do that?).
You are thinking in C++ (value semantics) but you need to be thinking in Java (reference semantics, OOP, object DAGs.) Suppose you need to have a logger object
shared_ptr<ILogger> pl; // new FileLogger( log_file_name fn );
and a database connector object (which uses a logger to log things)
shared_ptr<IDbConn> pdb; // new MysqlDb( mysql_db_info mdi, shared_ptr<ILogger> pl )
and an abstract configuration query interface, that can use an .ini file, or a JSON file, or a database
shared_ptr<IConfig> pc; // new DbConfig( shared_ptr<IDbConn> pdb, shared_ptr<ILogger> pl );
Now when you need to create the config query object, you need to give it a database connector and a logger, and you also need to give the same logger to the database connector. So with [NotYetBoost].DI you'll write something like
auto injector = make_injector( bind
.to( "something.log" ), bind<ILogger>.to<FileLogger>(), bind<>.to( mysql_db_info{ ... } ), bind<IDbConn>.to<MysqlDb>(), bind<IConfig>.to<DbConfig>() ); auto cfg = injector.create<IConfig>();
although I don't really know if this will compile or work. :-)
Now imagine that same thing but with 10 objects, or 20 objects. Also imagine that your day to day vocabulary includes "business logic", "enterprise", "UML" and "XML".
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost