
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 <boost@lists.boost.org> wrote:
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<log_file_name>.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