Featured image of post 第3回-レポートの共通ルールを決めよう - PerlとMooでレポートジェネレーターを作ってみよう

第3回-レポートの共通ルールを決めよう - PerlとMooでレポートジェネレーターを作ってみよう

レポートに共通のルールを定義しましょう。Moo::Roleのrequiresを使って、すべてのレポートが持つべきメソッドを強制する方法を学びます。

@nqounetです。

前回の振り返り

前回は、週次レポートを追加するためにif/elseで種別を切り替える方法を試しました。

しかし、この方法には問題がありました。

  • if/elseが肥大化する
  • 新しい種別を追加するたびに既存コードを修正する必要がある
  • 開放閉鎖原則に違反している

今回の目標

今回は、レポートクラスに共通のルールを定義します。

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

  • ReportRoleというロールを作成する
  • すべてのレポートクラスがgenerateメソッドを持つことを強制する
  • 各レポートクラスにwithでロールを適用する

これにより、新しいレポートクラスを作成するときに「何を実装すべきか」が明確になります。

	classDiagram
    class ReportRole {
        <<Role>>
        +generate()*
        +get_period()*
    }
    class MonthlyReport {
        +title
        +generate()
        +get_period()
    }
    class WeeklyReport {
        +title
        +generate()
        +get_period()
    }
    ReportRole <|.. MonthlyReport : with
    ReportRole <|.. WeeklyReport : with

この図は、ReportRoleがインターフェースとして機能し、各レポートクラスがそのロールを適用(with)している関係を示しています。

ストーリー設定

上司から「レポートのフォーマットを統一したい」と言われました。

現状、MonthlyReportWeeklyReportは似たようなメソッドを持っていますが、明確なルールがありません。

将来、別の人が新しいレポートクラスを作るときに「何を実装すればいいか」がわかるように、共通のルールを定義しましょう。

実装

コード例1: ReportRoleの作成

まず、すべてのレポートが持つべきメソッドを定義したロールを作成します。

  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
#!/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) {
        say "=== " . $self->title . " ===";
        say "期間: " . $self->get_period();
        say "月次レポートを生成しました。";
    }

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

# ========================================
# WeeklyReport クラス
# ========================================
package WeeklyReport {
    use Moo;
    with 'ReportRole';  # ロールを適用

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

    sub generate ($self) {
        say "=== " . $self->title . " ===";
        say "期間: " . $self->get_period();
        say "週次レポートを生成しました。";
    }

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

# ========================================
# ReportGenerator クラス
# ========================================
package ReportGenerator {
    use Moo;

    sub create_report ($self, $type, $title) {
        if ($type eq 'monthly') {
            return MonthlyReport->new(title => $title);
        }
        elsif ($type eq 'weekly') {
            return WeeklyReport->new(title => $title);
        }
        else {
            die "Unknown report type: $type";
        }
    }

    sub generate_and_print ($self, $type, $title) {
        my $report = $self->create_report($type, $title);
        $report->generate();
        return $report;
    }
}

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

my $generator = ReportGenerator->new();

say "--- 月次レポート ---";
$generator->generate_and_print('monthly', "2026年1月 売上レポート");

say "";

say "--- 週次レポート ---";
$generator->generate_and_print('weekly', "2026年1月第1週 売上レポート");

ReportRoleではrequiresを使って、このロールを適用するクラスが必ず実装すべきメソッドを宣言しています。

  • generate: レポートを生成するメソッド
  • get_period: レポートの期間を返すメソッド

Moo::Roleについて詳しくは、以下の記事をご覧ください。

コード例2: ロールを実装しないとエラーになる

ReportRoleを適用したクラスが必要なメソッドを実装していない場合、エラーになります。

試しに、get_periodを実装しないクラスを作ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package BrokenReport {
    use Moo;
    with 'ReportRole';  # ロールを適用

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

    sub generate ($self) {
        say "壊れたレポート";
    }

    # get_period を実装していない!
}

このコードを実行すると、以下のようなエラーが発生します。

1
Can't apply ReportRole to BrokenReport - missing get_period at ...

requiresで宣言したメソッドを実装していないため、ロールの適用に失敗しています。

これにより、新しいレポートクラスを作成する人が「何を実装すべきか」を忘れることを防げます。

Moo::Roleの利点

ロールを使うことで、以下の利点があります。

  • 契約の明示: どのメソッドが必要かが明確になる
  • コンパイル時チェック: 実装漏れを早期に発見できる
  • ドキュメントの役割: コードを読むだけでインターフェースがわかる

これは「インターフェース」や「抽象クラス」に似た概念です。JavaやTypeScriptを知っている人にはおなじみかもしれません。

今回のまとめ

今回は、レポートクラスに共通のルールを定義しました。

  • ReportRoleを作成し、requiresで必須メソッドを宣言した
  • MonthlyReportWeeklyReportwithでロールを適用した
  • ロールを実装しないとエラーになることを確認した

これで「レポートとして必要なメソッド」が明確になりました。

ただし、まだif/elseの問題は解決していません。次回以降で、この問題に取り組んでいきます。

次回予告

次回は「ジェネレーターを種別ごとに分けよう」として、継承を使ってif/elseの問題を解決します。

extendsを使ってMonthlyReportGeneratorWeeklyReportGeneratorを作成し、コードを整理していきましょう。

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