Featured image of post レート制限シナリオを追加しよう

レート制限シナリオを追加しよう

新しいシナリオ「レート制限」を追加します。既存のコードを一切修正せず、新しいクラスを追加するだけで機能拡張できることを体験しましょう。これがオープン・クローズドの原則です。

@nqounetです。

前回は、共通処理を基底クラスに集約しました。今回は、新しいシナリオ「レート制限」を追加して、既存コードを修正せずに拡張できることを体験します。

このシリーズについて

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

前回の振り返り

前回までに、以下の構造ができあがりました:

  • Scenario基底クラス: 共通のexecuteメソッド(ログ出力、処理時間計測を含む)
  • 各サブクラス: create_responseをオーバーライドして専用のレスポンスを生成

今回のゴール

APIのレート制限(429 Too Many Requests)シナリオを追加します。重要なのは、既存のコードを一切変更しないことです。

	flowchart LR
    subgraph "既存コード(修正しない)"
        A["Scenario"]
        B["SuccessScenario"]
        C["NotFoundScenario"]
        D["ResponseRole"]
    end
    
    subgraph "新規追加のみ"
        E["RateLimitScenario"]
        F["RateLimitResponse"]
    end
    
    A --> B
    A --> C
    A -.->|extends| E
    D -.->|with| F
    
    style E fill:#9f9,stroke:#333
    style F fill:#9f9,stroke:#333

オープン・クローズドの原則(OCP)

「拡張に対しては開いていて、修正に対しては閉じている」

この原則は、新しい機能を追加するときに既存のコードを変更してはいけない、という考え方です。私たちの設計がこの原則に従っているか確認してみましょう。

RateLimitResponseを作る

まず、レート制限用のレスポンスクラスを作ります。

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

use v5.36;

# ... (既存のパッケージ定義は省略)

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

    has retry_after => (is => 'ro', default => sub { 60 });

    sub render($self) {
        my $body = encode_json({
            success     => JSON::false,
            error       => 'リクエスト数が上限を超えました',
            code        => 'RATE_LIMIT_EXCEEDED',
            retry_after => $self->retry_after,
        });
        return sprintf(
            "HTTP/1.1 429 Too Many Requests\nContent-Type: application/json\nRetry-After: %d\n\n%s",
            $self->retry_after,
            $body,
        );
    }
}

RateLimitResponseRetry-Afterヘッダーを含むレスポンスを生成します。

RateLimitScenarioを作る

次に、このレスポンスを生成するシナリオクラスを作ります。

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

    has retry_after => (is => 'ro', default => sub { 60 });

    sub create_response($self) {
        return RateLimitResponse->new(
            retry_after => $self->retry_after,
        );
    }
}

たったこれだけです。既存のクラスは一切変更していません。

使ってみる

1
2
my $rate_limit = RateLimitScenario->new(retry_after => 30);
say $rate_limit->execute;

実行結果:

1
2
3
4
5
6
7
[Sat Jan 17 13:23:37 2026] Processing: RateLimit
[Sat Jan 17 13:23:37 2026] Completed: RateLimit (0ms)
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30

{"code":"RATE_LIMIT_EXCEEDED","error":"リクエスト数が上限を超えました","retry_after":30,"success":false}

ログ出力や処理時間計測も自動的に適用されています。基底クラスの共通処理が効いていますね。

OCPの確認

新しいシナリオを追加するために行ったことを振り返りましょう:

  • RateLimitResponseクラスを新規作成
  • RateLimitScenarioクラスを新規作成

行わなかったこと:

  • 既存のクラスの修正
  • if/elseの追加
  • 基底クラスの変更

これがオープン・クローズドの原則です。拡張(新しいシナリオの追加)に対しては開いていて、修正(既存コードの変更)に対しては閉じています。

さらにシナリオを追加してみる

勢いに乗って、サーバーエラーシナリオも追加してみましょう。

 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
package ServerErrorResponse {
    use Moo;
    use JSON qw(encode_json);
    with 'ResponseRole';

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

    sub render($self) {
        my $body = encode_json({
            success  => JSON::false,
            error    => 'サーバー内部エラーが発生しました',
            code     => 'INTERNAL_ERROR',
            error_id => $self->error_id,
        });
        return "HTTP/1.1 500 Internal Server Error\nContent-Type: application/json\n\n$body";
    }
}

package ServerErrorScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        my $error_id = sprintf("ERR-%06d", int(rand(1000000)));
        return ServerErrorResponse->new(error_id => $error_id);
    }
}

やはり既存コードは一切変更していません。

完成コード

今回の完成コードを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
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
181
182
183
184
#!/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 RateLimitResponse {
    use Moo;
    use JSON qw(encode_json);
    with 'ResponseRole';

    has retry_after => (is => 'ro', default => sub { 60 });

    sub render($self) {
        my $body = encode_json({
            success     => JSON::false,
            error       => 'リクエスト数が上限を超えました',
            code        => 'RATE_LIMIT_EXCEEDED',
            retry_after => $self->retry_after,
        });
        return sprintf(
            "HTTP/1.1 429 Too Many Requests\nContent-Type: application/json\nRetry-After: %d\n\n%s",
            $self->retry_after, $body,
        );
    }
}

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

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

    sub render($self) {
        my $body = encode_json({
            success  => JSON::false,
            error    => 'サーバー内部エラーが発生しました',
            code     => 'INTERNAL_ERROR',
            error_id => $self->error_id,
        });
        return "HTTP/1.1 500 Internal Server Error\nContent-Type: application/json\n\n$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    => 'リソースが見つかりません',
        );
    }
}

package RateLimitScenario {
    use Moo;
    extends 'Scenario';

    has retry_after => (is => 'ro', default => sub { 60 });

    sub create_response($self) {
        return RateLimitResponse->new(
            retry_after => $self->retry_after,
        );
    }
}

package ServerErrorScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        my $error_id = sprintf("ERR-%06d", int(rand(1000000)));
        return ServerErrorResponse->new(error_id => $error_id);
    }
}

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

まとめ

今回は、レート制限シナリオを追加してOCPを体験しました:

  • 新しいクラスを追加するだけで機能拡張が可能
  • 既存のコードは一切修正しない
  • 基底クラスの共通処理が自動的に適用される

次回はいよいよ最終回。この設計がFactory Methodパターンであることを明かし、全体を振り返ります。

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