Upload
john-napiorkowski
View
110
Download
0
Embed Size (px)
Citation preview
Catalyst Design Patterns
YAPC::EU 2016
Catalyst is built around and encourages the use
of design patterns.
– https://en.wikipedia.org/wiki/Software_design_pattern
“Design patterns are formalized best practices that the programmer can use to solve common
problems when designing an application or system.”
Design Patterns in Catalyst
Command
• An object wrapped around a function.
• Encapsulates / abstracts all the information needed to perform an action.
• Separate a reusable activity from class.
• Attach ‘bookkeeping’ and monitoring functions
package MyApp::Controller::User;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub list :Local { my ($self, $c) = @_; }
__PACKAGE__->meta->make_immutable;
package Catalyst::Action;
use Moose;
has [qw/class namespace attributes name code private_path/] => (is=>'ro', ...);
sub execute { ... } sub match { ... } sub list_extra_info { ... }
__PACKAGE__->meta->make_immutable;
Examples
package Catalyst::ActionRole::Scheme;
use Moose::Role;
requires 'match_args', 'match_captures';
around ['match_args','match_captures'] => sub { my ($orig, $self, $c, @args) = @_; my $scheme = lc($c->req->env->{'psgi.url_scheme'}); my $required = lc($self->scheme||’’);
return $scheme eq $required ? $self->$orig($ctx, @args) : 0; };
1;
package MyApp::Controller::User;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub list :Local Does(Scheme) Scheme(https) { my ($self, $c) = @_; }
__PACKAGE__->meta->make_immutable;
package MyApp::Controller::Example;
use Moose; use MooseX::MethodAttributes; use Catalyst::ActionSignatures;
extends 'Catalyst::Controller';
sub test(View $v, Model::User $user) :Local { $v->show_profile($user); }
package MyApp::Controller::Example;
use Moose; use MooseX::MethodAttributes; #use Catalyst::ActionSignatures;
extends 'Catalyst::Controller';
sub test :Local { my ($self, $c, @args) = @_; my $v = $c->view; my $user = $c->model('User');
$v->show_profile($user); }
???
Model - View - Controller
• Implement User Interfaces.
• “Pattern of Patterns”.
• Traditionally used for GUI Desktop applications.
• For classic server side applications its more about separation of job duties.
Examples
package MyApp::Controller::User;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub list :Local { my ($self, $c, @args) = @_; my @users = $c->model(‘Users’)->user_list; $c->forward(‘View::Users’, \@users); }
__PACKAGE__->meta->make_immutable;
package MyApp::Model::Users;
use Moose;
extends ‘Catalyst::Model';
sub user_list { return (qw/ Srinivas Joe Holly
/); }
__PACKAGE__->meta->make_immutable;
package MyApp::View::Users;
use Moose;
extends ‘Catalyst::View';
sub process { my ($self, $c, @users) = @_; my @list = map { “<li>$_</li>”} @users; $c->res->body(<<END);
<html> <head> <title>User List</title> </head>
<body> <ul> @list </ul> </body>
</html> END; }
__PACKAGE__->meta->make_immutable;
???
Chain of Responsibility
• An abstraction that decomposes a complex workflow into discrete objects.
• Promotes loose coupling of design, and simple action commands.
package MyApp::Controller::Root;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub root :Chained(/) PathPart('') CaptureArgs(0) { my ($self, $c) = @_; $c->view('SummaryList', items => $c->model)); }
sub summary_list :GET Chained(root) PathPart('') Args(0) { my ($self, $c) = @_; $c->view->http_ok; }
sub add :POST Chained(root) PathPart('') Args(0) { my ($self, $c) = @_; $c->model('Form::Todo')->is_valid ? $c->view->http_ok : $c->view->http_bad_request; }
???
Catalyst Components
• An abstraction that decouples the creation of required objects from the code that needs those objects.
• Associated with Dependency Injection and Service Locator Patterns.
package MyApp::Controller::User;
use DBI; use DBD::Pg; use MyApp::Schema;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub list :Local { my ($self, $c) = @_; my $schema = MyApp::Schema->connect( "dbi:Pg:dbname=myapp;host=localhost;port=5432", "user", "password");
my $user_rs = $schema->resultset('User'); }
package MyApp::Controller::User;
use DBI; use DBD::Pg; use MyApp::Schema;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub list :Local { my ($self, $c) = @_; my $schema = MyApp::Schema->connect( "dbi:Pg:dbname=myapp;host=localhost;port=5432", "user", "password");
my $user_rs = $c->model(‘User’); }
Decouple instance Creation from Usage!
Program against an Interface not an implementation!
package Catalyst::Component;
use Moose;
#wraps a class accessor sub config { … }
# Called by App->setup_components sub COMPONENT { my ($class, $app, $config) = @_; return $class->new($config) }
# Optional, called with $c->component sub ACCEPT_CONTEXT { my ($self, $c, @args) = @_; }
All Models, Views and Controllers inherit from Catalyst::Component
Component Concepts
• Lifecycle
• Dependency Management
Component Lifecycle
Application Scope
• Instance created once during ‘setup_components’ (via ->COMPONENT)
• Can only depend on literals and other application scoped components.
• Can’t meaningfully pass arguments to ->model / ->view / ->controller.
package MyApp::Model::ApplicationMeta;
use Moose; extends ‘Catalyst::Component';
has 'version' => (is=>'ro', required=>1); has 'license' => (is=>'ro', required=>1); has 'copyright' => (is=>'ro', required=>1);
__PACKAGE__->meta->make_immutable;
package MyApp::Web;
use Catalyst;
__PACKAGE__->config( 'Model::ApplicationMeta' => { version => '1.001', license => 'GLP', copyright => '2016', }, );
__PACKAGE__->setup;
package MyApp::Controller::Meta;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub info :Local { my ($self, $c) = @_; my $meta = $c->model('ApplicationMeta'); $c->log->debug("Application version: ${\$meta->version}"); }
Context Scope• does ACCEPT_CONTEXT, invoked during $c-
>model, $c->view (or even $c->controller).
• Can depend on literals, application scoped components and other context scoped components.
• Allows for the current context to in some way inform the component (provide dependencies, etc.)
package MyApp::Model::ApplicationMeta;
use Moose; extends 'Catalyst::Component';
has 'version' => (is=>'ro', required=>1); has 'license' => (is=>'ro', required=>1); has 'copyright' => (is=>'ro', required=>1); has 'total_requests' => (is=>'ro', required=>0);
sub ACCEPT_CONTEXT { my ($self, $c, @args) = @_; return (ref $self)->new( version => $self->version, license => $self->license, copyright => $self->copyright, total_requests => ref($c)::COUNT); }
__PACKAGE__->meta->make_immutable;
I’d prefer a Factory Component…
package MyApp::Model::ApplicationMetaFactory;
use MyApp::ApplicationMeta; use Moose; extends 'Catalyst::Component';
has 'version' => (is=>'ro', required=>1); has 'license' => (is=>'ro', required=>1); has 'copyright' => (is=>'ro', required=>1);
sub ACCEPT_CONTEXT { my ($self, $c, @args) = @_; return MyApp::ApplicationMeta->new( version => $self->version, license => $self->license, copyright => $self->copyright, total_requests => ref($c)::COUNT); }
__PACKAGE__->meta->make_immutable;
package MyApp::ApplicationMeta;
use Moo;
has 'version' => (is=>'ro', required=>1); has 'license' => (is=>'ro', required=>1); has 'copyright' => (is=>'ro', required=>1); has 'total_requests' => (is=>'ro', required=>1);
1;
• Context scoped components can be used to build any type of scope (session, request, workflow, even application!)
• Allows your component to depend on other components, but out of the box the ‘wiring’ is all manual.
• Watch out for performance issues since you are often creating a lot of objects this way.
‘Context’ Scoping can be used to build any other
type of Scope.
“PerRequest” Lifecycle
package MyApp::Model::ApplicationMetaFactory;
use Moose; use MyApp::ApplicationMeta; use Scalar::Util qw/blessed refaddr/;
extends 'Catalyst::Component';
has [‘version’, ‘license’, ‘copyright’] => (is=>'ro', required=>1);
sub ACCEPT_CONTEXT { my ($self, $c_or_app, @args) = @_; if(blessed $c_or_app) { return $c_or_app->stash->{refaddr $self} ||= $self->return_instance(ref $c_or_app); } else { return $self->return_instance($c_or_app); } }
sub return_instance { my ($self, $app) = @_; return MyApp::ApplicationMeta->new( version => $self->version, license => $self->license, copyright => $self->copyright, total_requests => $app::COUNT); }
package MyApp::Model::ApplicationMetaFactory;
use Moose; use MyApp::ApplicationMeta;
extends 'Catalyst::Component'; with 'Catalyst::Component::InstancePerContext';
has [‘version’, ‘license’, ‘copyright’] => (is=>'ro', required=>1);
sub build_per_context_instance { my ($self, $c_or_app) = @_; my $app = ref($c_or_app) || $c_or_app;
return MyApp::ApplicationMeta->new( version => $self->version, license => $self->license, copyright => $self->copyright, total_requests => $app::COUNT); }
__PACKAGE__->meta->make_immutable;
Lifecycle Questions?
Injecting Dependencies
package MyApp::LoginForm;
use HTML::FormHandler::Moose; extends 'HTML::FormHandler';
has 'user_rs' => ( is=>'ro', required=>1 );
has_field 'name' => ( type => 'Text' ); has_field 'password' => ( type => 'Password' );
1;
package MyApp::Model::LoginForm;
use Moose; use MyApp::LoginForm;
extends 'Catalyst::Component';
sub ACCEPT_CONTEXT { my ($self, $c, @args) = @_; return MyApp::LoginForm->process( user_rs => $c->model(‘Schema::User’), params => $c->req->body_params); }
__PACKAGE__->meta->make_immutable;
package MyApp::Controller::Login;
use Moose; use MooseX::MethodAttributes;
extends 'Catalyst::Controller';
sub process_login :Path('') Args(0) { my ($self, $c) = @_; my $form = $c->model('LoginForm'); # ... }
Best Practice: Don’t write a Model…
…Write a Class and use a Model Adaptor or
Model Injection.
Catalyst::Model::Adaptor
• Encapsulates three common patterns for adapting your class into a Catalyst model or view.
• Application, Factory, PerRequest.
Catalyst::Plugin::InjectionHelpers
• Builds on the component injection introduced in Catalyst version 5.90090.
• Has Application, Factory, PerRequest and PerSession lifecycle adaptors
• Less battle tested that Catalyst::Model::Adaptor
package MyApp;
use Catalyst qw/ InjectionHelpers MapComponentDependencies /;
use Catalyst::Plugin::MapComponentDependencies::Utils ':ALL';
__PACKAGE__->inject_components( 'Model::Schema' => { from_component => 'Catalyst::Model::DBIC::Schema'}, 'Model::Entities' => { from_class => 'MyApp::Entities' }, );
__PACKAGE__->config( 'Model::Entities' => { dbic_schema => FromModel 'Schema' }, 'Model::Schema' => { traits => ['Result', 'SchemaProxy'], schema_class => 'MyApp::Schema', connect_info => [ 'dbi:Pg:dbname=$ENV{DBNAME};host=$ENV{DBHOST};port=5432', '$ENV{DBUSER}', ''], }, );
__PACKAGE__->setup;
package MyApp::Entities;
use Moo;
has 'dbic_schema' => (is=>'ro', required=>1);
# Stuff!
package MyApp::Controller::Example;
use Moose; use MooseX::MethodAttributes;
sub entities :Local { my ($self, $c) = @_; my $e = $c->model('Entities'); }
Could be useful to encapsulate complex
controller logic.
• https://metacpan.org/release/Catalyst-Plugin-MapComponentDependencies
• https://metacpan.org/release/Catalyst-Plugin-InjectionHelpers
• https://metacpan.org/release/Catalyst-Model-Adaptor
• https://metacpan.org/release/Catalyst-Component-InstancePerContext
• https://metacpan.org/release/Catalyst-Model-HTMLFormhandler
• https://metacpan.org/release/Catalyst-View-Text-MicroTemplate-PerRequest
• https://metacpan.org/release/Catalyst-View-JSON-PerRequest
• https://metacpan.org/release/Catalyst-Runtime