Featured image of post これがFactory Methodパターンだ!

これがFactory Methodパターンだ!

シリーズ最終回。これまで作ってきた設計がFactory Methodパターンであることを明かし、GoFの定義と照らし合わせて理解を深めます。

@nqounetです。

いよいよ最終回です。これまで作ってきたAPIレスポンスシミュレーターの設計が、実は「Factory Methodパターン」というデザインパターンであることを明かします。

このシリーズについて

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

シリーズの振り返り

8回を通じて、以下のように設計を進化させてきました:

  1. 第1回: シンプルなMockApiを作る(基底クラスでの生成)
  2. 第2回: シナリオが増えてif/elseが肥大化(問題の認識)
  3. 第3回: シナリオごとにクラスを分ける(継承によるCreator分離)
  4. 第4回: Roleで共通インターフェースを定義(Productインターフェース)
  5. 第5回: 生成処理をオーバーライド(Factory Methodのオーバーライド)
  6. 第6回: 共通処理を基底クラスに集約(共通処理の集約)
  7. 第7回: 新しいシナリオを追加してOCPを体験(オープン・クローズドの原則)

この段階的な設計改善こそが、Factory Methodパターンを自然に習得するための道のりでした。そして今回、この設計パターンに名前がついていることをお伝えします。

Factory Methodパターンとは

GoF(Gang of Four)のデザインパターンの1つで、以下のように定義されています:

オブジェクトを生成するためのインターフェースを定義し、どのクラスをインスタンス化するかはサブクラスに決定させる。Factory Methodは、インスタンス化をサブクラスに委ねる。

難しく聞こえますが、私たちが作ったものそのものです。

構造の対応関係

Factory Methodパターンの登場人物と、私たちの実装を対応させてみましょう。

Factory Method用語私たちの実装役割
ProductResponseRole生成されるオブジェクトのインターフェース
ConcreteProductSuccessResponse, ErrorResponse, RateLimitResponse実際に生成されるオブジェクト
CreatorScenariofactory method(create_response)を宣言する抽象クラス
ConcreteCreatorSuccessScenario, NotFoundScenario, RateLimitScenariofactory methodを実装する具象クラス

クラス図で確認する

	classDiagram
    class ResponseRole {
        <<interface>>
        +render() String
    }
    
    class SuccessResponse {
        +data
        +render() String
    }
    
    class ErrorResponse {
        +status
        +error_code
        +message
        +render() String
    }
    
    class RateLimitResponse {
        +retry_after
        +render() String
    }
    
    class Scenario {
        +create_response() Response
        +execute() String
    }
    
    class SuccessScenario {
        +create_response() SuccessResponse
    }
    
    class NotFoundScenario {
        +create_response() ErrorResponse
    }
    
    class RateLimitScenario {
        +retry_after
        +create_response() RateLimitResponse
    }
    
    ResponseRole <|.. SuccessResponse
    ResponseRole <|.. ErrorResponse
    ResponseRole <|.. RateLimitResponse
    
    Scenario <|-- SuccessScenario
    Scenario <|-- NotFoundScenario
    Scenario <|-- RateLimitScenario
    
    SuccessScenario ..> SuccessResponse : creates
    NotFoundScenario ..> ErrorResponse : creates
    RateLimitScenario ..> RateLimitResponse : creates

Factory Methodのメリット

このパターンを使うことで得られるメリットを整理しましょう:

  • 生成処理の分離: オブジェクトの生成と使用を分離できる
  • 拡張性: 新しい種類のオブジェクトを追加しやすい(OCP)
  • 一貫性: 共通処理を基底クラスで保証できる
  • テスト容易性: 各Creatorを独立してテストできる

Factory Methodの適用場面

以下のような場面でFactory Methodが有効です:

  • どのクラスを生成するかが実行時まで決まらない
  • サブクラスで生成するオブジェクトを決定したい
  • 生成処理を局所化して、変更の影響範囲を限定したい

完成コード

シリーズ全体の完成コードを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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, JSON, Time::HiRes(cpanmでインストール)
#
# === Factory Methodパターン ===
# Product: ResponseRole
# ConcreteProduct: SuccessResponse, ErrorResponse, RateLimitResponse, ServerErrorResponse
# Creator: Scenario (create_response がfactory method)
# ConcreteCreator: SuccessScenario, NotFoundScenario, RateLimitScenario, ServerErrorScenario

use v5.36;

# ========================================
# Product(インターフェース)
# ========================================
package ResponseRole {
    use Moo::Role;
    requires 'render';
}

# ========================================
# ConcreteProduct(具象クラス群)
# ========================================
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";
    }
}

# ========================================
# Creator(抽象クラス)
# ========================================
package Scenario {
    use Moo;
    use Time::HiRes qw(gettimeofday tv_interval);

    # Factory Method(サブクラスでオーバーライドする)
    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)";
    }

    # Template Methodパターン風の共通処理
    sub execute($self) {
        my $start = [gettimeofday];
        $self->log_request;
        my $response = $self->create_response;  # Factory Methodを呼ぶ
        my $elapsed = int(tv_interval($start) * 1000);
        $self->log_complete($elapsed);
        return $response->render;
    }
}

# ========================================
# ConcreteCreator(具象クラス群)
# ========================================
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 UnauthorizedScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return ErrorResponse->new(
            status     => '401 Unauthorized',
            error_code => 'UNAUTHORIZED',
            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);
    }
}

# ========================================
# メイン処理
# ========================================
say "=== APIレスポンスシミュレーター(Factory Methodパターン) ===\n";

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

まとめ

全8回を通じて、if/elseの肥大化問題をFactory Methodパターンで解決する過程を体験しました:

  1. 問題の認識: シナリオが増えると条件分岐が爆発する
  2. 継承による分離: シナリオごとにクラスを分ける
  3. インターフェースの定義: Roleで共通ルールを強制
  4. 共通処理の集約: 基底クラスにロジックをまとめる
  5. OCPの実践: 既存コードを修正せずに拡張

Factory Methodは、オブジェクトの生成を柔軟に行いたい場面で強力なパターンです。ぜひ実際の開発でも活用してみてください。

参考資料

オブジェクト指向における再利用のためのデザインパターン amazon.co.jp
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。