niedz., 21 lut 2021 o 00:27 Krzysztof Jusiak via Boost < boost@lists.boost.org> napisaĆ(a):
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 :(
Can you demonstrate how this library works with types that are not default-constructible? E.g., the tutorial example has this code: ``` int main() { logger logger_; renderer renderer_; view view_{renderer_, logger_}; model model_{logger_}; controller controller_{model_, view_, logger_}; user user_{logger_}; app app_{controller_, user_}; } ``` Suppose I want all my objects to have labels so that I can easily identify them. I introduce a type label, which actually only stores a std:string, following the advice from the tutorial, but it serves only one purpose: to designate labels: ``` class label { std::string _s; public: label() = delete; // no empty labels label(std::string_view s); }; label operator ""_l(const char * s, size_t l ) { return label(std::string_view(s, l); } // to use "label"_l ``` Now, suppose that all the classes in the example -- logger, renderer, view, model, controller, user, app -- only expose constructors that take such a label as the first argument. Additionally, suppose that classes model and controller need a distinct logger. So when I manually initialize this herd, I do it like this: ``` int main() { logger log1{"app.logger.lo"_l}; logger log2{"app.logger.hi"_l}; renderer renderer_{"main_renderer"_l}; view view_{"main_view"_l, renderer_, log1}; model model_{"main_model"_l, log2}; // note: the other logger controller controller_{"main_controller"_l, model_, view_, log2}; // note: the other logger user user_{"main_user"_l, log1}; app app_{"main_app"_l, controller_, user_}; } ``` How is it going to look like when replaced with the proposed DI-library? Regards, &rzej;
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
.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
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost