Tuesday, 26 April 2011

Deconstructing a dependency injection-driven application

I've been using my C++ dependency injection library for a project in the last year and it's gone pretty well. There are a lot of rough edges but I thought it could be interesting to the 3 of you still subscribed to this blog to de-construct the stock quote application.

About the application

The example itself is pretty straight forward. You have a choice of 3 stock quote providers: Yahoo!, static and phone. You choose one and ask for a stock quote. Magic happens and your stock quote arrives.

Example session (with some debug output)

Welcome to the DI Stock Quote App. Simplifying and complicating software development since 2010.
Which stock quote service would you like to use?
1: static
2: phone
3: yahoo
Enter your choice (1-3) and press enter: 3
You chose: yahoo
[DICPP]: No scope constructing: di::type_key<YahooStockQuoteService, void>
[DICPP]: Constructing: di::type_key<di::typed_provider<HttpDownloadService>, void>
[DICPP]: Completed constructing: di::type_key<di::typed_provider<HttpDownloadService>, void> with address: 0x100750
Stock symbol (type quit to quit): goog
[DICPP]: No scope constructing: di::type_key<HttpDownloadService, void>
[DICPP]: Constructing: di::type_key<boost::asio::io_service, void>
[DICPP]: Singleton: constructing: di::type_key<boost::asio::io_service, void>
[DICPP]: Completed constructing: di::type_key<boost::asio::io_service, void> with address: 0x1008a0
Current price for goog: 532.82
Stock symbol (type quit to quit): quit

See how the construction of the HTTP service is automatically delayed until actually needed. This is done through a concept called a "provider" which is basically an automatically generated factory.

About Dependency Injection

A really good introduction to the dependency injection technique as implemented by Guice can be found here. It's probably one of my favourite tech talks of all time.

Anyway, to refresh your memory, here are some of the main benefits of the technique used in Guice:

  • Object construction and lifecycle management is mostly handled for you.
  • Less boilerplate.
  • Makes code more testable.
  • Scopes (~object creation/lifecycle) can be customized by the user.

In short: a lot of the time, you no longer need to allocate objects or pass some object unused down multiple layers of functions or object constructors just to use them once way deep down in some code.

Magic!

I don't really recall how it is done in Guice but in the C++ library linked above, this magic is driven by a type registry which recursively registers constructor arguments as well as user customizations.

In extreme cases, you can initialize an entire application with a few lines of code:

  di::registry r;
r.add( r.type<MyApplication>() );
r.construct<shared_ptr<MyApplication>>()->execute();

This constructs the type registry which is a kind of factory. There is a mini-DSL for describing how you want the registry to handle the type. More on this later. In this case, we are asking the registry to "learn" about the MyApplication type as well as all objects that are required for constructing MyApplication.

"Pish-posh", you say. "MyApplication has a 0-arg constructor. I could do that in my sleep."

Would you be surprised if I said that the MyApplication type actually has 3 arguments?

Well, the above is almost what the StockQuote application looks like. Here is the main function for the stock quote example:

di::injector inj;
inj.install( StockQuoteAppModule() );
StockQuoteApp & app = inj.construct<StockQutoeApp&>(); // lifetime
app.execute();

And here is the constructor for the StockQuoteApp type:

DI_CONSTRUCTOR ( StockQuoteApp ,
( boost :: shared_ptr < UserInterface > ui ,
boost :: shared_ptr < StockQuoteServiceFactory > factory ));

When we ask the "injector" to construct the StockQuoteApp instance, it automatically creates the UserInterface as well as the StockQuoteServiceFactory instance.

The di::injector type is just a thin wrapper around the registry so you can treat it as such. The only thing it really provides is a little bit of syntax to allow you to create modules in a similar manner as Guice. The guts of StockQuoteAppModule accept a registry as a parameter and register the various types. You can see the mini-DSL referred to earlier:

void
StockQuoteAppModule::operator()( di::registry & r ) const
{
// In each module we define the module's root objects, in this case,
// StockQuoteApp as well as implementations/specializations of any
// abstract classes. For example, UserInterface is an ABC and we choose
// the console-based UI here.

r.add(
r.type<StockQuoteApp>()
.in_scope<di::scopes::singleton>() // The reason we can request a reference in the main function!
);

r.add(
r.type<UserInterface>()
.implementation<ConsoleInterface>()
.in_scope<di::scopes::singleton>()
);

r.add(
r.type<StockQuoteServiceFactory>()
.implementation<StaticStockQuoteServiceFactory>()
.in_scope<di::scopes::singleton>()
);

r.add(
r.type<HttpDownloadService>()
.implementation<AsioHttpDownloadService>()
);

r.add(
r.type<boost::asio::io_service>()
.in_scope<di::scopes::singleton>()
);
}

As you can see, the mini-DSL (ugly, ugly, ugly, details) describes a few things:

  • Default implementations for various interface classes. See UserInterface and ConsoleInterface, for example.
  • Life-cycle management. Singleton is mostly used here but you can also have HTTP-session scopes, thread-local scopes or no scopes (as in HttpDownloadService).

What this means is wherever a type T with a DI_CONSTRUCTOR macro is registered, the registry will use these rules described by the DSL to construct any arguments to T.

Providers
In this library, there is a concept of a type called a provider whose sole responsibility it is to construct objects (usually within the constraints of a scope). In the app session above, I pointed out how the HTTP download service is not instantiated until it is actually needed. This is done via a provider. You can see the YahooStockQuoteService has a constructor which accepts a provider and a function which makes use of it.

That should be enough information to peruse the example itself. Check the README as there are a couple of interesting exercises you can try.

By the way, this requires a Boost checkout with a built version of Boost Build. I apologize if you can't get it to build on checkout, but I haven't really focused on having other people use it!

Comments and thoughts welcome.


No comments: