Featured image of post Perlで作るASCIIアート・フォントレンダラー(Singleton × Flyweight × Proxy)

Perlで作るASCIIアート・フォントレンダラー(Singleton × Flyweight × Proxy)

ASCIIアートの巨大文字を表示しながら、Singleton・Flyweight・Proxyの3パターンを実践。Mooで書くPerlデザインパターン入門。

Perlで作るASCIIアート・フォントレンダラー(Singleton × Flyweight × Proxy)

プログラミング学習でデザインパターンを学んだものの、「結局どう使えばいいの?」と迷っていませんか?本記事では、ASCIIアートで巨大文字を表示するツールを作りながら、Singleton・Flyweight・Proxyの3パターンを体験します。

「動く → 破綻 → パターン導入 → 完成」というストーリーで、なぜそのパターンが必要なのかを実感しながら学べます。

この記事で学べること

パターン解決する問題本記事での役割
Singleton設定が複数箇所に散らばるフォント設定の一元管理
Flyweight同じオブジェクトの大量生成文字グリフの共有
Proxy重いリソースの先読みフォントデータの遅延ロード

対象読者

  • デザインパターンの名前は知っているが、使いどころがわからない方
  • Perl入学式を卒業し、次のステップに進みたい方
  • オブジェクト指向の実践的な使い方を学びたい方

技術スタック

  • Perl v5.34以降(signatures使用)
  • Moo によるオブジェクト指向
  • CLI(コマンドライン)環境

第1章: 巨大な文字を表示してみよう

今回の目標

  • ASCIIアートで「HELLO」を表示する
  • 最もシンプルな実装からスタート

最初の実装

まずは、各文字のASCIIアートをハードコードで定義してみましょう。

 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
#!/usr/bin/env perl
use v5.34;
use warnings;

sub render_H {
    return <<"END_ART";
 H   H
 H   H
 HHHHH
 H   H
 H   H
END_ART
}

sub render_E {
    return <<"END_ART";
 EEEEE
 E
 EEE
 E
 EEEEE
END_ART
}

sub render_L {
    return <<"END_ART";
 L
 L
 L
 L
 LLLLL
END_ART
}

sub render_O {
    return <<"END_ART";
  OOO
 O   O
 O   O
 O   O
  OOO
END_ART
}

# メイン処理
say "=== ASCIIアート・フォントレンダラー v0.1 ===";
say "";
say "「HELLO」を表示します:";
say "";

# 各文字を順番に表示
my @lines_H = split /\n/, render_H();
my @lines_E = split /\n/, render_E();
my @lines_L = split /\n/, render_L();
my @lines_O = split /\n/, render_O();

# 横に並べて表示
for my $i (0..4) {
    say join("  ",
        $lines_H[$i] // "",
        $lines_E[$i] // "",
        $lines_L[$i] // "",
        $lines_L[$i] // "",  # 2つ目のL
        $lines_O[$i] // ""
    );
}

say "";
say "完成!巨大な文字が表示できました。";

実行結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
=== ASCIIアート・フォントレンダラー v0.1 ===

「HELLO」を表示します:

 H   H   EEEEE   L        L         OOO
 H   H   E       L        L        O   O
 HHHHH   EEE     L        L        O   O
 H   H   E       L        L        O   O
 H   H   EEEEE   LLLLL    LLLLL     OOO

完成!巨大な文字が表示できました。

今回のポイント

  • ✅ とりあえず動くものができた
  • ⚠️ 文字数が増えると関数が爆発する
  • ⚠️ 拡張性がまったくない

第2章: 文字が増えるとメモリが爆発する

前章の振り返り

第1章では、ハードコードで「HELLO」を表示できました。でも、A〜Z全部に対応したら?

今回の目標

  • A〜Z全文字に対応する
  • オブジェクト指向で書き直す
  • でも…問題が発生する

動く:Mooでクラス化

 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
#!/usr/bin/env perl
use v5.34;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use warnings;

package Glyph {
    use Moo;

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

    sub render($self) {
        return $self->art;
    }
}

