Featured image of post 第7回-レポートの型を保証しよう - PerlとMooでレポートジェネレーターを作ってみよう

第7回-レポートの型を保証しよう - PerlとMooでレポートジェネレーターを作ってみよう

create_reportの戻り値が正しいReportRoleを持つことを保証しましょう。Mooのisaで型安全性を高め、バグを未然に防ぎます。

@nqounetです。

前回の振り返り

前回は、基底クラスに共通処理を集約しました。

  • generate_and_saveメソッドを基底クラスに追加した
  • 「生成→表示→保存」の流れを統一した
  • サブクラスはcreate_reportだけに集中できるようになった

今回の目標

今回は、create_reportの戻り値が正しいレポートオブジェクトであることを保証します。

具体的には、以下のことを行います。

  • create_reportの戻り値を検証する
  • doesを使ってReportRoleを持つことをチェックする
  • 型エラーが発生した場合のデモを行う

ストーリー設定

新しいメンバーがチームに加わりました。

彼が新しいレポートジェネレーターを作ったのですが、create_reportが間違ったオブジェクトを返してしまい、バグが発生しました。

create_reportは必ずReportRoleを持つオブジェクトを返す」というルールを強制できないでしょうか?

実装

コード例1: create_reportでの戻り値検証

基底クラスに、create_reportの戻り値を検証する処理を追加します。

  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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env perl
use v5.36;

# ========================================
# ReportRole ロール
# ========================================
package ReportRole {
    use Moo::Role;

    requires 'generate';
    requires 'get_period';
}

# ========================================
# MonthlyReport クラス
# ========================================
package MonthlyReport {
    use Moo;
    with 'ReportRole';

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

    sub generate ($self) {
        my @lines = (
            "=== " . $self->title . " ===",
            "期間: " . $self->get_period(),
            "月次レポートを生成しました。",
        );
        return join("\n", @lines);
    }

    sub get_period ($self) {
        return '月次';
    }
}

# ========================================
# WeeklyReport クラス
# ========================================
package WeeklyReport {
    use Moo;
    with 'ReportRole';

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

    sub generate ($self) {
        my @lines = (
            "=== " . $self->title . " ===",
            "期間: " . $self->get_period(),
            "週次レポートを生成しました。",
        );
        return join("\n", @lines);
    }

    sub get_period ($self) {
        return '週次';
    }
}

# ========================================
# DailyReport クラス
# ========================================
package DailyReport {
    use Moo;
    with 'ReportRole';

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

    sub generate ($self) {
        my @lines = (
            "=== " . $self->title . " ===",
            "期間: " . $self->get_period(),
            "日次レポートを生成しました。",
        );
        return join("\n", @lines);
    }

    sub get_period ($self) {
        return '日次';
    }
}

# ========================================
# ReportGenerator 基底クラス
# ========================================
package ReportGenerator {
    use Moo;
    use Scalar::Util qw(blessed);

    # サブクラスでオーバーライドするメソッド
    sub create_report ($self, $title) {
        die "create_report() must be implemented by subclass";
    }

    # 戻り値を検証するラッパーメソッド
    sub create_validated_report ($self, $title) {
        my $report = $self->create_report($title);

        # 型チェック: ReportRoleを持っているか確認
        unless (blessed($report) && $report->does('ReportRole')) {
            die "create_report() must return an object that does ReportRole";
        }

        return $report;
    }

    # 共通の処理: レポートを生成して表示
    sub generate_and_print ($self, $title) {
        my $report = $self->create_validated_report($title);
        my $content = $report->generate();
        say $content;
        return $report;
    }

    # 共通の処理: レポートを生成して保存
    sub generate_and_save ($self, $title, $filename) {
        my $report = $self->create_validated_report($title);
        my $content = $report->generate();

        say $content;
        say "";
        say "[保存] $filename に保存しました。";

        return $report;
    }
}

