A Gentle Introduction to Bread::Board

« Return to Our Notebook

A Gentle Introduction to Bread::Board

Bread::Board. It has one hoary hairy heck of a scary reputation.

But while it's not totally undeserved—Inversion of Control as a concept tends even at its best to, well, turn one's mind inside-out like a sock—the truth is much less daunting than the hype would have you think.

What is it for, anyway?

Bread::Board's punny name is a bit of a tactical mistake. While it makes its purpose evident to those accustomed to dabbling with hardware and the physical aspects of electronics, it leaves the rest of us without a clue what is going on. Going straight for the Inversion of Control lingo doesn't help either, as those abstract concepts trigger glazed befuddlement.

Instead of all of that, let's take a pragmatic, down-to-earth approach, and build a case for Bread::Board by working an example.

Our example system

Say that you have a fairly large project involving a web application as well as a backend reporting system. At a very high level, the main classes of those two components could look like:

package My::WebApp {

    # we need a database handler
    has dbh => (
        is  => 'ro',
        isa => 'DBI',
    );

    # ... and a logging system
    has logger => (
        is => 'ro',
    );

    # lots of code goes here...

}

package My::Reporting {

    # we need a database handler
    has dbh => (
        is  => 'ro',
        isa => 'DBI',
    );

    # ... and a logging system
    has logger => (
        is => 'ro',
    );

    # ... and we'll generate a few PDF reports
    has pdf_generator => (
        is => 'ro',
    );

    # lots of code goes here...

}

To launch the web application, we'd instantiate and call run:

my $webapp = My::WebApp->new(
    dbh           => DBI->connect( 'dbi:SQLite:foo' ),
    logger        => My::Logger->new,
);

$webapp->run;

Ditto for the reporting system:

my $reporting = My::Reporting->new(
    dbh           => DBI->connect( 'dbi:SQLite:foo' ),
    logger        => My::Logger->new,
    pdf_generator => My::PDF::Generator->new,
);

$reporting->run;

The sore points

While it's not horrendous (yet), we can see where rough spots will develop. The list of arguments for each main object is already sizable, and we can expect it to grow quickly. Not only that, but the arguments are duplicated all over the place. Of course, we don't want to hard-code any parameters unless we absolutely have to. We want the flexibility to set the components to whatever we want, for those one-off special instances or when we want to test things in isolation. For example, testing My::Reporting with all the real trimmings (the database connection, the logger object and the PDF generator) could be a daunting, complex endeavor. But it can be greatly simplified by replacing them with suitable stand-ins:

# using DBD::Mock
my $dbh = DBI->connect( 'DBI:Mock:' );

use Test::Log::Dispatch;
my $log = Test::Log::Dispatch->new;

use Test::MockObject;
my $generator = Test::MockObject->new;
$generator->set_true( 'generate' );

my $reporting = My::Reporting->new(
    dbh           => $dbh,
    logger        => $log,
    pdf_generator => $generator,
);

$reporting->run;

# the fake $dbh and $log can now be 
# inspected for desired behaviors

So, we want our flexibility, but we also want the common case to be short and sweet.

A master object to rule them all

How can we have this delicious cake and eat it too? One solution is to add one more layer atop the master classes that will take care of the configuration details:

package My::AppBuilder {

    has config_file => (
        is => 'ro',
    );

    has config => (
        is   => 'ro',
        isa  => 'HashRef',
        lazy => 1,
        default => sub {
            # reads from $self->config_file
            # and transform into a perl struct
        },
    );

    has web_app => (
        is   => 'ro',
        isa  => 'My::WebApp',
        lazy => 1,
        default => sub {
            my $self = shift;

            return My::WebApp->new(
                dbh    => DBI->connect( @{ $self->config->{dsn} } ),
                logger => My::Logger->( $self->config->{logger_args} ),
            );
        },
    );

    has reporting_app => (
        is      => 'ro',
        isa     => 'My::Reporting',
        lazy    => 1,
        default => sub {
            my $self = shift;

            return My::Reporting->new(
                dbh           => DBI->connect( @{ $self->config->{dsn} } ),
                logger        => My::Logger->new( $self->config->{logger_args} ),
                pdf_generator => My::PDF::Generator->new( $self->config->{pdf_args} ),
            );
        },
    );

}

That's better. Now you can move your configuration details into a file and instantiate the object with it:

My::AppBuilder->new( config_file => 'config.yml' )->web_app->run;

The devil is in the details

That is better. But what happens if the PDF generator has a sub-component (say, its scheduler) that uses the logging system as well? The master object will have to take it into consideration:

has logger => (
    is      => 'ro',
    lazy    => 1,
    default => sub {
        my $self = shift;
        return My::Logger->new( $self->config->{logger_args} ),
    }
);

has reporting_app => (
    is      => 'ro',
    isa     => 'My::Reporting',
    lazy    => 1,
    default => sub {
        my $self = shift;

        return My::Reporting->new(
            dbh           => DBI->connect( @{ $self->config->{dsn} } ),
            logger        => $self->logger,
            pdf_generator => My::PDF::Generator->new(
                scheduler => My::Scheduler->new(
                    logger => $self->logger,
                ),
                %{ $self->config->{pdf_args} },
            ),
        );
    },
);

