Featured image of post 生成処理をオーバーライドしよう

生成処理をオーバーライドしよう

Factory Methodパターンの核心となる「生成処理のオーバーライド」を実装します。各シナリオクラスがそれぞれ専用のResponseを生成する仕組みを詳しく見ていきましょう。

@nqounetです。

前回は、Moo::Roleを使ってレスポンスの共通インターフェースを定義しました。今回は、シナリオごとに異なるレスポンスを生成する仕組み、つまり「生成処理のオーバーライド」を詳しく見ていきます。

このシリーズについて

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

前回の振り返り

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

  • ResponseRole: renderメソッドを必須にするRole
  • Response: JSONレスポンスを生成するクラス
  • Scenario: 基底クラス、create_responseexecuteを持つ
  • SuccessScenario/NotFoundScenario: 各シナリオの実装

今回のゴール

「オーバーライド」という概念を深く理解し、より多くのシナリオを追加してパターンを体感します。

	sequenceDiagram
    participant Client
    participant SuccessScenario
    participant Scenario
    participant SuccessResponse
    
    Client->>SuccessScenario: execute()
    Note over SuccessScenario,Scenario: Scenarioのexecuteが呼ばれる
    SuccessScenario->>SuccessScenario: create_response()
    Note over SuccessScenario: オーバーライドされた<br/>create_responseが実行
    SuccessScenario->>SuccessResponse: new()
    SuccessResponse-->>SuccessScenario: response
    SuccessScenario->>SuccessResponse: render()
    SuccessResponse-->>Client: HTTPレスポンス文字列

オーバーライドとは

オーバーライドとは、親クラスのメソッドを子クラスで再定義することです。Perlでは、同じ名前のメソッドを子クラスに定義するだけで自然にオーバーライドされます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Parent {
    use Moo;
    
    sub greet($self) {
        return "Hello from Parent";
    }
}

package Child {
    use Moo;
    extends 'Parent';
    
    # greet をオーバーライド
    sub greet($self) {
        return "Hello from Child";
    }
}

my $parent = Parent->new;
my $child  = Child->new;

say $parent->greet;  # Hello from Parent
say $child->greet;   # Hello from Child

create_responseのオーバーライド

私たちの設計では、Scenario基底クラスのcreate_responseを各サブクラスでオーバーライドしています。

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

use v5.36;

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

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

    has status       => (is => 'ro', required => 1);
    has content_type => (is => 'ro', default => sub { 'application/json' });
    has body         => (is => 'ro', required => 1);

    sub render($self) {
        my $json_body = encode_json($self->body);
        return sprintf(
            "HTTP/1.1 %s\nContent-Type: %s\n\n%s",
            $self->status, $self->content_type, $json_body,
        );
    }
}

package Scenario {
    use Moo;

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

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

基底クラスのcreate_responseは、呼び出されるとエラーを投げます。これは「サブクラスで必ず実装してね」というメッセージです。

SuccessResponseクラスを作る

レスポンスの種類ごとにクラスを分けることで、より明確な設計になります。

 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 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 SuccessScenario {
    use Moo;
    extends 'Scenario';

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

SuccessScenarioSuccessResponseを生成する責務を持ちます。

ErrorResponseクラスを作る

エラー系のレスポンスも専用クラスにしましょう。

 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
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 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    => '認証が必要です',
        );
    }
}

NotFoundScenarioUnauthorizedScenarioは、同じErrorResponseクラスを使いますが、異なるパラメータで生成します。

実行してみる

作成したシナリオを実行してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for my $scenario_class (qw(
    SuccessScenario
    NotFoundScenario
    UnauthorizedScenario
)) {
    say "=== $scenario_class ===";
    my $scenario = $scenario_class->new;
    say $scenario->execute;
    say "";
}

完成コード

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

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

    sub execute($self) {
        my $response = $self->create_response;
        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 UnauthorizedScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return ErrorResponse->new(
            status     => '401 Unauthorized',
            error_code => 'UNAUTHORIZED',
            message    => '認証が必要です',
        );
    }
}

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

まとめ

今回は、生成処理のオーバーライドを実装しました:

  • 基底クラスのcreate_responseをサブクラスでオーバーライド
  • レスポンスの種類ごとにSuccessResponseErrorResponseを定義
  • 各シナリオが専用のレスポンスを生成する責務を持つ

次回は、共通の送信フローを基底クラスに集約して、コードの重複を減らします。

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