Featured image of post 共通の送信フローを集約しよう

共通の送信フローを集約しよう

送信処理やログ出力など、全シナリオで共通する処理を基底クラスに集約します。これにより、コードの重複を減らし、一貫性のある動作を保証できます。

@nqounetです。

前回は、生成処理のオーバーライドを実装しました。今回は、送信処理やログ出力など、全シナリオで共通する処理を基底クラスに集約します。

このシリーズについて

シリーズ全体の目次は以下をご覧ください。

前回の振り返り

前回までに、各シナリオがcreate_responseをオーバーライドして専用のレスポンスを生成するようになりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package SuccessScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return SuccessResponse->new(
            data => { id => 1, name => 'サンプルアイテム' },
        );
    }
}

今回のゴール

共通処理(ログ出力、タイムスタンプ追加など)を基底クラスに集約し、サブクラスでは「レスポンス生成」だけに集中できるようにします。

	flowchart TB
    subgraph "Scenario基底クラス(共通処理)"
        A["execute()"] --> B["開始時刻を記録"]
        B --> C["log_request()"]
        C --> D["create_response()"]
        D --> E["処理時間を計算"]
        E --> F["log_complete()"]
        F --> G["response.render()"]
    end
    
    subgraph "サブクラス(個別処理)"
        D -.->|オーバーライド| H["SuccessScenario"]
        D -.->|オーバーライド| I["NotFoundScenario"]
    end
    
    style D fill:#f9f,stroke:#333

現状の問題

現在のexecuteメソッドは非常にシンプルです。

1
2
3
4
sub execute($self) {
    my $response = $self->create_response;
    return $response->render;
}

しかし実際のAPIシミュレーターでは、以下のような処理も必要になるでしょう:

  • リクエストのログ出力
  • 処理時間の記録
  • タイムスタンプの追加

これらの処理を各サブクラスに書くと、コードが重複してしまいます。

共通処理を基底クラスに追加する

Scenario基底クラスに共通処理を追加しましょう。

 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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, JSON, Time::HiRes(cpanmでインストール)

use v5.36;

package ResponseRole {
    use Moo::Role;
    requires 'render';
}

package SuccessResponse {
    use Moo;
    use JSON qw(encode_json);
    with 'ResponseRole';

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

    sub render($self) {
        my $body = encode_json({
            success => JSON::true,
            message => 'リクエストが正常に処理されました',
            data    => $self->data,
        });
        return "HTTP/1.1 200 OK\nContent-Type: application/json\n\n$body";
    }
}

package ErrorResponse {
    use Moo;
    use JSON qw(encode_json);
    with 'ResponseRole';

    has status     => (is => 'ro', required => 1);
    has error_code => (is => 'ro', required => 1);
    has message    => (is => 'ro', required => 1);

    sub render($self) {
        my $body = encode_json({
            success => JSON::false,
            error   => $self->message,
            code    => $self->error_code,
        });
        return sprintf(
            "HTTP/1.1 %s\nContent-Type: application/json\n\n%s",
            $self->status, $body,
        );
    }
}

package Scenario {
    use Moo;
    use Time::HiRes qw(gettimeofday tv_interval);

    sub create_response($self) {
        die "create_response must be implemented by subclass";
    }

    sub scenario_name($self) {
        my $class = ref($self);
        $class =~ s/Scenario$//;
        return $class;
    }

    sub log_request($self) {
        my $name = $self->scenario_name;
        my $timestamp = localtime();
        say STDERR "[$timestamp] Processing: $name";
    }

    sub log_complete($self, $elapsed) {
        my $name = $self->scenario_name;
        my $timestamp = localtime();
        say STDERR "[$timestamp] Completed: $name (${elapsed}ms)";
    }

    sub execute($self) {
        my $start = [gettimeofday];
        
        # リクエストログ
        $self->log_request;
        
        # レスポンス生成
        my $response = $self->create_response;
        
        # 処理時間計算
        my $elapsed = int(tv_interval($start) * 1000);
        
        # 完了ログ
        $self->log_complete($elapsed);
        
        return $response->render;
    }
}

