=pod
=head1 Bread::Board by example
=head2 META
=head3 Author
Thomas Klausner
=head3 Email
domm AT plix.at
=head3 URL
http://domm.plix.at
=head3 URL_slides
http://domm.plix.at/talks/2013_kiev_bread_board
=head3 Date
2013-08-12
=head3 Location
Kiev
=head3 Event
YAPC::Europe 2013
=head2 Thomas Klausner
domm
Just another freelance Perl Hacker
=for newslide
=for img vienna.pm.org.png
=for newslide
=for img bicycle.pm.png
=for newslide
=for img validad.com.png
=head2 Bread::Board
"A solderless way to wire up your application components"
Stevan Little
https://metacpan.org/module/Bread::Board
=for newslide
=for html
Bread::Board is an inversion of control framework
with a focus on dependency injection
and lifecycle management .
=for newslide
=for html
Bread::Board is an WTF is he talking about? framework
with a focus on dependency injection
and lifecycle management .
=for newslide
=for html
Bread::Board is an WTF is he talking about framework
with a focus on something with syringes?
and lifecycle management .
=for newslide
=for html
Bread::Board is an WTF is he talking about framework
with a focus on something with syringes
and Buddhism?
=for newslide
I'll try to explain B, B and B in the a few slides, but first
=head3 What's with the name??
What I (as a native German-speaker and not-a-hardware person) think when I hear C
=for img schneidbrett.jpg
=for newslide
What english speaking geeks with an aptitude towards hardware hacking think when they hear C:
=for img breadboard.jpg
=for newslide
"A breadboard (or protoboard) is usually a construction base for prototyping of electronics. The term 'breadboard' is commonly used to refer to a solderless breadboard (plugboard)." - http://en.wikipedia.org/wiki/Breadboard)
=for newslide
=for img usedbreadboard.jpg
=for newslide
Now this makes a bit more sense:
"Bread::Board - A solderless way to wire up your application components"
BTW, it seems that Matt Trout is to blame for the name...
=head2 Inversion of Control
"In software engineering, inversion of control (IoC) is a programming technique, expressed here in terms of object-oriented programming, in which object coupling is bound at run time by an assembler object and is typically not known at compile time using static analysis." - http://en.wikipedia.org/wiki/Inversion_of_control
It took me quite long to understand that IoC solves a problem that does not exists in Perl.
Well, sort of "does not exists".
=for newslide
The key point (as I understood it) is:
B
Perl is a dynamic language
(a B dynamic languge)
We do all kinds of funky things at run-time
So the "compile time ... static analysis" part isn't that relevant
But:
=for newslide
IoC is about decoupling things.
Decoupling is good, because it allows us to specify, develop and test small & distinct units
which do one thing and do it proper
and later loosely couple them together
to build our app.
=for newslide
=for img lego.jpg
=for newslide
=for img cpan.png
=for newslide
In my opinion, C is just an overly complex buzzword used by system "architects" to make a totaly obvious practice every sane developer uses sound more grown-up and corporate.
see also:
orthogonal
idempotent
=head2 Lifecycle Management
That's the easiest part.
C allows you to specify how long certain objects should stay around
or rather, how often they should be newly initiated.
Singleton vs New Object every time
=for newslide
So: "IoC" is just a buzzword, "Lifecycle Management" is easy, that only leaves:
=head2 Dependency Injection
"Dependency injection is a software design pattern that allows the removal of hard-coded dependencies and makes it possible to change them, whether at run-time or compile-time." - http://en.wikipedia.org/wiki/Dependency_injection
This is the real core of what C does.
But doing Dependency Injection in Perl is so easy that you've probably done it for ages.
Lets look at an example
=head2 An contrieved example
Lets start with a simple-ish example
Here's a script / class that will fetch & parse the RSS feed of http://eztv.it/
(eztv provides links to torrent files of current TV shows)
The script shall report all new series (i.e. Season 1, Episode 1) so I can easily figure out if there is a new series that I should check out.
I don't care for 720p, so only report "small" files (<800MB)
=head3 The wapper script
=for include_code code/BadExample/find_new_series.pl
=for newslide
=for include_fragment code/BadExample/find_new_series.pl
1-10
7:4-18:red;
=for newslide
=for include_fragment code/BadExample/find_new_series.pl
1-10
9:4-7:blue;11-28:red;
=for newslide
=for include_fragment code/BadExample/find_new_series.pl
1-10
10:5-9:blue;11-13:red
=head3 The template
=for include_code code/tt/new_series.tt
=head2 How NOT to do it
=for include_code code/BadExample/FindNewSeries.pm
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
1-9
# The code needs various other objects (LWP::UserAgent, Number::Format, Template, XML::Feed)
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
11-16
# run method calls all other methods
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
18-32
# fetch the feed
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
18-32
21:8-10:blue;14-32:red
# LWP::UserAgent is initialised inline
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
34-53
# find s01e01
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
34-53
37:8-17:blue;21-39:red
38:8-12:blue;21-36:red
# The same is true for Number::Format and XML::Feed
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
55-63
# report it
=for newslide
=for include_fragment code/BadExample/FindNewSeries.pm
55-63
58:8-10:blue;14-26:red
# again Template::Toolkit is initalised inline
=for newslide
It is impossible to replace this objects with other objects.
Either objects with a different configuration
or objects of other classes
=for newslide
replace LWP::UserAgent with Mojo::UserAgent
define a locale for Number::Format
use another Templating engine
use mocked objects during testing
...
=for newslide
And this is the problem Dependency Injections wants to solve.
=head2 How NOT to do it, but a bit better
=for include_code code/LessBadExample/FindNewSeries.pm
=for newslide
=for include_fragment code/LessBadExample/FindNewSeries.pm
11-18
11:4-6:blue;10-28:red
16:4-13:blue;17-35:red;
18:4-6:blue;10-22:red;
# Objects are initiated at module load time
and stored in package variables
=for newslide
this is basically just a performance improvement
at least we now don't init all the objects on each run
might be expensive!
But it still sucks, because we cannot replace the lexically scoped object instances from the outside
Using package globals would be an option, but we're not in the 90ies anymore!
=head2 So lets inject some dependencies
I hope it's now clearer what is C means.
Instead of building your dependencies inside your object / methods
you "inject" them from the outside
=head2 How to do it - Parameter passing
So instead of defining the deps inside the object, lets define them outside:
=for newslide
=for include_code code/ParameterPassing/find_new_series.pl
=for newslide
=for include_fragment code/ParameterPassing/find_new_series.pl
13-20
13:4-6:blue;10-28:red
18:4-13:blue;17-35:red;
20:4-6:blue;10-22:red;
# We init the objects
=for newslide
=for include_fragment code/ParameterPassing/find_new_series.pl
22-23
23:11-13:red;16-35:blue
# and pass them to the run-method
=for newslide
=for include_code code/ParameterPassing/FindNewSeries.pm
=for newslide
=for include_fragment code/ParameterPassing/FindNewSeries.pm
8-13
9:16-35:blue
# The run-methods gets all the objects
=for newslide
=for include_fragment code/ParameterPassing/FindNewSeries.pm
8-13
10:25-29:red;31-33:blue
11:25-28:red;30-39:blue
12:12-16:red;18-20:blue
# and passes them on to the methods that actually do stuff
=for newslide
While we can now inject dependencies, this is a very ugly and inefficient way to do it.
You have to pass all the objects around yourself
Inside the class, it's not very obvious what external objects we need
If you want to re-use the class in another script / context, you have to copy all the setup code
Fucking Hell
=for newslide
=for img fucking_hell_6pack.jpg
=head2 How to do it - attributes
One obvious step to improve the situation is to put all the deps into attributes of the class
=for newslide
=for include_code code/Attributes/FindNewSeries.pm
=for newslide
=for include_fragment code/Attributes/FindNewSeries.pm
1-4
3:5-9:red
# Obviously we use Moose!
=for newslide
=for include_fragment code/Attributes/FindNewSeries.pm
7-25
9:1-3:red;6-7:blue
15:1-3:red;6-14:blue
21:1-3:red;6-14:blue
# We declare the attributes via has
=for newslide
=for include_fragment code/Attributes/FindNewSeries.pm
7-25
7:4-32:purple
12:17-25:red;32-34:blue;
18:17-25:red;32-43:blue;
24:17-25:red;32-44:blue;
# for a change I use duck-typing instead of hardcoded classnames
=for newslide
=for include_fragment code/Attributes/FindNewSeries.pm
34-44
37:15-19:blue;22-23:red;26-28:purple
# and now we can access LWP::UserAgent as an attribute of our object
But we still need to initiate the objects in the calling script:
=for newslide
=for include_code code/Attributes/find_new_series.pl
=for newslide
=for include_fragment code/Attributes/find_new_series.pl
13-21
13:11-29:red
14:5-6:blue;11-29:purple;
18:5-13:blue;18-37:purple;
19:5-12:blue;18-30:purple;
=for newslide
While we now don't have to pass the objects around
(because we can now access them via our object)
we still need to set them up in the calling script
and we still need to do that for every context
=head2 How to do it - lazy building
C has a nice feature called C that can help us set up the objects
=for newslide
=for include_code code/LazyBuild/FindNewSeries.pm
=for newslide
=for include_fragment code/LazyBuild/FindNewSeries.pm
12-23
12:1-3:red;6-7:blue;
# for example, the ua attribute
=for newslide
=for include_fragment code/LazyBuild/FindNewSeries.pm
12-23
16:5-14:red;19-20:blue;
# we add lazy_build
=for newslide
=for include_fragment code/LazyBuild/FindNewSeries.pm
12-23
18:5-11:red;12-13:blue
# and a _build_ method
And if we now do not set C when initating the C object, Moose will build a new UserAgent for us.
=for newslide
The script is now again nearly empty:
=for newslide
=for include_fragment code/LazyBuild/find_new_series.pl
1-10
9:11-28:red
=for newslide
But I can now easily pass in other objects than the default ones:
=for newslide
=for include_code code/LazyBuild/mocked.pl
=for newslide
=for include_fragment code/LazyBuild/mocked.pl
9-25
# This is a simple mocked UserAgent that will always return the same string
Very good for testing / on the plane
=for newslide
=for include_fragment code/LazyBuild/mocked.pl
29-30
29:11-29:purple;31-32:blue;37-48:red
# instead of using the default LWP::UserAgent, we pass in our Mocked Agent
nice.
=for newslide
I used setups like this for ages
and was quite happy
even though there are a few problems:
=for newslide
As always when using Moose, I tend to pack everything into roles.
For example, the DB connection
=for newslide
package MyApp::Role::DB
use Moose::Role
has 'dbh' => (is => 'ro', lazy_build => 1);
sub _build_dbh {
# some fancy code to figure out which DB to use
return $dbh
}
=for newslide
package MyApp::A;
with 'MyApp::Role::DB';
sub do_something {
my $self = shift;
$self->dbh->...
}
=for newslide
package MyApp::B;
with 'MyApp::Role::DB';
use MyApp::A;
sub do_something_else {
my $self = shift;
my $a = MyApp::A->new;
$a->do_something;
$self->dbh->...
}
two different database connections
two different transactions
not nice
=for newslide
package MyApp::B;
with 'MyApp::Role::DB';
use MyApp::A;
sub do_something_else {
my $self = shift;
my $a = MyApp::A->new( dbh => $self->dbh );
$a->do_something;
$self->dbh->...
}
=for newslide
Another (performance related) problem of this approach is that you always load all the classes your object depends on.
Even if you do not need that particular attribute (= object) in a specific use case.
eg LWP::UserAgent is loaded even if use the Mocked class
=for newslide
But we could live with this problems
(or work around them)
But then came along OX
and with it Bread::Board
=head2 OX
the hardest working two letters in Perl
"OX is a web application framework based on Bread::Board, Path::Router, and PSGI."
"The philosophy behind OX is that the building blocks of your web application should just 'click' together, without the overhead of an additional plugin system or 'glue' layer."
i.e. no Catalyst Model
2012 they published a very nice OX Advent Calender
http://iinteractive.github.io/OX/advent/
=for newslide
but now finally:
=head2 How to do it - Bread::Board
=for newslide
=for include_code code/BreadBoard/FindNewSeries.pm
=for newslide
=for include_fragment code/BreadBoard/FindNewSeries.pm
1-5
# no more use LWP::UserAgent etc
=for newslide
=for include_fragment code/BreadBoard/FindNewSeries.pm
9-25
# no more lazy_load
=for newslide
Instead we now need a Bread::Board container
I like to set up this container in a class:
=for newslide
=for include_code code/BreadBoard/MyBB.pm
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
1-6
# just a plain class
using Bread::Board
and providing a C method.
We'll look at that later.
=for newslide
Bread::Board provides two basic building blocks:
Containers and Services
A Container is a bit like a namespace that can contain Services
And a Service is basically an object (i.e. an instance of a class)
No, that's wrong:
A Service provides us with the information we need to initiated an object
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
8-16
8:12-20:red;23-34:blue;
# here we define a Container named 'CouchPotatoe'
assume that CouchPotatoe is a big application that makes my TV viewing easier
and that FindNewSeries (the code I've been showing you in this talk) is just a small part of this application
=for newslide
B: Bread::Board makes no sense for small application or simple scripts
The effort really only pays off if you have a big app consisting of lots of different parts which depend on similar things (e.g. a Database, a Templating Engine, ..)
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
8-16
8:12-20:red;23-34:blue;
# ok, we have a container
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
8-16
9:9-15:red;18-30:blue
# now we define a Service called "FindNewSeries"
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
8-16
10:13-17:red;30-42:blue;
# It shall be an instance of the class FindNewSeries
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
8-16
11:13-24:red
12:17-18:red;
13:17-25:red;
14:17-24:red;
# And has a bunch of dependencies
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
8-16
12:17-18:purple;31-54:red;
13:17-25:purple;31-54:red;
14:17-24:purple;31-43:red;
# but the dependencies are declared a bit strange
This means: load the service that can be found under '/Component/LWP-UserAgent'
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
17-33
17:9-17:red;20-28:blue;
18:13-19:red;22-34:blue;
22:13-19:red;22-34:blue;
26:13-19:red;22-23:blue;
# here's this Component, which contains 3 Services
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
17-33
18:13-19:red;22-34:blue;
19:17-21:red;31-44:blue;
# The LWP-UserAgent Service shall be an instance of LWP::UserAgent
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
17-33
18:13-19:red;22-34:blue;
20:17-25:red;31-39:blue;
# and it shall only be initiated once (i.e. a Singleton)
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
17-33
29:17-21:red;30-32:red;
# You can finetune object creation by passing in a 'block'
=for newslide
=for include_fragment code/BreadBoard/MyBB.pm
17-33
30:28-40:red;
# and decide how the object shall be created
=for newslide
But you can also define big chains of dependencies
Which Bread::Board will resolve for you on demand and as lazy as possible
=for newslide
But how do we run it?
=for newslide
=for include_code code/BreadBoard/find_new_series.pl
=for newslide
=for include_fragment code/BreadBoard/find_new_series.pl
7-12
7:5-8:red
# Use the Bread::Board
=for newslide
=for include_fragment code/BreadBoard/find_new_series.pl
7-12
9:4-6:purple;10-20:red;
# Init the Bread::Board and store it somewhere
=for newslide
=for include_fragment code/BreadBoard/find_new_series.pl
7-12
11:11-13:purple;16-22:red
# On our Bread::Board, we call resolve
=for newslide
=for include_fragment code/BreadBoard/find_new_series.pl
7-12
11:11-13:purple;16-22:red;25-31:blue;37-49:red
# and ask for the service named 'FindNewSeries'
=for newslide
=for include_fragment code/BreadBoard/find_new_series.pl
7-12
11:4-7:green;11-13:purple;16-22:red;25-31:blue;37-49:red
# Bread::Board will init all dependencies, pass them to the class and return a ready-to-use object
yay!
=head2 Much Ado About Nothing
Wow, that's a lot of fuzz
For what seems to be little benefit
Or even a setback
(note the hardcoded classnames of C etc in C)
I'll show some examples where you hopefully will see why the effort may be worth it in a minute
=head2 Parameterized Containers
Every time you ask on irc how to do something semi-complex with Bread::Board, the answer is "Parameterized Containers"
It took me writing two blog posts, some extensive IRCing on #bread-board and lots of experimenting to figure this out
So here's a complex enough example of a Parameterized Container
=for newslide
=for include_code code/ParameterizedContainer/MyBB.pm
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
1-10
# Here's the setup-method again
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
1-10
6:18-26:red
# but we now pass an environment name in
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
1-10
8:23-31:red
# This env_name shall be a method that we call now
this method returns a Bread::Board container
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
58-64
58:5-8:red
# here's the production environment
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
58-64
61:9-13:red
62:9-13:red
# we use alias to set up a link from 'UserAgent' defined in this container
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
58-64
61:9-13:red;31-54:blue
62:9-13:red;31-43:blue
# to /Component/LWP-UserAgent
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
50-56
50:5-8:red;
# and here's the dev environment
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
50-56
53:9-13:red;
54:9-13:red;
# again we use alias
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
50-56
53:9-13:red;42-47:blue
54:9-13:red;42-47:blue
# but here we link to other services ('/Component/MockUA');
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
1-10
8:8-11:red
# env now contains either the prod or the dev container
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
1-10
10:44-50:red
# we need tell Bread::Board that the CouchPotato Container takes one parameter
This parameter has to be a Container.
And it shall be named "Env".
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
46-48
47:28-30:red;36-39:blue
# When we finally create our Bread::Board, we need to pass in whatever C<$env> we choose.
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
10-18
11:18-30:purple
# In the Service definition
=for newslide
=for include_fragment code/ParameterizedContainer/MyBB.pm
10-18
11:18-30:purple
14:32-34:red
16:32-34:red
# we can now reference the 'Env' container even though it's not defined here.
We can later pass in another container as a parameter
(or several..)
=for newslide
=for include_fragment code/ParameterizedContainer/find_new_series.pl
1-12
9:23-40:red
# here we set the enviroment via the commandline or default to 'prod'
=for newslide
We now have a Bread::Board that contains info on how to initiate all potential objects we might need
And a way to ask the Bread::Board to initiate our objects.
But even this example is way to simple to really legitimate Bread::Board
so...
=head2 Real Life Example
We use the following Bread::Board in a project we're currently developing
It uses the parameterized container context I just showed you
=for newslide
=for include_code code/validad/BreadBoard.pm
=for newslide
As you can see from the small font, that's quite a bit of code
~420 lines
let's pick a few
=for newslide
=for include_fragment code/validad/BreadBoard.pm
14-25
=for newslide
=for include_fragment code/validad/BreadBoard.pm
38-52
=for newslide
=for include_fragment code/validad/BreadBoard.pm
187-204
=for newslide
=for include_fragment code/validad/BreadBoard.pm
205-215
=for newslide
=for include_fragment code/validad/BreadBoard.pm
216-226
=for newslide
As we have all our components defined in Bread::Board, we don't need special scripts to run different apps.
We use one generic script which derives the name of the component we want to start from the name of the symlink that we used to call it
=for newslide
~/project/bin$ ls -la
lrwxrwxrwx 1 domm domm 6 Jun 18 12:01 admin -> winxle
lrwxrwxrwx 1 domm domm 6 Jun 18 12:01 campaignspread -> winxle
lrwxrwxrwx 1 domm domm 6 Apr 20 21:24 l10n_service_backend -> winxle
lrwxrwxrwx 1 domm domm 6 May 31 23:55 mailer -> winxle
lrwxrwxrwx 1 domm domm 6 Apr 20 21:24 renderservice_ventilator -> winxle
lrwxrwxrwx 1 domm domm 6 Apr 20 21:24 renderservice_worker -> winxle
-rwxr-xr-x 1 domm domm 3295 Jun 30 18:31 single_fork_app
lrwxrwxrwx 1 domm domm 6 Jun 18 12:01 web -> winxle
-rwxr-xr-x 1 domm domm 1833 Jun 9 20:43 winxle
=for newslide
=for include_fragment code/validad/winxle
1-20
=for newslide
As you maybe have notices, this app consists of a lot of services
It's annoying to allways start all of them
in the right order
=for newslide
=for include_code code/validad/single_fork_app
=for newslide
=for include_fragment code/validad/single_fork_app
18-30
=for newslide
=for include_fragment code/validad/single_fork_app
53-67
=for newslide
=for include_fragment code/validad/single_fork_app
32-41
=for newslide
If we need another service (or more instances of one) we just need to add the service name to the C<@serivces>
=for newslide
Here's another script that's not a service itself
but a maintaince / testing thingy that dumps some random data into the datebase
so the designers have something to work on
=for newslide
=for include_code code/validad/fill_campaigns
=for newslide
=for include_fragment code/validad/fill_campaigns
1-15
=for newslide
=for include_fragment code/validad/fill_campaigns
16-30
=for newslide
=for include_fragment code/validad/fill_campaigns
36-43
=for newslide
And here's an example that uses Bread::Board to test an OX controller
=for newslide
=for include_code code/validad/OxController.pm
=for newslide
=for include_fragment code/validad/OxController.pm
1-18
=for newslide
=for include_fragment code/validad/OxController.pm
20-42
=for newslide
some more helper methods like to "send" requests and parse the results
one nice think about OX: you can really easily unit test controller actions
it's just a method call
no fuzzying with forking a server or running a plack app
faster tests, because you don't need to start the whole app (just the Controller)
=for newslide
=for include_code code/validad/login.t
=for newslide
=for include_fragment code/validad/login.t
1-15
=for newslide
=for include_fragment code/validad/login.t
17-29
=head2 Summary
Bread::Board is complex and introduces a lot of work when you use it the first time
But after you mastered it, it makes it very easy to have lots of well defined application components
So if you have a big application consisting of lots of services, Bread::Board might be a nice tool to check out.