=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.