Featured image of post アルゴリズムとテーマを分離する - Perl Bridgeパターンによる設計改善【第4回】

アルゴリズムとテーマを分離する - Perl Bridgeパターンによる設計改善【第4回】

Bridgeパターンで生成アルゴリズムとテーマを分離。PerlとMooを使った委譲の実装により、9クラス→6クラスへの劇的削減を実現。抽象と実装の分離設計を習得します。

PerlとMooで「ランダムダンジョンジェネレーター」を作る連載の第4回です。

前回はクラス爆発問題に直面しました。 今回はBridgeパターンを導入して、この問題を解決します。

Bridgeパターンで解決

Bridgeパターンとは

Bridgeパターンは、「抽象」と「実装」を分離して、それぞれを独立に拡張できるようにする設計パターンです。

GoF(Gang of Four)の23デザインパターンの1つで、構造パターン(Structural Pattern)に分類されます。

今回のダンジョンジェネレーターでは、以下のように分離します。

  • 抽象(Abstraction): テーマ(表示方法)
  • 実装(Implementor): アルゴリズム(生成方法)

設計図

	classDiagram
    class DungeonTheme {
        <<abstract>>
        +algorithm: GenerationAlgorithm
        +generate()
        +render()
        +wall_char()*
        +floor_char()*
    }

    class CaveTheme {
        +wall_char() "#"
        +floor_char() "."
    }

    class CastleTheme {
        +wall_char() "█"
        +floor_char() "░"
    }

    class GenerationAlgorithm {
        <<role>>
        +generate(map, width, height)*
    }

    class RandomAlgorithm {
        +generate(map, width, height)
    }

    class MazeAlgorithm {
        +generate(map, width, height)
    }

    DungeonTheme <|-- CaveTheme
    DungeonTheme <|-- CastleTheme
    DungeonTheme o-- GenerationAlgorithm : 委譲
    GenerationAlgorithm <|.. RandomAlgorithm
    GenerationAlgorithm <|.. MazeAlgorithm

テーマとアルゴリズムが独立したクラス階層になっています。 テーマはアルゴリズムを「持って」いて、生成処理を委譲します。

「委譲(delegation)」とは、あるオブジェクトが別のオブジェクトに仕事を任せることです。 継承が「親クラスの能力を引き継ぐ」のに対し、委譲は「他のクラスに仕事を頼む」考え方です。

今回の場合、DungeonThemeは自分でダンジョンを生成せず、algorithmgenerateを呼び出して任せています。

アルゴリズムをRoleとして定義

まず、生成アルゴリズムのインターフェースをMoo Roleとして定義します。

1
2
3
4
5
6
7
8
9
# GenerationAlgorithm.pm - アルゴリズムのRole
package GenerationAlgorithm;
use v5.36;
use Moo::Role;

# サブクラスで実装必須
requires 'generate';

1;

requiresgenerateメソッドの実装を強制しています。 このRoleを適用するクラスは、必ずgenerateメソッドを実装しなければなりません。

ランダム配置アルゴリズム

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# RandomAlgorithm.pm - ランダム配置アルゴリズム
package RandomAlgorithm;
use v5.36;
use Moo;

with 'GenerationAlgorithm';

sub generate ( $self, $map, $width, $height ) {
    for my $y ( 1 .. $height - 2 ) {
        for my $x ( 1 .. $width - 2 ) {
            if ( rand() < 0.7 ) {
                $map->[$y][$x] = '.';
            }
        }
    }
}

1;

迷路型アルゴリズム

 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
# MazeAlgorithm.pm - 迷路型アルゴリズム
package MazeAlgorithm;
use v5.36;
use Moo;

with 'GenerationAlgorithm';

has visited => (
    is      => 'rw',
    lazy    => 1,
    default => sub { {} },
);

sub generate ( $self, $map, $width, $height ) {
    $self->visited( {} );    # リセット
    $self->_carve( $map, $width, $height, 1, 1 );
}

sub _carve ( $self, $map, $width, $height, $x, $y ) {
    my $visited = $self->visited;

    $map->[$y][$x] = '.';
    $visited->{"$x,$y"} = 1;

    my @directions = ( [ 0, -2 ], [ 0, 2 ], [ -2, 0 ], [ 2, 0 ] );
    @directions = sort { rand() <=> rand() } @directions;

    for my $dir (@directions) {
        my ( $dx, $dy ) = $dir->@*;
        my $nx = $x + $dx;
        my $ny = $y + $dy;

        if (   $nx > 0
            && $nx < $width - 1
            && $ny > 0
            && $ny < $height - 1
            && !$visited->{"$nx,$ny"} )
        {
            $map->[ $y + $dy / 2 ][ $x + $dx / 2 ] = '.';
            $self->_carve( $map, $width, $height, $nx, $ny );
        }
    }
}

1;

テーマの基底クラス

テーマの基底クラスを作成します。 アルゴリズムを属性として持ち、生成処理を委譲します。

 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
# DungeonTheme.pm - テーマの基底クラス
package DungeonTheme;
use v5.36;
use Moo;

# アルゴリズムを注入
has algorithm => (
    is       => 'ro',
    required => 1,
    does     => 'GenerationAlgorithm',
);

