Featured image of post 第5回-Compositeパターンでツリー構造を作ろう - PerlとMooで学ぶComposite

第5回-Compositeパターンでツリー構造を作ろう - PerlとMooで学ぶComposite

PerlとMooでCompositeパターンを実装!if/else階層判定の限界を克服し、見出しツリー構造を作る方法を学びます。Heading Role、SectionHeadingで部分-全体階層を統一的に扱う体験。

前回の振り返り

前回は、階層構造をif/else条件分岐で表現しようとして、壁にぶつかりました。レベル差の計算、HTMLタグの開閉、状態管理が複雑に絡み合い、条件分岐が爆発する問題を体験しました。

今回は、この問題を解決するCompositeパターンを導入します。

Compositeパターンとは

Compositeパターンは、部分-全体階層を統一的に扱うためのデザインパターンです。

	classDiagram
    class Heading {
        <<Role>>
        +level() int
        +text() string
        +render() string
    }
    class LeafHeading {
        -level: int
        -text: string
        +render() string
    }
    class SectionHeading {
        -level: int
        -text: string
        -children: Heading[]
        +add_child(Heading)
        +render() string
    }
    Heading <|.. LeafHeading
    Heading <|.. SectionHeading
    SectionHeading o-- Heading : children

3つの構成要素

  1. Component(Heading Role): 共通インターフェースを定義
  2. Leaf(LeafHeading): 子を持たない末端要素
  3. Composite(SectionHeading): 子を持つ複合要素

ポイントは、LeafとCompositeが同じインターフェース(Role)を実装していること。これにより、「子を持つ要素」も「持たない要素」も、同じrender()メソッドで処理できます。

よくある例との比較

Compositeパターンの典型例はファイルシステムです:

  • Leaf: ファイル(他のファイルを含まない)
  • Composite: ディレクトリ(ファイルや他のディレクトリを含む)

私たちのMarkdown目次では:

  • Leaf: 子見出しを持たない見出し
  • Composite: 子見出しを持つセクション

Heading Roleの実装

まず、共通インターフェースを定義するRoleを作成します。

1
2
3
4
5
6
7
8
9
package Heading;
use v5.36;
use Moo::Role;

requires 'level';
requires 'text';
requires 'render';

1;

requiresで、このRoleを実装するクラスが必ず提供すべきメソッドを宣言します。

LeafHeadingクラスの実装

子を持たない末端の見出しです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package LeafHeading;
use v5.36;
use Moo;

with 'Heading';

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

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

sub render ($self, $indent = 0) {
    my $spaces = '  ' x $indent;
    return $spaces . '- ' . $self->text;
}

1;

renderメソッドは、インデントレベルを受け取り、その分のスペースを先頭に付けて見出しを返します。

SectionHeadingクラスの実装

子を持つセクション見出しです。

 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
package SectionHeading;
use v5.36;
use Moo;

with 'Heading';

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

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

has 'children' => (
    is      => 'ro',
    default => sub { [] },
);

sub add_child ($self, $child) {
    push $self->children->@*, $child;
    return $self;
}

sub render ($self, $indent = 0) {
    my $spaces = '  ' x $indent;
    my @lines = ($spaces . '- ' . $self->text);
    
    for my $child ($self->children->@*) {
        push @lines, $child->render($indent + 1);
    }
    
    return join("\n", @lines);
}

1;

コードの解説

children属性

1
2
3
4
has 'children' => (
    is      => 'ro',
    default => sub { [] },
);

子要素を格納する配列リファレンス。default => sub { [] }で、インスタンスごとに新しい空配列を生成します。

add_childメソッド

1
2
3
4
sub add_child ($self, $child) {
    push $self->children->@*, $child;
    return $self;
}

子要素を追加します。return $selfでメソッドチェーンを可能にしています。

renderメソッド(再帰的処理)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sub render ($self, $indent = 0) {
    my $spaces = '  ' x $indent;
    my @lines = ($spaces . '- ' . $self->text);
    
    for my $child ($self->children->@*) {
        push @lines, $child->render($indent + 1);
    }
    
    return join("\n", @lines);
}

自分自身をレンダリングした後、各子要素のrenderを呼び出します。子要素も同じrenderメソッドを持っているので、子がさらに子を持っていれば、その子も再帰的に処理されます。

これがCompositeパターンの威力です。

使用例:ツリー構造を手動で構築

まずは手動でツリー構造を構築してみましょう。

 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
#!/usr/bin/env perl
use v5.36;
use lib '.';
use LeafHeading;
use SectionHeading;

# ツリー構造を構築
my $root = SectionHeading->new(
    level => 1,
    text  => 'はじめに',
);

my $chapter1 = SectionHeading->new(
    level => 2,
    text  => '第1章',
);

my $section1_1 = LeafHeading->new(
    level => 3,
    text  => 'セクション1.1',
);

my $chapter2 = LeafHeading->new(
    level => 2,
    text  => '第2章',
);

# 親子関係を設定
$chapter1->add_child($section1_1);
$root->add_child($chapter1);
$root->add_child($chapter2);

# レンダリング
say $root->render;

実行結果

1
2
3
4
- はじめに
  - 第1章
    - セクション1.1
  - 第2章

条件分岐なしで、再帰的に正しい階層構造がレンダリングされました!

関連する構造パターン

Compositeと同じく「構造パターン」に分類されるパターン:

Compositeとの違いは、部分-全体階層の統一的扱いにあります。

完成コード

Heading Role

1
2
3
4
5
6
7
8
9
package Heading;
use v5.36;
use Moo::Role;

requires 'level';
requires 'text';
requires 'render';

1;

LeafHeading

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package LeafHeading;
use v5.36;
use Moo;

with 'Heading';

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

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

sub render ($self, $indent = 0) {
    my $spaces = '  ' x $indent;
    return $spaces . '- ' . $self->text;
}

1;

SectionHeading

 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
package SectionHeading;
use v5.36;
use Moo;

with 'Heading';

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

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

has 'children' => (
    is      => 'ro',
    default => sub { [] },
);

sub add_child ($self, $child) {
    push $self->children->@*, $child;
    return $self;
}

sub render ($self, $indent = 0) {
    my $spaces = '  ' x $indent;
    my @lines = ($spaces . '- ' . $self->text);
    
    for my $child ($self->children->@*) {
        push @lines, $child->render($indent + 1);
    }
    
    return join("\n", @lines);
}

1;

まとめ

今回は、Compositeパターンを導入して、見出しのツリー構造を表現できるようにしました。

学んだこと:

  • Compositeパターンの3つの構成要素(Component、Leaf、Composite)
  • Moo::Rolerequiresで共通インターフェースを定義
  • 再帰的なrenderメソッドでツリー全体を処理
  • 条件分岐なしで階層構造をレンダリング

現時点では、ツリー構造を手動で構築しています。次回は、パーサーを実装して、抽出した見出しの配列から自動的にツリー構造を構築します。

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