Featured image of post 処理を重ねる:Decoratorで機能を追加 - Perl/Mooでテキスト処理パイプライン

処理を重ねる:Decoratorで機能を追加 - Perl/Mooでテキスト処理パイプライン

Chain of Responsibilityに加えてDecoratorパターンを導入し、パイプラインを強化します。SortFilter(ソート)とUniqFilter(重複排除)を追加しながら、機能の動的な重ね合わせを学びます。

@nqounetです。

前回は、GrepFilterを作成してChain of Responsibilityパターンの基礎を学びました。今回は、SortFilterとUniqFilterを追加し、Decoratorパターンの考え方も取り入れてみましょう。

このシリーズについて

前回の振り返り

前回はGrepFilterを作成し、フィルターを連鎖させる仕組みを構築しました。

1
2
3
4
5
my $second = GrepFilter->new(pattern => 'timeout');
my $first = GrepFilter->new(
    pattern     => 'ERROR',
    next_filter => $second,
);

この方法でも動作しますが、フィルターの種類が増えると、それぞれのクラスに同じような next_filter の処理を書く必要が出てきます。これは重複コードの問題(DRY原則違反)につながります。

基底クラスを導入する

Decoratorパターンの概念図:コアを複数のレイヤーが包み込む

まずは共通部分を基底クラスに抽出しましょう。

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

has next_filter => (
    is        => 'ro',
    predicate => 'has_next_filter',
);

sub process ($self, $lines) {
    my $result = $self->apply($lines);
    
    if ($self->has_next_filter) {
        return $self->next_filter->process($result);
    }
    return $result;
}

sub apply ($self, $lines) {
    # サブクラスでオーバーライドする
    return $lines;
}

1;

この設計がDecoratorパターンの核心です。

  • process メソッドは連鎖の制御を担当する
  • apply メソッドは各フィルター固有の処理を担当する
  • サブクラスは apply だけをオーバーライドすればよい

GrepFilterを書き換える

基底クラスを継承してGrepFilterを書き換えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package GrepFilter;
use Moo;
use experimental qw(signatures);
extends 'Filter';

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

sub apply ($self, $lines) {
    my $pattern = $self->pattern;
    return [grep { /$pattern/ } @$lines];
}

1;

コードがとてもシンプルになりました。apply メソッドでフィルタリング処理だけを記述すればよくなっています。

SortFilterを追加する

同様にSortFilterを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package SortFilter;
use Moo;
use experimental qw(signatures);
extends 'Filter';

sub apply ($self, $lines) {
    return [sort @$lines];
}

1;

驚くほどシンプルです。基底クラスが連鎖処理を担当してくれるため、このクラスはソート処理だけを記述すればよいのです。

UniqFilterを追加する

重複を除去するUniqFilterも作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package UniqFilter;
use Moo;
use experimental qw(signatures);
extends 'Filter';

sub apply ($self, $lines) {
    my %seen;
    return [grep { !$seen{$_}++ } @$lines];
}

1;

Perlの %seen テクニックを使った、重複除去の定番実装です。

Decoratorパターンとは

ここで、Decoratorパターンについて解説します。

	classDiagram
    class Filter {
        +next_filter
        +process(lines)
        +apply(lines)
    }
    class GrepFilter {
        +pattern
        +apply(lines)
    }
    class SortFilter {
        +apply(lines)
    }
    class UniqFilter {
        +apply(lines)
    }
    
    Filter <|-- GrepFilter
    Filter <|-- SortFilter
    Filter <|-- UniqFilter
    Filter o-- Filter : next_filter

Decoratorパターンのポイントは以下の通りです。

  • 元のオブジェクトを包み込んで機能を追加する
  • クライアントから見ると、装飾前と同じインターフェースで使える
  • 実行時に動的に機能を追加・削除できる

Chain of Responsibilityとの違いを整理すると、こうなります。

パターン目的フォーカス
Chain of Responsibilityリクエストを順番に処理者に渡す処理の連鎖と責任の分担
Decoratorオブジェクトに機能を動的に追加機能の拡張と合成

今回の実装では、両方のパターンを組み合わせています。連鎖の仕組み(Chain of Responsibility)を使いながら、各フィルターが機能を追加する(Decorator)という設計です。

3つのフィルターを繋げてみる

