Featured image of post 第5回-Decoratorパターンで柔軟に機能追加しよう - PerlとMooで学ぶDecorator

第5回-Decoratorパターンで柔軟に機能追加しよう - PerlとMooで学ぶDecorator

いよいよDecoratorパターンの登場です。Roleを使った共通インターフェースの定義と、Mooのaround修飾子を使った動的な機能追加(ラッピング)の実装方法を解説します。

@nqounetです。

前回は「継承」によって機能を追加しようとした結果、クラスの数が爆発的に増えてしまう「クラス爆発」問題に直面しました。

今回はいよいよ、この問題を解決する救世主「Decoratorパターン」を導入します。

Decoratorパターンの考え方

継承が「親子の関係(Is-A)」だとすれば、Decoratorは「マトリョーシカの関係(Has-A / Wraps-A)」です。

  1. 中心に「基本の機能(LogParser)」がいる
  2. それを「フィルタリング機能(Decorator)」で包む
  3. さらにそれを「別の機能(Decorator)」で包む

外側から見ると、どれだけ包んでも「ただのログパーサー」に見えるのがポイントです。

ステップ1: 共通の「顔」を作る (Role)

まず、「ログパーサーとは何か?」を定義する共通のインターフェース(Role)を作ります。

1
2
3
4
5
6
7
package LogProcessor;
use Moo::Role;

# 「next_logというメソッドを必ず持っていること」という契約
requires 'next_log';

1;

そして、第2回で作った LogParser に、このRoleを適用(with)しておきます。

1
2
3
4
5
6
7
8
package LogParser;
use Moo;
# ... (以前と同じコード)

# LogProcessorの役割を果たすことを宣言
with 'LogProcessor';

# ...

ステップ2: Decoratorのひな型を作る

次に、Decoratorたちの親玉となるクラスを作ります。

このクラスは、自分自身も LogProcessor でありながら、別の LogProcessor を持っている(包んでいる) というのが最大の特徴です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package LogDecorator;
use Moo;
use experimental qw(signatures);

# 包み込む対象(wrapped)を持つ
has wrapped => (
    is       => 'ro',
    does     => 'LogProcessor', # LogProcessorの役割を持つものしか受け取らない
    required => 1,
);

# デフォルトでは、呼び出しをそのまま中身に委譲する
sub next_log ($self) {
    return $self->wrapped->next_log;
}

# 自分自身もLogProcessorとして振る舞う(Roleはメソッド定義後に適用)
with 'LogProcessor';

1;

このコードのポイントは、with 'LogProcessor'next_log メソッドのに書いていることです。Roleの requires は適用時点でメソッドの存在をチェックするため、先に next_log を定義しておく必要があります。

ステップ3: 具体的なDecoratorを作る

では、IPフィルタリング機能をDecoratorとして実装してみましょう。

LogDecorator を継承して、next_log だけを自分のロジックで「上書き(オーバーライド)」します。しかし、Mooにはもっと便利な around という機能があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package IPFilterDecorator;
use Moo;
use experimental qw(signatures);

extends 'LogDecorator';

has target_ip => ( is => 'ro', required => 1 );

# next_logの処理を「包み込む」
around next_log => sub ($orig, $self) {
    # 中身($orig)からログを取得してループ
    while (defined(my $log = $self->$orig)) {
        # フィルタリング条件
        if ($log->{ip} eq $self->target_ip) {
            return $log;
        }
    }
    return undef;
};

1;

使ってみよう

これがDecoratorパターンの真骨頂です。レゴブロックのように組み立ててみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use LogParser;
use IPFilterDecorator;

# 1. まず基本のパーサーを作る
my $parser = LogParser->new(filename => 'access.log');

# 2. それをIPフィルターで包む
my $filtered_parser = IPFilterDecorator->new(
    wrapped   => $parser,
    target_ip => '127.0.0.1',
);

# 3. 使う(使い方はLogParserと同じ!)
while (defined(my $log = $filtered_parser->next_log)) {
    print "IP: $log->{ip}\n";
}

クラス爆発との決別

この方法なら、「404エラー検出機能(StatusFilterDecorator)」を追加したい時も、単に新しいクラスを1つ作るだけで済みます。

IPAndStatusFiltered... のような組み合わせクラスを事前に作る必要はありません。実行時に自由に組み合わせられるのです。

1
2
3
4
5
6
7
8
# IPでフィルタリングし、かつ 404 のものだけ
my $pipeline = StatusFilterDecorator->new(
    target_status => 404,
    wrapped       => IPFilterDecorator->new(
        target_ip => '127.0.0.1',
        wrapped   => LogParser->new(filename => 'access.log'),
    ),
);

これが「コンポジション(組み合わせ)」の力です!

次回予告

これで基礎は完成しました。次回は、この仕組みを使って「統計情報を集計する」という全く新しい機能を、既存コードを一切変更せずに(!)追加してみましょう。

第6回: 統計集計Decoratorを追加しよう

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。