@nqounetです。
前回は「継承」によって機能を追加しようとした結果、クラスの数が爆発的に増えてしまう「クラス爆発」問題に直面しました。
今回はいよいよ、この問題を解決する救世主「Decoratorパターン」を導入します。
Decoratorパターンの考え方
継承が「親子の関係(Is-A)」だとすれば、Decoratorは「マトリョーシカの関係(Has-A / Wraps-A)」です。
- 中心に「基本の機能(LogParser)」がいる
- それを「フィルタリング機能(Decorator)」で包む
- さらにそれを「別の機能(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を追加しよう