Time to delegate?

As you can imagine, that can get hairy real fast. An alternative would be to provide a common pool of resources to all sub-components, and let them pick up whatever they need from this centralized bag. Using that approach, we would change My::AppBuilder to be My::AppConfig:

package My::AppConfig {

    has config_file => (
        is => 'ro',
    );

    has config => (
        is   => 'ro',
        isa  => 'HashRef',
        lazy => 1,
        default => sub {
            # reads from $self->config_file
            # and transform into a perl struct
        },
    );

    has logger => (
        is      => 'ro',
        lazy    => 1,
        default => sub {
            my $self = shift;
            return My::Logger->new( $self->config->{logger_args} ),
        }
    );

}

And the reporting system would be modified to look like this:

package My::Reporting {

    has app_builder => (
        is => 'ro',
    );

    # we need a database handler
    has dbh => (
        is   => 'ro',
        isa  => 'DBI',
        lazy => 1,
        default => sub {
            my $self = shift;

            return DBI->connect( @{ $self->app_builder->config->{dsn}} );
        },
    );

    # ... and a logging system
    has logger => (
        is      => 'ro',
        lazy    => 1,
        default => sub {
            my $self = shift;

            return My::Logger->new( app_builder => $self->app_builder );
        },
    );

    # ... and we'll generate a few PDF reports
    has pdf_generator => (
        is      => 'ro',
        lazy    => 1,
        default => sub {
            my $self = shift;

            return My::PDF::Generator->new( app_builder => $self->app_builder );
        },
    );

    has scheduler => (
        is      => 'ro',
        lazy    => 1,
        default => sub {
            my $self = shift;

            return My::Scheduler->new( app_builder => $self->app_builder );
        },
    );

    # lots of code goes here...

}

It might look like a little bit more code, but at least it localizes the logic. My::Reporting doesn't have to worry about all the needs of its sub-objects. If they need anything from the main configuration, they can just go and take it directly from app_builder themselves.

Good, but… good enough?

It's still not perfect. Now we have to bandy that app_builder object all around, and we kind of perverted the default settings of our attributes. My::PDF::Generator could have been a perfectly generic PDF-generating class, but now it has become tied to our overall system. Not overwhelmingly tightly, perhaps, but the bindings are there.

So what to do?

Taking a step back

Looking back at what we did so far, we can notice an interesting fact. We are trying to solve not one, but two different problems. On one hand, we want our huge system to configure itself, which asks for knowledge of our specific settings. Yet on the other hand, we want its inner components to remain as generic as possible. For two such opposing requirements, maybe what we need is not one, but two solutions?

And this is where Bread::Board comes into play. It can be seen as a kind of pre-compiler that assembles the particular application we want to run. And it's in that "pre-compiling/constructing" element that all the magic resides, because once Bread::Board finishes its thing, we'll have our precious generic objects that just happen to have been instantiated with all our specific configuration.

Bread::Board in four bullet points

At its core, the ideas behind Bread::Board are quite simple:

  1. You have services, which are things that your Bread::Board object is aware of. Those services can be any type of Perl value: simple strings, hashrefs, objects, etc.

  2. Each service is identified by a unique path. For example the database username could be /database/username.

  3. Each service can have dependencies on other services.

  4. Each service is given instructions on how to use its dependencies to build its final product.

Is this the whole truth? No. Have I lied? Maybe a little bit. But for the time being, that's enough of the real deal to serve our purpose.

The example system, revisited

Let's rewrite the app building logic using Bread::Board's DSL.

Spinning the web of dependencies

First, let's just layout the services and their dependencies. From the top, we know that our web app service needs a database handle and a logger:

use Bread::Board;

my $root_container = container MyApp => as {
    service webapp => (
        dependencies => { dbh => '/database/handle', logger => '/logger' },
    );
};

Each dependency has a local name, and is tied to a service that will provide its value.

Going down the food chain, we now define the database handler and logger services:

my $root_container = container MyApp => as {
    service webapp => (
        dependencies => { dbh => '/database/handle', logger => '/logger' },
    );

    container database => as {
        service handle => (
            dependencies => {
                dsn      => '/database/dsn',
                username => '/database/username',
                password => '/database/password',
            },
        );

        service dsn      => ();
        service username => ();
        service password => ();
    };

    service logger => (
    );
};

Nice trick #1: dependency paths can also be relative to the current container, which can help to make the whole dependency tree more modular. So we can change the previous code to:

my $root_container = container MyApp => as {
    service webapp => (
        dependencies => { dbh => '/database/handle', logger => '/logger' },
    );

    container database => as {
        service handle => (
            dependencies => {
                dsn      => 'dsn',
                username => 'username',
                password => 'password',
            },
        );

        service dsn      => ();
        service username => ();
        service password => ();
    };

    service logger => (
    );
};

Nice trick #2: if the dependency key and value are the same… yes, like Moose's handles feature, we can use an array reference instead:

my $root_container = container MyApp => as {
    service webapp => (
        dependencies => { dbh => '/database/handle', logger => '/logger' },
    );

    container database => as {
        service handle => (
            dependencies => [ qw/ dsn username password / ],
        );

        service dsn      => ();
        service username => ();
        service password => ();
    };

    service logger => (
    );
};

For the sake of completeness, we now add the reporting app to the mix:

my $root_container = container MyApp => as {
    service webapp => (
        dependencies => { dbh => '/database/handle', logger => '/logger' },
    );

    service reporting => (
        dependencies => {
            dbh           => '/database/handle',
            logger        => '/logger',
            pdf_generator => '/pdf_generator',
        },
    );

    container database => as {
        service handle => (
            dependencies => [ qw/ dsn username password / ],
        );

        service dsn      => ();
        service username => ();
        service password => ();
    };

    service logger => ();

    service pdf_generator => (
        dependencies => [ 'logger' ],
    );
};

Building stuff

So far, we're only established the relationship of dependencies. Now we have to tell Bread::Board how to build the end-product of all those services.

The easiest case is when a service consists of a single value:

container database => as {
    ...;

    service username => 'foo';

    ...;
};

When we request an instance of the service /database/username, we'll get the value foo.

Of course, there is more to life than simple scalar values. In the general case, we pass a code block to the service, whose job is to use the resolved dependencies and construct the final thing we want. For the database handle, it would be:

container database => as {
    service handle => (
        dependencies => [ qw/ dsn username password / ],
        block        => sub {
            my $service = shift;

            DBI->connect(
                $service->param('dsn'),
                $service->param('username'),
                $service->param('password'),
            );
        },
    );

    service dsn      => 'dbi:SQLite:foo';
    service username => 'foo';
    service password => 'bar';
};

And, like that, between the über-generic block and the literal values, we have the tools to populate the whole tree:

my $root_container = container MyApp => as {
    service webapp => (
        dependencies => { dbh => '/database/handle', logger => '/logger' },
        block        => sub {
            my $service = shift;
            return My::WebApp->new(
                dbh    => $service->param('dbh'),
                logger => $service->param('logger'),
            );
        }
    );

    service reporting => (
        dependencies => {
            dbh           => '/database/handle',
            logger        => '/logger',
            pdf_generator => '/pdf_generator',
        },
        block => sub {
            my $service = shift;
            return My::Reporting->new(
                dbh           => $service->param('dbh'),
                logger        => $service->param('logger'),
                pdf_generator => $service->param('pdf_generator'),
            );
        }
    );

    container database => as {
        service handle => (
            dependencies => [ qw/ dsn username password / ],
            block        => sub {
                my $service = shift;

                DBI->connect(
                    $service->param('dsn'),
                    $service->param('username'),
                    $service->param('password'),
                );
            },
        );

        service dsn      => 'dbi:SQLite:foo';
        service username => 'foo';
        service password => 'bar';
    };

    service logger => (
        block => sub {
            return My::Logger->new;
        }
    );

    service pdf_generator => (
        dependencies => [ 'logger' ],
        block        => sub {
            my $service = shift;
            return My::PDF::Generator->new( logger => $service->param('logger'));
        }
    );
};

Classy services

As we define our services, a simple pattern emerges. For many of them, all we do in the block sub is to create an object of a given class using all the specified dependencies as parameters. This is such a common pattern that Bread::Board will recognize it if we use a class attribute for the service:

my $root_container = container MyApp => as {
    service webapp => (
        dependencies => { dbh => '/database/handle', logger => '/logger' },
        class        => 'My::WebApp',
    );

    service reporting => (
        dependencies => {
            dbh           => '/database/handle',
            logger        => '/logger',
            pdf_generator => '/pdf_generator',
        },
        class => 'My::Reporting',
    );

    container database => as {
        service handle => (
            dependencies => [ qw/ dsn username password / ],
            block        => sub {
                my $service = shift;

                DBI->connect(
                    $service->param('dsn'),
                    $service->param('username'),
                    $service->param('password'),
                );
            },
        );

        service dsn      => 'dbi:SQLite:foo';
        service username => 'foo';
        service password => 'bar';
    };

    service logger => (
        class => 'My::Logger',
    );

    service pdf_generator => (
        dependencies => [ 'logger' ],
        class        => 'My::PDF::Generator',
    );
};

The long-awaited payout

All our dominoes are now in place. If we want the Bread::Board to generate the value for a service, we call its resolve() method with the wanted service path:

my $report_app = $root_container->resolve(
    service => '/reporting'
);

Under the hood, there is nothing magic, just a lot of the tedium automated away as Bread::Board follows all the dependencies to create all the required objects. For example, the call to resolve() above is equivalent to:

my $report_app = My::Reporting->new(
    dbh => DBI->connect(
        'dbi:SQLite:db_name', 'foo', 'bar'
    ),
    logger        => My::Logger->new,
    pdf_generator => My::PDF::Generator->new(
        logger => My::Logger->new
    ),
);

What next?

Already, we have enough material covered to use the basic features of Bread::Board. But so much more awaits…

Next time, we'll turn up the heat a few degrees and explore the finer points of service configuration.

We solve problems with technology. What can we solve for you?

Reach Out

t: 800.646.0188