has width  => ( is => 'ro', default => 41 );
has height => ( is => 'ro', default => 11 );

has map => (
    is      => 'rw',
    lazy    => 1,
    builder => '_build_map',
);

sub _build_map ($self) {
    my @map;
    for my $y ( 0 .. $self->height - 1 ) {
        my @row;
        for my $x ( 0 .. $self->width - 1 ) {
            push @row, '#';
        }
        push @map, \@row;
    }
    return \@map;
}

# 生成をアルゴリズムに委譲
sub generate ($self) {
    $self->algorithm->generate( $self->map, $self->width, $self->height );
}

# 表示(テーマごとにオーバーライド)
sub render ($self) {
    my $map    = $self->map;
    my $output = '';

    for my $row ( $map->@* ) {
        for my $cell ( $row->@* ) {
            if ( $cell eq '#' ) {
                $output .= $self->wall_char;
            }
            else {
                $output .= $self->floor_char;
            }
        }
        $output .= "\n";
    }

    return $output;
}

# サブクラスでオーバーライド
sub wall_char ($self)  { '#' }
sub floor_char ($self) { '.' }

1;

algorithm属性にはdoes => 'GenerationAlgorithm'を指定しています。 これにより、GenerationAlgorithmロールを適用したオブジェクトのみを受け入れます。

doesオプションは、渡されたオブジェクトが指定したRoleを持っているかチェックします。 もしRandomAlgorithmMazeAlgorithm以外のオブジェクトを渡すと、エラーになります。 これにより、型安全性が確保されます。

テーマの具象クラス

各テーマは基底クラスを継承して、表示文字だけを変更します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# CaveTheme.pm - 洞窟テーマ
package CaveTheme;
use v5.36;
use Moo;

extends 'DungeonTheme';

# デフォルトのまま(#と.)

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# CastleTheme.pm - 城テーマ
package CastleTheme;
use v5.36;
use Moo;

extends 'DungeonTheme';

sub wall_char ($self)  { '█' }
sub floor_char ($self) { '░' }

1;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# RuinsTheme.pm - 遺跡テーマ
package RuinsTheme;
use v5.36;
use Moo;

extends 'DungeonTheme';

sub wall_char ($self)  { '▓' }
sub floor_char ($self) { '▒' }

1;

使用例

テーマとアルゴリズムを自由に組み合わせられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/usr/bin/env perl
# bridge_demo.pl - Bridgeパターンによるダンジョン生成
use v5.36;
use lib '.';

use CastleTheme;
use MazeAlgorithm;

# 城テーマ × 迷路型アルゴリズム
my $dungeon = CastleTheme->new(
    algorithm => MazeAlgorithm->new,
    width     => 41,
    height    => 11,
);

$dungeon->generate;
print $dungeon->render;

実行結果は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
█████████████████████████████████████████
█░█░░░░░█░░░░░█░░░░░░░█░█░░░░░█░░░░░░░░░█
█░█░███░█░███░█░█████░█░█░███░█░███████░█
█░█░░░█░█░░░█░█░█░░░█░█░█░█░░░█░░░░░░░█░█
█░███░█░███░█░█░█░█░█░█░█░█░█████████░█░█
█░░░░░█░░░░░█░░░█░█░░░█░░░█░░░░░░░░░░░█░█
█████░█████░█████░█░███████████████░███░█
█░░░█░░░░░█░░░░░█░█░█░░░░░░░░░░░░░█░░░█░█
█░█░█████░█████░█░█░█░███████████░███░█░█
█░█░░░░░░░░░░░░░█░░░█░░░░░░░░░░░░░░░█░░░█
█████████████████████████████████████████

城テーマで迷路型のダンジョンが生成されました。

クラス数の比較

Bridgeパターン導入前後でクラス数を比較してみましょう。

導入前(継承ベース)の場合は以下のクラスが必要でした。

1
2アルゴリズム × 3テーマ = 6クラス

Bridgeパターン導入後は以下の通りです。

1
2
3
アルゴリズム: 2クラス + 1ロール = 3モジュール
テーマ:       3クラス + 1基底   = 4モジュール
合計:         7モジュール

現時点ではあまり差がありませんが、組み合わせが増えると効果が顕著になります。

3アルゴリズム × 4テーマの場合は以下のようになります。

  • 導入前: 3 × 4 = 12クラス
  • 導入後: 3 + 4 + 2(ロール・基底) = 9モジュール

4アルゴリズム × 5テーマの場合は以下の通りです。

  • 導入前: 4 × 5 = 20クラス
  • 導入後: 4 + 5 + 2 = 11モジュール

今回のまとめ

第4回では、Bridgeパターンを導入してクラス爆発問題を解決しました。

  • アルゴリズムをRoleとして分離(Implementor)
  • テーマを基底クラスで統一(Abstraction)
  • 委譲により、テーマとアルゴリズムを自由に組み合わせ
  • n × m クラスが n + m クラスに削減

次回は、新しいテーマ「水中神殿」を追加して、Bridgeパターンの拡張性を体験します。 既存のコードを変更せずに、1クラス追加するだけで済むことを確認しましょう。

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