Featured image of post 第2回-週次レポートも生成したい! - PerlとMooでレポートジェネレーターを作ってみよう

第2回-週次レポートも生成したい! - PerlとMooでレポートジェネレーターを作ってみよう

週次レポートも作りたい!でもif/elseで条件分岐すると、コードが肥大化して管理が大変に。この問題をどう解決するか、一緒に考えてみましょう。

@nqounetです。

前回の振り返り

前回は、月次レポートを生成する基本的な仕組みを作りました。

  • MonthlyReportクラス: レポートの内容を表すクラス
  • ReportGeneratorクラス: レポートを生成するクラス
  • create_reportメソッド: 具体的なレポートオブジェクトを生成する

今回の目標

今回は「週次レポートも生成したい」という新しい要望に対応します。

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

  • WeeklyReportクラスを新しく作成する
  • create_reportメソッドに引数を追加し、レポートの種類を切り替える
  • if/elseで生成するレポートを切り替える

そして、この方法の問題点を確認します。

ストーリー設定

上司から「週次レポートも欲しい」と依頼されました。

月次レポートだけでなく、週ごとの売上推移も確認したいとのことです。

「簡単そうだな」と思いながら、コードを修正してみましょう。

実装

コード例1: if/elseでレポート種別を切り替えるコード

まず、WeeklyReportクラスを追加し、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
#!/usr/bin/env perl
use v5.36;

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

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

    has period => (
        is      => 'ro',
        default => sub { '月次' },
    );

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

# ========================================
# WeeklyReport クラス(新規追加)
# ========================================
package WeeklyReport {
    use Moo;

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

    has period => (
        is      => 'ro',
        default => sub { '週次' },
    );

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

# ========================================
# 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週 売上レポート");

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

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

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

無事に月次レポートと週次レポートの両方を生成できました!

コード例2: 問題点の確認

一見うまくいっているように見えますが、問題があります。

	flowchart TD
    A[create_report呼び出し] --> B{type の値は?}
    B -->|monthly| C[MonthlyReport を生成]
    B -->|weekly| D[WeeklyReport を生成]
    B -->|daily| E[DailyReport を生成]
    B -->|quarterly| F[QuarterlyReport を生成]
    B -->|yearly| G[YearlyReport を生成]
    B -->|その他| H[エラー: Unknown type]

    style B fill:#ffcccc
    style E fill:#ffffcc
    style F fill:#ffffcc
    style G fill:#ffffcc

この図のように、種別が増えるたびに分岐が増えていきます。

例えば、さらに「日次レポート」「四半期レポート」「年次レポート」が必要になったらどうなるでしょうか?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
sub create_report ($self, $type, $title) {
    if ($type eq 'monthly') {
        return MonthlyReport->new(title => $title);
    }
    elsif ($type eq 'weekly') {
        return WeeklyReport->new(title => $title);
    }
    elsif ($type eq 'daily') {
        return DailyReport->new(title => $title);
    }
    elsif ($type eq 'quarterly') {
        return QuarterlyReport->new(title => $title);
    }
    elsif ($type eq 'yearly') {
        return YearlyReport->new(title => $title);
    }
    else {
        die "Unknown report type: $type";
    }
}

問題点をまとめると

  • if/elseがどんどん長くなる
  • 新しいレポート種別を追加するたびにReportGeneratorを修正する必要がある
  • レポートの種類が増えると、コードが読みにくくなる
  • 修正箇所が増えると、バグが入り込む可能性が高まる

これは「開放閉鎖原則」(Open-Closed Principle)に違反しています。

開放閉鎖原則: ソフトウェアの構成要素は「拡張に対しては開いていて、修正に対しては閉じている」べき

つまり、新しい機能を追加するときに、既存のコードを修正するのではなく、新しいコードを追加するだけで済むのが理想です。

今回のまとめ

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

  • WeeklyReportクラスを新規作成した
  • create_reportメソッドにtype引数を追加した
  • if/elseでレポートの種類を切り替えた

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

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

次回以降で、この問題を段階的に解決していきます。

次回予告

次回は「レポートの共通ルールを決めよう」として、レポートクラス間で共通のルールを定義します。

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

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