では、GrepFilter、SortFilter、UniqFilterを繋げてみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
my @log_lines = (
    '2026-01-30 10:00:15 ERROR: Database timeout',
    '2026-01-30 10:00:05 ERROR: Connection failed',
    '2026-01-30 10:00:15 ERROR: Database timeout',
    '2026-01-30 10:00:20 INFO: Connection restored',
    '2026-01-30 10:00:05 ERROR: Connection failed',
);

my $uniq = UniqFilter->new();
my $sort = SortFilter->new(next_filter => $uniq);
my $grep = GrepFilter->new(
    pattern     => 'ERROR',
    next_filter => $sort,
);

my $result = $grep->process(\@log_lines);

say "=== ERROR行をソートして重複除去 ===";
say $_ for @$result;

実行結果は以下の通りです。

1
2
3
=== ERROR行をソートして重複除去 ===
2026-01-30 10:00:05 ERROR: Connection failed
2026-01-30 10:00:15 ERROR: Database timeout

Unixの grep ERROR | sort | uniq と同じ結果が得られました。

問題点に気づく

今の書き方には問題があります。

1
2
3
4
5
6
my $uniq = UniqFilter->new();
my $sort = SortFilter->new(next_filter => $uniq);
my $grep = GrepFilter->new(
    pattern     => 'ERROR',
    next_filter => $sort,
);

パイプラインを構築するには、逆順にフィルターを作成する必要があります。最後に実行したい処理から先に作成しなければなりません。

これは直感に反しており、コードを読む人にとって混乱の元になります。また、フィルターの順序を変更したい場合にも、大幅な書き換えが必要です。

この問題は次回、パイプラインビルダーを導入することで解決します。

今回のポイント

  • 基底クラスFilterを導入し、共通処理を抽出した
  • SortFilterとUniqFilterを追加した
  • Decoratorパターンの考え方を学んだ
  • Chain of ResponsibilityとDecoratorの組み合わせを理解した
  • 現在の設計の問題点(逆順構築)に気づいた

今回の完成コード

以下が今回作成したコードの完成版です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo

use v5.36;

# === Filter(基底クラス) ===
package Filter {
    use Moo;
    use experimental qw(signatures);

    has next_filter => (
        is        => 'ro',
        predicate => 'has_next_filter',
    );

    sub process ($self, $lines) {
        my $result = $self->apply($lines);
        
        if ($self->has_next_filter) {
            return $self->next_filter->process($result);
        }
        return $result;
    }

    sub apply ($self, $lines) {
        return $lines;
    }
}

# === GrepFilter ===
package GrepFilter {
    use Moo;
    use experimental qw(signatures);
    extends 'Filter';

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

    sub apply ($self, $lines) {
        my $pattern = $self->pattern;
        return [grep { /$pattern/ } @$lines];
    }
}

# === SortFilter ===
package SortFilter {
    use Moo;
    use experimental qw(signatures);
    extends 'Filter';

    sub apply ($self, $lines) {
        return [sort @$lines];
    }
}

# === UniqFilter ===
package UniqFilter {
    use Moo;
    use experimental qw(signatures);
    extends 'Filter';

    sub apply ($self, $lines) {
        my %seen;
        return [grep { !$seen{$_}++ } @$lines];
    }
}

# === メイン処理 ===
package main {
    my @log_lines = (
        '2026-01-30 10:00:15 ERROR: Database timeout',
        '2026-01-30 10:00:05 ERROR: Connection failed',
        '2026-01-30 10:00:15 ERROR: Database timeout',
        '2026-01-30 10:00:20 INFO: Connection restored',
        '2026-01-30 10:00:05 ERROR: Connection failed',
    );

    # パイプライン構築(逆順に作成する必要がある)
    my $uniq = UniqFilter->new();
    my $sort = SortFilter->new(next_filter => $uniq);
    my $grep = GrepFilter->new(
        pattern     => 'ERROR',
        next_filter => $sort,
    );

    my $result = $grep->process(\@log_lines);

    say "=== ERROR行をソートして重複除去 ===";
    say $_ for @$result;
}

次回予告

次回は、逆順構築の問題を解決するためにパイプラインビルダーを導入します。Fluent Interface(流れるようなインターフェース)を使って、直感的にパイプラインを構築できるようにしましょう。

お楽しみに!

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