「デザインパターンは勉強したけど、いつ組み合わせて使えばいいかわからない」——そんな悩みを抱えていませんか?
この記事では、複雑な正規表現を解読・可視化するツールを作りながら、Interpreter・Visitor・Compositeの3つのパターンを実践します。正規表現のツリー構造、マッチング処理、複数の出力形式——それぞれの問題に対して、パターンがどう解決策を提供するかを体験しましょう。
完成したら、「君の正規表現、冗長だよ」と友人のコードを添削できるようになります。
この記事で学べること
| パターン | 解決する問題 | 本記事での役割 |
|---|---|---|
| Composite | ツリー構造の統一的な扱い | 正規表現のAST構築 |
| Interpreter | 式の評価・実行 | マッチング処理の実装 |
| Visitor | 構造を変えずに操作追加 | 可視化・最適化機能 |
対象読者
- デザインパターンの名前は知っているが、組み合わせ方がわからない方
- Perl入学式を卒業し、次のステップに進みたい方
- 正規表現の内部構造に興味がある方
技術スタック
- Perl v5.36以降(signatures使用)
- Moo によるオブジェクト指向
- CLI(コマンドライン)環境
第1章: 正規表現を解読してみよう

今回の目標
- 正規表現の構造を人間が読める形に展開する
- 最もシンプルな実装からスタート
複雑な正規表現の悩み
こんな正規表現を見たことはありませんか?
| |
Stack Overflowからコピペしたけど、何をやっているか説明できない……。そんな経験、ありますよね。
まずは簡単な正規表現から始めましょう。a(b|c)*d という正規表現を「解読」してみます。
最初の実装
| |
実行結果
| |
動きました!……でも、これはハードコードですね。
第2章: 複雑な正規表現で破綻
今回の目標
- 動的に正規表現を解析する
- 条件分岐の限界を体験する
パーサーを作ってみる
正規表現を1文字ずつ見て解析してみましょう。
| |
実行結果
| |
一見うまくいっていますが……
ネストしたグループで破綻
a((b|c)(d|e))*f のようなネストした正規表現を解析しようとすると?
| |
問題点を整理しましょう:
- グループのネストを追跡するのが大変
- 選択
|の優先順位が不明確 - 繰り返し
*+?がどこにかかるか曖昧 - 新しい機能を追加するたびにif/elseが増殖
これは設計を見直す必要があります。
第3章: 正規表現をツリーで表す

今回の目標
- 正規表現をツリー構造(AST)で表現する
- Compositeパターンを導入する
正規表現はツリー構造
a(b|c)*d を図にすると、こうなります:
graph TD
Root[Concat] --> L1[Literal 'a']
Root --> S[Star]
Root --> L2[Literal 'd']
S --> G[Group]
G --> A[Alt]
A --> L3[Literal 'b']
A --> L4[Literal 'c']
各ノードは:
- リーフノード: リテラル文字(
a,b,c,d) - 複合ノード: 連結(Concat), 選択(Alt), 繰り返し(Star), グループ(Group)
これはまさにCompositeパターンの構造です!
ASTノードの定義
まず、すべてのノードに共通するインターフェースをRoleで定義します。
| |
実行結果
| |
ASTからオリジナルの正規表現を復元できました!これがCompositeパターンの威力です。
Compositeパターンのポイント
| 構成要素 | 本記事での役割 |
|---|---|
| Component | Node Role(共通インターフェース) |
| Leaf | Literal(リテラル文字) |
| Composite | Concat, Alt, Star, Group(子を持つノード) |
クライアントは to_string を呼ぶだけで、リーフでも複合ノードでも同じように扱えます。
第4章: パーサーでツリーを組み立て
今回の目標
- 文字列からASTを自動構築する
- 再帰下降パーサーを実装する
パーサーの設計
正規表現の文法を整理します(優先順位順):
| |
これを再帰下降パーサーとして実装します。
| |
実行結果
| |
文字列からASTを自動構築できるようになりました!
第5章: ツリーを歩いてマッチング

今回の目標
- ASTを走査して文字列とマッチングする
- Interpreterパターンを導入する
各ノードに evaluate メソッドを追加
Interpreterパターンでは、各ノードが「自分自身を評価する」メソッドを持ちます。
| |
実行結果
| |
ASTを歩いてマッチング判定ができるようになりました!これがInterpreterパターンです。
Interpreterパターンのポイント
| 構成要素 | 本記事での役割 |
|---|---|
| AbstractExpression | Node Role(evaluate必須) |
| TerminalExpression | Literal(最も単純な評価) |
| NonterminalExpression | Concat, Alt, Star(子に委譲) |
| Context | 入力文字列と現在位置 |
第6章: 可視化機能を追加したい