package main;

# フォントデータ(A-Zまで全部定義)
my %FONT_DATA = (
    A => " AAA\nA   A\nAAAAA\nA   A\nA   A",
    B => "BBBB\nB   B\nBBBB\nB   B\nBBBB",
    # ... 中略 ...
    H => "H   H\nH   H\nHHHHH\nH   H\nH   H",
    L => "L\nL\nL\nL\nLLLLL",
    O => " OOO\nO   O\nO   O\nO   O\n OOO",
    # ... 中略 ...
);

# 問題のあるコード: 文字を使うたびに新しいオブジェクトを生成
sub get_glyph($char) {
    my $art = $FONT_DATA{uc $char} // "  ?\n  ?\n  ?\n  ?\n  ?";
    return Glyph->new(name => $char, art => $art);
}

# テスト
my @text = split //, "LOLLOL";
my @glyphs = map { get_glyph($_) } @text;

say "文字数: " . scalar @text;
say "作成されたGlyphオブジェクト数: " . scalar @glyphs . " (全部別々!)";

破綻:同じ文字なのに別オブジェクト

「LOLLOL」という6文字を表示するだけなのに…

1
2
文字数: 6
作成されたGlyphオブジェクト数: 6 (全部別々!)

問題点を整理すると:

  • 「L」が3回登場 → 3つの別々のGlyphオブジェクト
  • 「O」が2回登場 → 2つの別々のGlyphオブジェクト
  • 100万文字のテキストを表示したら、100万個のオブジェクト!

今回のポイント

  • ⚠️ 同じデータなのに毎回新規作成はムダ
  • ⚠️ メモリ使用量が文字数に比例して増大
  • 同じ文字は1つのオブジェクトを共有すればよいのでは?

第3章: 文字グリフを共有しよう

Flyweight Pattern: Sharing Glyphs

前章の振り返り

同じ「L」なのに毎回新しいオブジェクトを作っていました。これを解決します。

今回の目標

  • 同じ文字は1つのオブジェクトを共有する
  • Flyweightパターンの導入

完成:Flyweightパターン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package Glyph;
use v5.34;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use warnings;
use Moo;

# Flyweightパターン: 共有される内部状態(intrinsic state)
has char => (is => 'ro', required => 1);
has art  => (is => 'ro', required => 1);

sub render($self) {
    return $self->art;
}

sub get_lines($self) {
    return split /\n/, $self->art;
}

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
package GlyphFactory;
use v5.34;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use warnings;
use Moo;
use Glyph;

# 既に作成したGlyphオブジェクトを保持するプール
has _pool => (
    is      => 'ro',
    default => sub { {} },
);

has _font_data => (
    is      => 'ro',
    default => sub {
        return {
            A => " AAA\nA   A\nAAAAA\nA   A\nA   A",
            # ... 省略 ...
        };
    },
);

# Glyphを取得(既に存在すれば再利用、なければ新規作成)
sub get_glyph($self, $char) {
    my $key = uc $char;

    # プールに既にあれば再利用
    if (exists $self->_pool->{$key}) {
        return $self->_pool->{$key};
    }

    # 新規作成してプールに保存
    my $art = $self->_font_data->{$key} // "  ?\n  ?\n  ?\n  ?\n  ?";
    my $glyph = Glyph->new(char => $key, art => $art);
    $self->_pool->{$key} = $glyph;

    return $glyph;
}

sub pool_size($self) {
    return scalar keys $self->_pool->%*;
}

1;

Flyweightの効果

1
2
3
4
5
6
my $factory = GlyphFactory->new;
my @text = split //, "LOLLOL";
my @glyphs = map { $factory->get_glyph($_) } @text;

say "文字数: " . scalar @text;                        # → 6
say "プールサイズ: " . $factory->pool_size . "種類";  # → 2(L と O だけ)
	classDiagram
    class GlyphFactory {
        -_pool: Hash
        -_font_data: Hash
        +get_glyph(char): Glyph
        +pool_size(): Int
    }

    class Glyph {
        +char: Str
        +art: Str
        +render(): Str
        +get_lines(): Array
    }

    GlyphFactory "1" --> "*" Glyph : creates/caches
    note for GlyphFactory "同じ文字は同じオブジェクトを返す"

