Featured image of post 第4回-ジェネレーターを種別ごとに分けよう - PerlとMooでレポートジェネレーターを作ってみよう

第4回-ジェネレーターを種別ごとに分けよう - PerlとMooでレポートジェネレーターを作ってみよう

if/elseの肥大化問題を継承で解決!Mooのextendsを使って、MonthlyReportGeneratorとWeeklyReportGeneratorを作成し、コードを整理します。

@nqounetです。

前回の振り返り

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

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

しかし、まだif/elseの問題は解決していませんでした。

今回の目標

今回は、継承を使ってジェネレーターを種別ごとに分けます。

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

  • ReportGeneratorを基底クラスとして整理する
  • MonthlyReportGeneratorクラスを作成する
  • WeeklyReportGeneratorクラスを作成する

これにより、if/elseを使わずにレポートの種類を切り替えられるようになります。

	classDiagram
    class ReportGenerator {
        +create_report(title)*
        +generate_and_print(title)
    }
    class MonthlyReportGenerator {
        +create_report(title)
    }
    class WeeklyReportGenerator {
        +create_report(title)
    }
    ReportGenerator <|-- MonthlyReportGenerator : extends
    ReportGenerator <|-- WeeklyReportGenerator : extends
    MonthlyReportGenerator ..> MonthlyReport : creates
    WeeklyReportGenerator ..> WeeklyReport : creates

この図は、ReportGenerator基底クラスを継承(extends)した2つのサブクラスが、それぞれ対応するレポートを生成する構造を示しています。

ストーリー設定

if/elseが増えてきて、コードが読みにくくなってきました。

「月次レポートの生成処理」と「週次レポートの生成処理」を分離して、それぞれ専用のジェネレーターを作ることにしましょう。

実装

コード例1: ReportGenerator基底クラスの整理

まず、ReportGeneratorを基底クラスとして整理します。

  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
#!/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, $title) {
        die "create_report() must be implemented by subclass";
    }

    # 共通の処理
    sub generate_and_print ($self, $title) {
        my $report = $self->create_report($title);
        $report->generate();
        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);
    }
}

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

say "--- 月次レポート ---";
my $monthly_generator = MonthlyReportGenerator->new();
$monthly_generator->generate_and_print("2026年1月 売上レポート");

say "";

say "--- 週次レポート ---";
my $weekly_generator = WeeklyReportGenerator->new();
$weekly_generator->generate_and_print("2026年1月第1週 売上レポート");

大きな変更点は以下の通りです。

  • ReportGeneratorは基底クラスになり、create_reportメソッドは「サブクラスで実装すべき」というエラーを出す
  • MonthlyReportGeneratorextendsReportGeneratorを継承し、create_reportをオーバーライド
  • WeeklyReportGeneratorも同様に継承とオーバーライド

継承について詳しくは、前提知識のシリーズなどをご覧ください。

コード例2: 実行結果の確認

実行結果は前回と同じです。

1
2
3
4
5
6
7
8
9
--- 月次レポート ---
=== 2026年1月 売上レポート ===
期間: 月次
月次レポートを生成しました。

--- 週次レポート ---
=== 2026年1月第1週 売上レポート ===
期間: 週次
週次レポートを生成しました。

重要なポイントは、if/elseがなくなったことです。

レポートの種類に応じて、適切なジェネレーターを使うだけです。

1
2
3
4
5
# 月次レポートが欲しいとき
my $generator = MonthlyReportGenerator->new();

# 週次レポートが欲しいとき
my $generator = WeeklyReportGenerator->new();

継承による設計のメリット

この設計には以下のメリットがあります。

  • if/elseが不要: 種別に応じたジェネレーターを選ぶだけ
  • 責任の分離: 各ジェネレーターは自分が担当するレポートだけを知っている
  • 拡張が容易: 新しいレポート種別を追加するときは、新しいジェネレーターを作るだけ

今回のまとめ

今回は、継承を使ってジェネレーターを種別ごとに分けました。

  • ReportGeneratorを基底クラスとして整理した
  • MonthlyReportGeneratorを作成し、create_reportを実装した
  • WeeklyReportGeneratorを作成し、create_reportを実装した
  • if/elseが不要になった

これで、if/elseの肥大化問題は解決しました。

しかし、まだcreate_reportメソッドの実装方法に改善の余地があります。次回は、このメソッドのオーバーライドについて詳しく見ていきます。

次回予告

次回は「生成処理をオーバーライドしよう」として、各ジェネレーターでcreate_reportをオーバーライドする仕組みを詳しく解説します。

さらにDailyReportGeneratorも追加して、拡張性を確認しましょう。

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