Bread::Board is the right tool for this job

... with a big emphasis on this!

Maybe you remember my question from last week, where I asked if Bread::Board is the right tool for a project we are currently starting. (If not, I'll wait until you've read it..)

I've got some good feedback via IRC and in the comments, and building on that feedback we hacked together a system that seems to work. And which I will describe now, so you might also understand Bread::Board (and most notably Parameterized Containers) better. But also to get even more feedback on potential design errors and gross misunderstandings of Bread::Board concepts that might hide in our solution.

"this job"

For our newest project we want to use a new approach. Instead of building a very big, monolithic Catalyst application we want to implement lots of small(-ish) apps. Each app does one thing, and does it well and well-tested. The apps talk to each other over ZeroMQ. The apps depend on various components (0mq, DB, templates, caches, ...). The components need to be initiated slightly differently in different environments (prod, dev, testing, ..).

As you can see, this is a rather complex setup. And Bread::Board is the (a) right tool for complex setups.

Parameterized Containers

I took me a while to fully understand parameterized containers. For me, the most important part was to see that you sort of "pass in" a container into another container, thereby naming the passed-in container. The containing container can depend on services that will be provided by the passed-in container (sort of like a method that takes a callback as an argument).

Maybe a bit of code is clearer:

my $apps = container $class => ['Environment'] => as {
        container 'App' => as {
            service 'renderservice_worker' => (
                class        => 'OurApp::RenderService::Worker',
                dependencies => {
                    coder         => '/Environment/json_coder',
                    renderer      => '/Component/Renderer/TTnowrap',
                }
            );
        };
        container 'Component' => as {
            container 'Renderer' => as {
                service 'TTnowrap' => (
                    lifecycle => "Singleton",
                    class     => 'Template::AutoFilter',
                    block     => sub {
                        return Template::AutoFilter->new( ... );
                    },
                );
            };
        };
   };

Or maybe not, so I'll explain that a bit...

In the above code snippet, I have set up Bread::Board container. A container is some sort of namespace that includes services. A service defines a way to initiate an object that is needed by the application.

Here I only define on app, "OurApp::RenderService::Worker". This app is a worker process that can render something. We plan to start lots of those (on different machines), so we can easily scale our app. The RenderService::Worker (well, the trimmed down version I use for this example) needs two objects: a coder to en/decode JSON and a renderer (in this case Template::Toolkit).

Besides the App-namespace we also define a Component-namespace (i.e. container). This container contains another container, containing a service, TTnowrap (this service has some more dependencies (eg INCLUDE_PATH) that I removed for the sake of a "simple" example).

In other parts of our Bread::Board, we can address this service as /Component/Renderer/TTnowrap. We can get to the RenderService::Worker via /App/renderservice_worker. And there we can depend on TTnowrap.

But we also depend on something called /Environment/json_coder. Which is nowhere to be found (yet). Well, in fact parts of it are already found (if you look at the first line of my example):

my $apps = container $class => ['Environment'] => as {

Here we tell Bread::Board that the container we're setting up takes another container as a parameter, and that the services defined in the container shall be accessible as /Environment in the containers and services we're just going to set up.

So we promise the container that later there will be a container named Environment, but just now we're very sorry, and we just don't know what exactly this container will contain.

The Parameter Container

So let's fulfill that promise:

my $env = container $self => as {
    service 'json_coder' => (
        class     => 'JSON::XS',
        lifecycle => 'Singleton',
        block     => sub {
            return JSON::XS->new->utf8->pretty->allow_nonref;
        },
    );
};

Now we create our final Bread::Board (i.e. the mix of general definitions and the environment):

my $bb = $apps->create( 'Environment' => $env );

Running the App

To initiate a new RenderService::Worker, we can now do:

my $worker = $bb->resolve( service => "App/renderservice_worker" );
$worker->run;

Putting it all together

We have on class, OurApp::BreadBoard. In this class we define all our Apps and all our Components in one big Bread::Board that expects one container as a parameter. This container is named Environment.

For each environment, we have a class (eg OurApp::BreadBoard::Env::Dev). In this class we provide the services that are "missing" in the general Bread::Board. They are missing because they are different for each environment.

Again in OurApp::BreadBoard we have a method called setup that looks sort of like this:

sub setup {
    my ( $class, $env_class_name) = @_;

    my $env_class = 'OurApp::BreadBoard::Env::' . $env_class_name;
    Class::Load::load_class($env_class);
    my $env = $env_class->new( name => $env_class_name );

    my $apps = container $class => ['Environment'] => as {
        ... # define all Apps and all Components
    }

    return $apps->create( 'Environment' => $env );
}

So in our scripts we can do:

use OurApp::BreadBoard;

my $env = $ARGV[0] || $ENV{BB} || 'Dev';

my $bb  = OurApp::BreadBoard->setup( $env );
my $app = $bb->resolve( service => "App/render_worker" );
$app->run;

Problem: Command Line Params

There is still one problem that I have: We can now no longer use MooseX::Getopt to make different options needed by different apps settable from the command line. It might be possible to solve that via parameters in Bread::Board (something completely different than parameterized containers!), but I haven't looked into this yet.

But there's more

Even though this example isn't really simple, it is still to simple to see the real benefit of Bread::Board. I will go into further detail in some future blog posts.

I also did not cover how we are using alias for fun and profit. Another nice side effect of Bread::Board is that we unified our scripts (and in fact replaced all of them with one script to rule them all) (which also explains why the services in Apps have lowercase names instead of CamelCase).

Stay tuned...