― Web Technology and Life ―

PerlのCPANモジュールQudoにみるTemplateMethodパターン

2011-08-27
『PerlのCPANモジュールに学ぶオブジェクトデザインパターン』第一弾、かつ、エアHachioji.pm#8のLTとしてQudoからデザインパターンの中でも基本であるTemplateMethodパターンを読み取ります。

問題:似たような処理を毎回ぐちゃぐちゃ書いている

概要

「大枠のやっていることは同じなんだけど、対象がちょっと異なって対象毎の特徴に合わせてコードを書いていると、なんだかまったく別のコードになって、最終的にコード全体のメンテナンス性の低下する」ということはしばしばあります。

具体例

例えば、Twitterとfacebookのタイムラインを取得するというコードを考えます。

#facebookのタイムライン取得クラス
package TL::Facebook;
use WWW::Facebook::API;

sub fetch_tl {
    my $facebook = WWW::Facebook::API->new(...);
    .
    .
    .
    return +[
        +{
            message   => '',
            name      => '',
            timestamp => '',
        },
        +{
            message   => '',
            name      => '',
            timestamp => '',
        },
    ];
}
#Twitterのタイムライン取得クラス
package TimeLine::Twiiter;
use Net::Twitter;

sub get_tl_twitter {
    my $twitter = Net::Twitter->new(...);
    .
    .
    .
    return +[
        +{
            tweet    => '',
            user     => '',
            datetime => '',
        },
        +{
            tweet    => '',
            user     => '',
            datetime => '',
        },
    ];
}

2つくらいならまだいいですが、こんな統一感のないコードだと、mixiとかIRCなどにも対応していこうと思うとこのままだと何がなんだかわからなくなってきます。

TemplateMethodパターンで解決

先ほどのコードの問題は、統一感がなくて横展開しづらいというあたりが問題でした。つまり、パッケージ名にしても、メソッド名にしても、リターンバリューの構造にしても、共通化できそうなところがバラバラということです。

TemplateMethodパターンとは

このようなときに強力な威力を発揮するのがTemplateMethodパターンです。TemplateMethodパターンは、共通の処理構造を抽出して抽象クラスとして定義し、抽象クラスを継承する具象クラスに個別の対象に関する具体的な内容を実装するというデザインパターンです。

問題の解決例

先ほどの問題の例にに対する解決としてTemplateMethodパターンをあてると以下のようなコードを書くことになります。

まず、「タイムライン取得」するということそもそもやりたいことの共通項なので、それを抽象化させたクラスを書きます。Perlの継承は、「use parent '継承したいクラス名'」で行えます。

#「タイムライン取得」に関する抽象クラス
package TimeLine;

sub get_timeline {
    die 'this method must be override';

    return +[
        +{
            message  => '',
            user     => '',
            datetime => '',
        },
        +{
            tweet    => '',
            user     => '',
            datetime => '',
        },
    ];
}

次に、抽象クラスTimeLineを継承したTwitterとFacebookの各クラスを書きます。

#TimeLineを継承したTwitterのタイムライン取得クラス
package TimeLine::Twitter;
use parent 'TimeLine';
use Net::Twitter;


sub get_timeline {
    my $twitter = Net::Twitter->new(...);
    .
    .
    .
    return +[
        +{
            message  => '',
            user     => '',
            datetime => '',
        },
        +{
            tweet    => '',
            user     => '',
            datetime => '',
        },
    ];
}
#TimeLineを継承したFacebookのタイムライン取得クラス
package TimeLine::Facebook;
use parent 'TimeLine';
use WWW::Facebook::API;

sub get_timeline {
    my $facebook = WWW::Facebook::API->new(...);
    .
    .
    .
    return +[
        +{
            message   => '',
            name      => '',
            timestamp => '',
        },
        +{
            message   => '',
            name      => '',
            timestamp => '',
        },
    ];
}

mixiやIRCなど他のタイムラインを取りたい時も、メンテナンス性の高いわかりやすいコードを気軽横展開していくことができます。

CPANモジュールQudoにみるTemplateMethodパターン

今回は、nekokakさんが「WEB+DB PRESS Vol.64」でジョブキューをとりあげていたので、流れに乗っかってnekokakさん作のQudoを具体的な実装として取り上げます。

具体的な実装箇所

Qudoからは以下の三つの箇所にTemplateMethodパターンを見出すことができます。(もちろん、別のデザインパターンとしても見れますが、とりあえず、ここではTemplateMethodパターンとして扱います。)

「WEB+DB PRESS Vol.64」でもnekokakさんが言及していますが、Qudoは拡張を売りにしているジョブキューシステムなので、上記の三つのクラスが、それぞれTemplateMethodパターンの抽象クラスであり、ライブラリのユーザーが独自に拡張する部分が具象クラスとなります。それは、Qudo::Hookを取り上げてさらに具体的に見ていきます。

Qudo::Hook内での具体的な実装

