catalyst patterns-yapc-eu-2016

Post on 16-Apr-2017

110 Views

Category:

Software

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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

top related