# ========================================
# MonthlyReportGenerator クラス
# ========================================
package MonthlyReportGenerator {
    use Moo;
    extends 'ReportGenerator';

    sub create_report ($self, $title) {
        return MonthlyReport->new(title => $title);
    }
}

# ========================================
# WeeklyReportGenerator クラス
# ========================================
package WeeklyReportGenerator {
    use Moo;
    extends 'ReportGenerator';

    sub create_report ($self, $title) {
        return WeeklyReport->new(title => $title);
    }
}

# ========================================
# DailyReportGenerator クラス
# ========================================
package DailyReportGenerator {
    use Moo;
    extends 'ReportGenerator';

    sub create_report ($self, $title) {
        return DailyReport->new(title => $title);
    }
}

# ========================================
# メイン処理
# ========================================
package main;

say "=== 正常なケース ===";
my $monthly = MonthlyReportGenerator->new();
$monthly->generate_and_print("2026年1月 売上レポート");

ポイントは、create_validated_reportメソッドです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sub create_validated_report ($self, $title) {
    my $report = $self->create_report($title);

    # 型チェック: ReportRoleを持っているか確認
    unless (blessed($report) && $report->does('ReportRole')) {
        die "create_report() must return an object that does ReportRole";
    }

    return $report;
}

doesメソッドを使って、オブジェクトがReportRoleを持っているかをチェックしています。

型チェックについて詳しくは、前提知識のシリーズなどをご覧ください。

コード例2: 型エラーのデモ

もしcreate_reportが間違ったオブジェクトを返したらどうなるでしょうか?

 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
# ========================================
# 間違った実装のジェネレーター
# ========================================
package BrokenGenerator {
    use Moo;
    extends 'ReportGenerator';

    # ReportRoleを持たないオブジェクトを返してしまう
    sub create_report ($self, $title) {
        # 単なるハッシュリファレンスを返す(間違い!)
        return { title => $title };
    }
}

# ========================================
# エラーのデモ
# ========================================
package main;

say "";
say "=== 型エラーのデモ ===";

my $broken = BrokenGenerator->new();

eval {
    $broken->generate_and_print("テストレポート");
};

if ($@) {
    say "エラーが発生しました: $@";
}

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

1
2
3
4
5
6
7
=== 正常なケース ===
=== 2026年1月 売上レポート ===
期間: 月次
月次レポートを生成しました。

=== 型エラーのデモ ===
エラーが発生しました: create_report() must return an object that does ReportRole at ...

create_reportReportRoleを持たないオブジェクト(この場合はハッシュリファレンス)を返したため、エラーが発生しました。

型チェックのメリット

	flowchart TD
    A[create_report 呼び出し] --> B[report オブジェクト取得]
    B --> C{blessed?}
    C -->|No| E[エラー: オブジェクトではない]
    C -->|Yes| D{does ReportRole?}
    D -->|No| F[エラー: ReportRole を持たない]
    D -->|Yes| G[検証成功: report を返す]

    style E fill:#ffcccc
    style F fill:#ffcccc
    style G fill:#ccffcc

この型チェックには以下のメリットがあります。

  1. バグの早期発見

実行時にすぐにエラーが発生するため、問題を早期に発見できます。

  1. 契約の強制

create_reportは必ずReportRoleを持つオブジェクトを返す」という契約が強制されます。

  1. ドキュメントの役割

コードを読むだけで「何を返すべきか」がわかります。

今回のまとめ

今回は、create_reportの戻り値を検証する仕組みを追加しました。

  • create_validated_reportメソッドで型チェックを行う
  • doesを使ってReportRoleを持つことを確認する
  • 型エラーが発生した場合は例外をスローする

これにより、新しいジェネレーターを作成する際に、間違った実装をしてしまうリスクを減らせます。

次回予告

次回は「新しいレポート種別を追加しよう」として、四半期レポートを追加します。

既存のコードを一切修正せずに、新しいクラスを追加するだけで機能を拡張できることを体験しましょう。

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