今回のポイント

  • Flyweightパターン = オブジェクトの共有
  • ✅ 100万文字のテキストでも、使用する文字種類数分のメモリで済む
  • ✅ プールで既存オブジェクトを管理

第4章: フォントの設定が散らばる問題

前章の振り返り

Flyweightでメモリ問題を解決しました。でも、コードを見返すと別の問題が…

今回の目標

  • 設定がハードコードされている問題を認識
  • DRY原則(Don’t Repeat Yourself)違反を発見

破綻:設定が5箇所に散らばる

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
my $FONT_PATH = "/usr/share/fonts/ascii/standard.fnt";  # 設定1箇所目
my $DEFAULT_STYLE = "bold";                              # 設定2箇所目

sub initialize_renderer {
    my $font_path = "/usr/share/fonts/ascii/standard.fnt";  # 設定3箇所目(重複!)
    say "フォントを$font_pathから読み込んでいます...";
}

sub get_font_style {
    return "bold";  # 設定4箇所目(重複!)
}

sub render_with_style {
    my $style = "bold";  # 設定5箇所目(またもや重複!)
    # ...
}

問題点:

  • 同じ値が5箇所にハードコード
  • フォントパスを変更したい → 5箇所を修正
  • 修正漏れがあるとバグの原因に

今回のポイント

  • ⚠️ DRY原則(Don’t Repeat Yourself)に違反
  • ⚠️ 設定変更時に修正漏れリスク
  • 設定を1箇所で一元管理すればよいのでは?

第5章: 設定を一元管理しよう

Singleton Pattern: Centralized Configuration

前章の振り返り

設定が5箇所に散らばっていました。これを1箇所にまとめます。

今回の目標

  • 設定を一元管理するクラスを作る
  • Singletonパターンの導入

完成:Singletonパターン

 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
package FontManager;
use v5.34;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use warnings;
use Moo;

# クラス変数としてインスタンスを保持
my $_instance;

# 設定項目
has font_path => (
    is      => 'rw',
    default => '/usr/share/fonts/ascii/standard.fnt',
);

has default_style => (
    is      => 'rw',
    default => 'bold',
);

has char_spacing => (
    is      => 'rw',
    default => 1,
);

# Singletonアクセサ: インスタンスを取得
sub instance($class = __PACKAGE__) {
    $_instance //= $class->new;
    return $_instance;
}

# テスト用: インスタンスをリセット
sub reset_instance($class = __PACKAGE__) {
    $_instance = undef;
}

sub show_config($self) {
    say "=== FontManager 設定 ===";
    say "  フォントパス: " . $self->font_path;
    say "  デフォルトスタイル: " . $self->default_style;
}

1;

Singletonの使い方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use FontManager;

# どこからでも同じインスタンスにアクセス
my $manager = FontManager->instance;
say $manager->font_path;  # → /usr/share/fonts/ascii/standard.fnt

# 設定を変更
$manager->font_path('/home/user/fonts/fancy.fnt');

# 別の場所でも同じ設定が見える
sub another_function {
    my $m = FontManager->instance;  # 同じインスタンス
    say $m->font_path;  # → /home/user/fonts/fancy.fnt
}
	classDiagram
    class FontManager {
        -$_instance: FontManager
        +font_path: Str
        +default_style: Str
        +char_spacing: Int
        +instance(): FontManager
        +reset_instance(): void
        +show_config(): void
    }

    note for FontManager "アプリ全体で唯一のインスタンス"

今回のポイント

  • Singletonパターン = 唯一のインスタンスを保証
  • FontManager->instance で常に同じオブジェクト
  • ✅ 設定変更は1箇所でOK、どこからでも反映

第6章: 使わない文字もロードしてしまう

前章の振り返り

Singletonで設定を一元管理できました。でも、起動が遅いことに気づきました…