executeメソッドは以下の流れになりました:

  1. 開始時刻を記録
  2. リクエストログを出力
  3. レスポンスを生成(サブクラスのcreate_responseを呼ぶ)
  4. 処理時間を計算
  5. 完了ログを出力
  6. レスポンスを返す

サブクラスはシンプルなまま

共通処理は基底クラスにあるので、サブクラスはレスポンス生成だけに集中できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package SuccessScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return SuccessResponse->new(
            data => { id => 1, name => 'サンプルアイテム' },
        );
    }
}

package NotFoundScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return ErrorResponse->new(
            status     => '404 Not Found',
            error_code => 'NOT_FOUND',
            message    => 'リソースが見つかりません',
        );
    }
}

サブクラスは何も変わっていません。共通処理を意識する必要がありません。

実行ログの例

1
2
3
4
5
6
[Sat Jan 17 13:23:20 2026] Processing: Success
[Sat Jan 17 13:23:20 2026] Completed: Success (0ms)
HTTP/1.1 200 OK
Content-Type: application/json

{"data":{"id":1,"name":"サンプルアイテム"},"message":"リクエストが正常に処理されました","success":true}

ログが自動的に出力されるようになりました。

完成コード

今回の完成コードを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
 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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, JSON, Time::HiRes(cpanmでインストール)

use v5.36;

package ResponseRole {
    use Moo::Role;
    requires 'render';
}

package SuccessResponse {
    use Moo;
    use JSON qw(encode_json);
    with 'ResponseRole';

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

    sub render($self) {
        my $body = encode_json({
            success => JSON::true,
            message => 'リクエストが正常に処理されました',
            data    => $self->data,
        });
        return "HTTP/1.1 200 OK\nContent-Type: application/json\n\n$body";
    }
}

package ErrorResponse {
    use Moo;
    use JSON qw(encode_json);
    with 'ResponseRole';

    has status     => (is => 'ro', required => 1);
    has error_code => (is => 'ro', required => 1);
    has message    => (is => 'ro', required => 1);

    sub render($self) {
        my $body = encode_json({
            success => JSON::false,
            error   => $self->message,
            code    => $self->error_code,
        });
        return sprintf(
            "HTTP/1.1 %s\nContent-Type: %s\n\n%s",
            $self->status,
            'application/json',
            $body,
        );
    }
}

package Scenario {
    use Moo;
    use Time::HiRes qw(gettimeofday tv_interval);

    sub create_response($self) {
        die "create_response must be implemented by subclass";
    }

    sub scenario_name($self) {
        my $class = ref($self);
        $class =~ s/Scenario$//;
        return $class;
    }

    sub log_request($self) {
        my $name = $self->scenario_name;
        my $timestamp = localtime();
        say STDERR "[$timestamp] Processing: $name";
    }

    sub log_complete($self, $elapsed) {
        my $name = $self->scenario_name;
        my $timestamp = localtime();
        say STDERR "[$timestamp] Completed: $name (${elapsed}ms)";
    }

    sub execute($self) {
        my $start = [gettimeofday];
        $self->log_request;
        my $response = $self->create_response;
        my $elapsed = int(tv_interval($start) * 1000);
        $self->log_complete($elapsed);
        return $response->render;
    }
}

package SuccessScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return SuccessResponse->new(
            data => { id => 1, name => 'サンプルアイテム' },
        );
    }
}

package NotFoundScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return ErrorResponse->new(
            status     => '404 Not Found',
            error_code => 'NOT_FOUND',
            message    => 'リソースが見つかりません',
        );
    }
}

for my $scenario_class (qw(SuccessScenario NotFoundScenario)) {
    say "=== $scenario_class ===";
    my $scenario = $scenario_class->new;
    say $scenario->execute;
    say "";
}

まとめ

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

  • ログ出力、処理時間計測をScenario基底クラスに追加
  • サブクラスはcreate_responseの実装だけに集中
  • 共通処理の変更が1箇所で済むようになった

次回は、新しいシナリオ「レート制限」を追加して、既存コードを修正せずに拡張できることを体験します。

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