Qudoでは、ジョブのエンキュー(インサート)前後、ジョブの実行前後に、特定の処理をライブラリのユーザー側が実装して埋め込むことができます。具体的には、以下の6つのポイントにコールバックを登録できます。

  • pre_enqueue : jobをdatabaseにenqueueする前に呼び出されます
  • post_enqueue : jobをdatabaseにenqueueされた後に呼び出されます
  • pre_work : jobの実際の処理が行われる前に呼び出されます。
  • psot_work : jobの実際の処理が行われた後に呼び出されます。
  • serialize : jobをdatabaseにenqueueする直前、pre_enqueueの呼び出し直後によびだされます。
  • deserialize : jobの実際の処理が行われる直前、pre_workの呼び出し直後によびだされます。

それでは、具体的な実装を確認しましょう。まず、TemplateMethodパターンの抽象クラスであるQudo::Hookと具象クラスであるQudo::Hook::Notify::ReachMaxRetryをみてください。

#TemplateMethodパターンの抽象クラスであるQudo::Hook
package Qudo::Hook;
use strict;
use warnings;

sub load {
    warn 'this method is abstract';
}

sub unload {
    warn 'this method is abstract';
}

1;
#TemplateMethodパターンの具象クラスであるQudo::Hook::Notify::ReachMaxRetry
package Qudo::Hook::Notify::ReachMaxRetry;
use strict;
use warnings;
use base 'Qudo::Hook';

sub hook_point { 'post_work' }

sub load {
    my ($class, $klass) = @_;

    $klass->hooks->{post_work}->{'notify_reach_max_retry'} = sub {
        my $job = shift;

        if ($job->is_failed && ( $job->funcname->max_retries <= ($job->retry_cnt) )) {
            $klass->plugin->{logger}->emergency(
                sprintf('%s already retry max!!',$job->funcname)
            );
        }
    };
}

sub unload {
    my ($class, $klass) = @_;

    delete $klass->hooks->{post_work}->{'notify_reach_max_retry'};
}


1;

使われ方としては、まず、ライブラリの内部でloadが呼び出され、「$klass->hooks->{post_work}->{'notify_reach_max_retry'}」のようなコードリファレンスがQudoオブジェクトに登録されます。登録されたコードhook_pointのポイントで呼び出されます。基本的に、ライブラリのユーザーはQudo::Hookで定義されているメソッドをオーバーライドして埋め込みたいコールバックを書けばよいだけです。(抽象クラスにhook_pointの定義がありませんが、まぁそのあたりはドキュメント等からよみとるということで)

ライブラリのユーザーが書いた具象クラスは以下のようにQudoオブジェクトを生成するとき等に登録できます。

#Qudo::Hook::Notify::ReachMaxRetryの登録の仕方
use Qudo;

my $qudo = Qudo->new(
    driver_class => 'Skinny', # optional.
    databases => [+{
        dsn      => 'dbi:SQLite:/tmp/qudo.db',
        username => '',
        password => '',
    }],
    default_hooks => qw/Qudo::Hook::Notify::ReachMaxRetry/,
);

ライブラリの内部でHookが登録されるところとHookが呼び出されるところは以下のようにQudo::Manager実装されています。

#Qudo::Manager
use Qudo::Manager;

.
.
.

#@hook_modulesにQudo::Hook::Notify::ReachMaxRetryなどが入っている
sub register_hooks {
    my ($self, @hook_modules) = @_;

    for my $module (@hook_modules) {
        $module->require or Carp::croak $@;
        #ここでloadが呼びされてQudo::Managerオブジェクトにコールバックが登録
        $module->load($self);
    }
}

.
.
.

sub call_hook {
    my ($self, $hook_point, $args) = @_;

    #hookポイント別に登録されたHookのコールバックが呼び出される
    for my $module (keys %{$self->hooks->{$hook_point}}) {
        my $code = $self->hooks->{$hook_point}->{$module};
        $code->($args);
    }
}
.
.
.

まとめ~使いどころ、メリット/デメリット~

TemplateMethodパターンは、抽象的なコードを考えるときの基本なので、即座に業務でも応用が効きますし、今後取り上げるデザインパターンの多くがこのパターンの派生形となっているので、しっかりと身につけておきたい重要なデザインパターンだと思います。

参考になる実装がたくさん学べるCPANモジュールでも、上記のQudoの例でも見たように、もPlugin機構を備えているモジュールからはTemplateMethodパターンかその派生形のデザインパターンを見出すことができます。また、ウェブアプリケーションフレームワークを筆頭に、継承して利用するタイプのフレームワークは全体がTemplateMethodパターンの抽象クラスであると言えます。

このように幅広い利用ができる一方で注意しなければいけないのが、共通化を考えるあまり、パフォーマンスが低下したり、共通化できないものを強引に共通化しようとしてしまうこともあります。本当に共通化できるのか、よくよく考えることも重要です。

終わりに

いろいろ書きましたが、ぼくもデザインパターン勉強中なので間違っていることがあれば、twitterやcommentでどしどし指摘ください。個人的には、最初の解説にいれたサンプルとか助長でいらないような気もしつつ、どうせ書くならしっかりと動くものを書けばいい気もしつつ、しっかり書きすぎるとその中で使っているモジュールとかコードがわかりづらい原因になる気もしつつ、とか、全体的に助長だったかなという感じ。まぁ、いくつか書きながら洗練させていければと思っています。

Perl hachioji.pm update_at : 2011-08-28T11:13:44
hirobanex.netの更新情報の取得
 RSSリーダーで購読する   
blog comments powered by Disqus