今回の目標

  • 全フォントを先読みする問題を認識
  • 起動時間の無駄を発見

破綻:起動時に全部ロード

 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
package EagerFontLoader {
    use Moo;

    has fonts => (is => 'ro', default => sub { {} });

    # 起動時にすべてのフォントをロード(問題のある設計)
    sub BUILD($self, $args) {
        say "フォントを読み込み中...";
        for my $char ('A'..'Z', '0'..'9') {
            my $font = HeavyFont->new(
                name => $char,
                data => "DUMMY_DATA_FOR_$char" x 100,
            );
            $self->fonts->{$char} = $font;
            print ".";
        }
        say " 完了!";
    }
}

# 起動時間を計測
my $start = time();
my $loader = EagerFontLoader->new;  # ← ここで全部ロード
my $elapsed = time() - $start;

say "起動時間: ${elapsed}秒";  # → 0.36秒(36文字 × 10ms)
say "ロードしたフォント数: 36種類";
say "実際に使うのは: HELLO の 4種類だけ...";

問題点:

  • 「HELLO」を表示するだけなのに36種類全部ロード
  • 起動が遅い
  • 使わない文字のデータまでメモリに保持

今回のポイント

  • ⚠️ Eager Loading(先読み)の弊害
  • ⚠️ 使わないデータまでロードして無駄
  • 必要なときに必要な分だけロードすればよいのでは?

第7章: 使う文字だけロードしよう

Proxy Pattern: Lazy Loading

前章の振り返り

起動時に全フォントをロードしていました。これを効率化します。

今回の目標

  • 使う文字だけオンデマンドでロード
  • Proxyパターンの導入

完成:Proxyパターン

 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
package RealFont;
use v5.34;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use warnings;
use Moo;

has char => (is => 'ro', required => 1);
has _data => (is => 'rw');
has _loaded => (is => 'rw', default => 0);

# 遅延ロード: 実際に必要になったときにロード
sub _load($self) {
    return if $self->_loaded;

    # 重いロード処理をシミュレート
    select(undef, undef, undef, 0.01);  # 10ms

    my $data = $FONT_DATA{$self->char} // "?";
    $self->_data($data);
    $self->_loaded(1);
}

sub get_art($self) {
    $self->_load;
    return $self->_data;
}

sub is_loaded($self) {
    return $self->_loaded;
}

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
package FontProxy;
use v5.34;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use warnings;
use Moo;
use RealFont;

has char => (is => 'ro', required => 1);
has _real_font => (is => 'rw');

# RealFontへの参照を遅延生成
sub _get_real_font($self) {
    unless ($self->_real_font) {
        $self->_real_font(RealFont->new(char => $self->char));
    }
    return $self->_real_font;
}

# RealFontの操作を委譲
sub get_art($self) {
    return $self->_get_real_font->get_art;
}

sub is_loaded($self) {
    return $self->_real_font && $self->_real_font->is_loaded;
}

1;

Proxyの効果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 起動時間を計測
my $start = time();
my $loader = LazyFontLoader->new;  # Proxyだけ作成(0.0001秒)
my $elapsed = time() - $start;

say "起動時間: ${elapsed}秒";  # → ほぼ瞬時!
say "ロード済みフォント数: 0";  # まだ何もロードしていない

# HELLOを表示(この時点で H, E, L, O だけロード)
say render("HELLO");

say "ロード済みフォント数: 4";  # 使った4種類だけ
	classDiagram
    class FontProxy {
        +char: Str
        -_real_font: RealFont
        +get_art(): Str
        +is_loaded(): Bool
    }

    class RealFont {
        +char: Str
        -_data: Str
        -_loaded: Bool
        +get_art(): Str
        +is_loaded(): Bool
    }

    FontProxy "1" --> "0..1" RealFont : lazy creates
    note for FontProxy "軽量なプロキシ\nアクセス時にRealFontを生成"
    note for RealFont "重いフォントデータ\n必要になるまで生成しない"

