Bread::Board, part II: Beyond the DSL

« Return to Our Notebook

Bread::Board, part II: Beyond the DSL

Welcome to the second installment of our Bread::Board tutorials. In the previous article, we've covered what type of situation calls for Bread::Board, and we had a high-level overview of how to use it. In this installment, we'll begin to dig deeper into the inner workings of the framework. More specifically, we'll look beyond the DSL we used thus far for our examples, and learn how to manually create the underlying objects of a Bread::Board application.

A Quick Recap

By the end of the last article, using Bread::Board's DSL, we had created this simple Bread::Board application:

use 5.20.0;

use DBI;

use My::Logger;
use My::WebApp;
use My::PDF::Generator;
use My::Reporting;

use Bread::Board;

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',
    );
};

With this, we saw that that can we now generate instances of any of the given services (reporting, database, etc) via resolve(). For example, to get a reporting object, we would do:

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

Peeling the DSL off

While the Bread::Board's DSL is helpful in terms of readability and conciseness of code, we'll put it aside for a moment so that we can have a peek at what is really going on behind the scenes, and learn how to create all of its components manually.

At its core, Bread::Board is a fully object-oriented framework built using Moose. This means that each and every component we're creating is an object, which we then assemble together to create the desired system.

The service objects

Let's begin with the service objects, as they are the central pieces of our systems.

Taking the logger service of our example

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

we can create an equivalent service object using the Bread::Board::ConstructorInjection class:

use Bread::Board::ConstructorInjection;

my $service = Bread::Board::ConstructorInjection->new(
    name  => 'logger',
    class => 'My::Logger',
);

We now have a standalone service generator. To then create an instance of the service, we call the get() method on the object:

my $logger       = $service->get;

my $other_logger = $service->get;  # provide another object

Bread::Board comes with different service injectors, and, for the moment, we'll leave it at that; the next blog entry of this series will touch on the differences between the different injectors in more detail.

Adding dependencies

Of course, creating a single service is hardly useful. Dealing with dependencies and relationships between services is what we wanted Bread::Board for in the first place. So let's move to the database handle service of our example, which does have dependencies:

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

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

Generating the same service, without the DSL:

use Bread::Board::BlockInjection;

my $handle = Bread::Board::BlockInjection->new(
    name => 'handle',
    dependencies => [ qw/ dsn username password / ],
    block        => sub {
        my $service = shift;

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

(Note that here, because we're providing our own code block to construct the object, we use Bread::Board::BlockInjection to instantiate the service.)

But now, get() doesn't work anymore:

my $dbh = $handle->get();
# crashes with 'Can't call method "isa" on an undefined value'

Why is that? It's because the service now has dependencies (dsn, username and password) that need to be satisfied to generate a handle object. And Bread::Board can't resolve them because the service is currently floating in limbo, all on its own.

Container objects: no service is an island

For services to have resolvable dependencies, they must be put in container objects. And those containers can be, in turn, embedded in each other to form hierarchies. It helps to think of it as a directory structure where the containers are the directories, and the services the files.

To continue with the database handle example, it means we'll now create a 'database' container to hold the handle as well as its dependencies.

Before we deal with the container itself, let's create the services for dsn, username, and password. As those services are simple strings, they use the Bread::Board::Literal service class:

my $dsn      = Bread::Board::Literal->new( name => 'dsn',      value => 'DBI:SQLite:foo.db' );
my $username = Bread::Board::Literal->new( name => 'username', value => 'yanick' );
my $password = Bread::Board::Literal->new( name => 'password', value => 'hush' );

Now that we have all the required services, we can create the container:

my $database = Bread::Board::Container->new(
    name     => 'database',
    services => [
        $handle, $dsn, $username, $password,
    ],
);

Under the hood, the services are now linked to the context of their container, so doing:

my $dbh = $handle->get();

will work. It is, however, a little more comme il faut to go through the container we just created and do:

my $dbh = $database->resolve( service => '/handle');

which translates to 'find the service associated with this path and make it generate whatever it's supposed to generate'.

Creating the hierarchy: it's containers all the way down

With those two building blocks, containers and services, we can now build any system we want. Since containers can hold both services and sub-containers, it's only a question of populating the whole thing.

It's worth mentioning that although, so far we've created our containers and services as-is, they can be modified post-creation as well. For example, if we want to add the reporting service to our growing system, we could do the following:

my $root_app =  Bread::Board::Container->new(name => 'root');

# add the database container we already have
$root_app->add_sub_container( $database );

# create new service
my $reporting = Bread::Board::ConstructorInjection->new(
    name => 'reporting',
    class => 'My::Reporting',
    dependencies => {
        dbh           => '/database/handle',
        logger        => '/logger',
        pdf_generator => '/pdf_generator',
    },
);

# add it to the root app
$root_app->add_service($reporting);

say $root_app->resolve( service => '/database/handle');  # will work

say $root_app->resolve( service => '/reporting');    # will not, as '/logger'
                                                     # has not been defined yet

It also opens the door to more complex systems that could, if one so fancies, be self-mutating. But we'll keep to saner waters for now, and keep this kind of dark magic for a future article.

A Third Option:: Bread::Board::Declare

Now we know how to create a hierarchy of Bread::Board services and containers using its DSL, or going at it manually. Since, after all, this is Perl, there are yet a few more ways to do it. Bread::Board::Declare is an alternative which allows you to declare containers as Moose classes, with the attributes of the class defining its services and sub-containers. To provide a point of comparison, here is what our full example would look like, BBDeclare-style:

package MyApp {

use Moose;
use Bread::Board::Declare;

has webapp => (
    is  => 'ro',
    isa => 'My::WebApp',
);

has reporting => (
    is => 'ro',
    isa => 'My::Reporting',
    dependencies => {
        dbh           => '/database/handle',
        logger        => '/logger',
        pdf_generator => '/pdf_generator',
    },
);

has logger  => (
    is => 'ro',
    isa => 'My::Logger'
);

has pdf_generator => (
    is => 'ro',
    isa => 'My::PDF::Generator',
    dependencies => [ 'logger' ],
);

has database => (
    traits => [ 'Container' ],
    is     => 'ro',
    isa    => 'MyApp::Database',
);

};

package MyApp::Database {

use Moose;
use Bread::Board::Declare;

has handle => (
    is => 'ro',
    block => sub {
        my $service = shift;

        DBI->connect( map { $service->param($_) } qw/
            dsn username password
        /);
    },
    dependencies => [ qw/ dsn username password / ],
);

has dsn => (
    is    => 'ro',
    value => 'dbi:SQLite:foo',
);

has username => (
    is    => 'ro',
    value => 'yanick',
);

has password => (
    is    => 'ro',
    value => 'hush',
);

}

With that we have MyApp, a class generating objects that are the root containers of the Bread::Board systems that will be generating our target objects. Head swimming yet? Levity aside, don't let the many layers scare you. In truth, it all boils down to:

# MyApp returns an instance of the BB system
# we defined in its class
my $root_container = MyApp->new;

# an instance which we can use like any other instances we have
# dealt with before
my $dbh = $root_container->resolve( service => '/database/handle' );

Conclusion

In this installment, we went beyond the Bread::Board DSL to see how to manually create services and set them in a hierarchy of containers so that their inter-dependencies can be resolved by the system. We also touched on a third way to create Bread::Board systems by using Bread::Board::Declare and creating containers as Moose classes.

Next time, we'll spend some more time on the different types of services. Stay tuned!

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

Reach Out

t: 800.646.0188