Bread::Board, part II: Beyond the DSL
Yanick Champoux (@yenzie)
july 9th, 2015
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!
Tags: technology perl