今回のポイント

  • Proxyパターン = 代理オブジェクトで遅延ロード
  • ✅ 起動は瞬時(Proxyだけ作成)
  • ✅ 使う文字だけオンデマンドでロード
  • Virtual Proxy(仮想プロキシ)とも呼ばれる

第8章: 3つのパターンで完成!

全体の振り返り

ここまでの成長を振り返りましょう。

問題解決策パターン
1-2同じ文字でも毎回新規作成オブジェクトを共有Flyweight
3-4設定が複数箇所に散らばる一元管理クラスSingleton
5-6使わない文字も先読み遅延ロードProxy

3パターンの連携

	graph TD
    subgraph Singleton
        FM[FontManager] --> C[設定: パス, スタイル]
    end

    subgraph Flyweight
        FF[GlyphFactory] --> G1[Glyph: A]
        FF --> G2[Glyph: B]
        FF --> G3[Glyph: ...]
    end

    subgraph Proxy
        P[FontProxy] --> RF[RealFont: 重いデータ]
    end

    FM -.->|設定を参照| FF
    FF -.->|データが必要なとき| P

最終版コードのハイライト

 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
package ASCIIArtRenderer {
    use Moo;
    use FontManager;
    use GlyphFactory;
    use FontProxy;

    has glyph_factory => (
        is      => 'ro',
        default => sub { GlyphFactory->new },
    );

    sub render($self, $text) {
        my $manager = FontManager->instance;  # Singleton
        my $spacing = $manager->char_spacing;

        # Flyweightから共有Glyphを取得
        my @glyphs = map { $self->glyph_factory->get_glyph($_) } split //, $text;

        # 各行を横に結合して表示
        my @result_lines = ("") x 5;
        for my $glyph (@glyphs) {
            my @art_lines = $glyph->get_lines;
            for my $i (0..4) {
                $result_lines[$i] .= sprintf("%-" . (5 + $spacing) . "s", $art_lines[$i] // "");
            }
        }
        return join("\n", @result_lines);
    }
}

実行結果

 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
============================================================
 ASCIIアート・フォントレンダラー v1.0 (完成版)
 Singleton × Flyweight × Proxy
============================================================

初期化時間: 0.0001秒(瞬時!)

【 HELLO WORLD を表示 】

 H   H  EEEEE  L      L       OOO
 H   H  E      L      L      O   O
 HHHHH  EEE    L      L      O   O
 H   H  E      L      L      O   O
 H   H  EEEEE  LLLLL  LLLLL   OOO

 W   W   OOO   RRRR   L      DDD
 W   W  O   O  R   R  L      D  D
 W W W  O   O  RRRR   L      D  D
 WW WW  O   O  R R    L      D  D
 W   W   OOO   R  R   LLLLL  DDD

=== レンダラー統計情報 ===
Glyphプールサイズ: 8種類
プール内容: D, E, H, L, O, R, W
遅延ロード済み: 8種類

まとめ

学んだ3つのパターン

パターン一言で使うタイミング
Singleton唯一のインスタンスを保証設定、ロガー、接続プール
Flyweightオブジェクトを共有してメモリ節約大量の同種オブジェクト
Proxy代理で遅延ロード・アクセス制御重いリソース、リモートアクセス

3パターンが協力する場面

この3パターンは「リソース効率化」という共通テーマで連携しやすい組み合わせです:

  • Singleton → グローバルな設定・状態管理
  • Flyweight → 共有可能なオブジェクトの一元管理
  • Proxy → 重いオブジェクトへのアクセス制御

次のステップ

  • 実際の開発で「同じデータを何度も生成している」と思ったら → Flyweight
  • 「設定が散らばっている」と思ったら → Singleton
  • 「起動が遅い・先読みが無駄」と思ったら → Proxy

パターンの名前を覚えるより、**「この問題にはこの解決策」**という感覚を身につけることが大切です。


動作環境・コード

本記事のコードは以下の環境で動作確認しています:

  • Perl v5.34以降(signatures機能を使用)
  • cpanm経由で Moo をインストール

完全なコードはGitHubリポジトリで公開しています。

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