今回の目標
- 正規表現を可読形式で出力したい
- 開放閉鎖原則(OCP)の問題を体験する
新しい出力形式を追加
to_string とは別に、インデント付きの可読形式で出力したい:
| |
素朴なアプローチ: 各クラスにメソッド追加
| |
問題点
これは動作しますが、問題があります:
新しい出力形式を追加するたびに全クラスを修正
- Markdown出力、JSON出力、HTML出力……
- クラスが「閉じていない」(OCP違反)
クラスの責務が増え続ける
- ノードクラスは「構造の表現」に集中すべき
- 出力形式ごとにメソッドが増殖
graph TD
subgraph "今の状態"
L1[Literal]
L1 --> M1[to_string]
L1 --> M2[evaluate]
L1 --> M3[pretty_print]
L1 --> M4[to_json]
L1 --> M5[to_html]
L1 --> M6[...]
end
「操作を追加するたびにクラスを変更しなければならない」 これが第7章で解決する問題です。
第7章: 操作をクラスに分離
今回の目標
- Visitorパターンを導入する
- ノードクラスを変更せずに操作を追加する
Visitorパターンの構造
graph LR
subgraph "ノードクラス(変更しない)"
L[Literal]
C[Concat]
A[Alt]
S[Star]
end
subgraph "Visitor(操作を追加)"
V1[PrettyPrinter]
V2[JsonExporter]
V3[Optimizer]
end
L --> |accept| V1
L --> |accept| V2
L --> |accept| V3
実装
| |
実行結果
| |
ノードクラスに accept メソッドを1回追加するだけで、新しい Visitor を自由に追加できるようになりました!
Double Dispatch のしくみ
| |
これにより、「ノードの種類 × 操作の種類」の組み合わせを型安全に処理できます。
第8章: 最適化と鉄道図も追加
今回の目標
- 複数の Visitor を追加する
- ノードクラスを変更せずに機能拡張を体験
RailroadDiagram Visitor
正規表現を鉄道図風のASCIIアートで可視化します。
| |
Optimizer Visitor(簡易版)
冗長な記法を検出します。
| |
使用例
| |
実行結果
| |
ノードクラスを一切変更せずに、3つの Visitor を追加できました!
第9章: 3パターンで完成!
今回の目標
- 3つのパターンの連携を振り返る
- パターン名と役割を明確にする
完成した構造
graph TD
subgraph "入力"
R[/"a(b|c)*d"/]
end
subgraph "パーサー"
P[RegexParser]
end
subgraph "Composite パターン"
C[Concat]
C --> L1[Literal 'a']
C --> S[Star]
C --> L2[Literal 'd']
S --> G[Group]
G --> A[Alt]
A --> L3[Literal 'b']
A --> L4[Literal 'c']
end
subgraph "Interpreter パターン"
I[evaluate]
end
subgraph "Visitor パターン"
V1[PrettyPrinter]
V2[RailroadDiagram]
V3[Optimizer]
end
R --> P
P --> C
C --> I
C --> V1
C --> V2
C --> V3
3パターンの役割
| パターン | 解決した問題 | 本記事での実装 |
|---|---|---|
| Composite | ツリー構造の統一的な扱い | Node Role, Literal, Concat, Alt, Star, Group |
| Interpreter | 式の評価・実行 | 各ノードの evaluate メソッド |
| Visitor | 構造を変えずに操作追加 | Visitor Role, PrettyPrinter, RailroadDiagram, Optimizer |
なぜこの組み合わせが強力か
Composite で構造を定義
- リーフ(Literal)と複合(Concat, Alt, Star)を統一的に扱えます
- 再帰的な構造を自然に表現します
Interpreter で基本動作を実装
- 各ノードが「自分自身を評価する」責務を持ちます
- マッチング処理が構造に沿って実行されます
Visitor で拡張性を確保
- 可視化、最適化、変換など様々な操作を追加します
- ノードクラスを閉じたまま(OCP遵守)
まとめ
「正規表現リファインリー」を作りながら、3つのデザインパターンを体験しました:
- 第3章: Composite でツリー構造を表現
- 第5章: Interpreter で評価処理を実装
- 第7章: Visitor で操作を分離
これらのパターンは単独でも有用ですが、組み合わせることで言語処理やコンパイラ設計の基礎となる強力なアーキテクチャを構築できます。
完成コード
最終的なコードを1ファイルにまとめました。
| |
実行例
| |
パターン対応表
| GoF用語 | 本記事での実装 |
|---|---|
| Composite | |
| Component | Node Role |
| Leaf | Literal |
| Composite | Concat, Alt, Star, Plus, Optional, Group |
| Interpreter | |
| AbstractExpression | Node Role(evaluate必須) |
| TerminalExpression | Literal |
| NonterminalExpression | Concat, Alt, Star, Plus, Optional, Group |
| Context | 入力文字列 + 位置 |
| Visitor | |
| Visitor | Visitor Role |
| ConcreteVisitor | PrettyPrinter, etc. |
| Element | Node Role(accept必須) |
| ConcreteElement | 各ノードクラス |
発展課題
完成したツールをさらに拡張してみましょう:
- 文字クラス対応:
[abc],[a-z],[^abc] - メタ文字対応:
.,\d,\w,\s - JSON出力 Visitor: ASTをJSON形式でエクスポート
- Markdown出力 Visitor: 正規表現のドキュメント生成
- 性能分析 Visitor: バックトラッキングの深さを測定
これらの拡張は、ノードクラスを変更せずに Visitor を追加するだけで実現できます。
関連シリーズ
各パターンを単独で学びたい場合は、以下のシリーズもご覧ください:
