Featured image of post レスポンスの共通ルールを決めよう

レスポンスの共通ルールを決めよう

Moo::Roleを使ってレスポンスクラスに共通のインターフェースを定義します。renderメソッドを必須にすることで、レスポンスの一貫性を保証しましょう。

@nqounetです。

前回は、継承を使ってシナリオごとにクラスを分けました。今回は、Moo::Roleを使ってレスポンスクラスに共通のインターフェースを定義します。

このシリーズについて

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

前回の振り返り

前回は、Scenario基底クラスと、それを継承するSuccessScenarioNotFoundScenarioを作りました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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;
    }
}

executeメソッドは、create_responseが返すオブジェクトにrenderメソッドがあることを期待しています。

今回のゴール

renderメソッドを必須にするRoleを定義し、レスポンスクラスに適用します。

	classDiagram
    class ResponseRole {
        <<interface>>
        +render()*
    }
    
    class Response {
        +status
        +content_type
        +body
        +render()
    }
    
    class HtmlResponse {
        +status
        +html
        +render()
    }
    
    ResponseRole <|.. Response : with
    ResponseRole <|.. HtmlResponse : with

なぜRoleが必要か

現状では、create_responseが返すオブジェクトにrenderメソッドがあるかどうかは、実行時まで分かりません。もし誰かがrenderメソッドを持たないオブジェクトを返したら、実行時エラーになります。

Roleを使うと、クラスに「このメソッドを必ず持つこと」というルールを強制できます。

ResponseRoleを定義する

renderメソッドを必須にするRoleを作りましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, JSON(cpanmでインストール)

use v5.36;

package ResponseRole {
    use Moo::Role;

    requires 'render';
}

requires 'render'は、「このRoleを適用するクラスはrenderメソッドを持っていなければならない」という宣言です。

ResponseクラスにRoleを適用する

ResponseクラスにResponseRoleを適用します。

 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
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,
        );
    }
}

with 'ResponseRole'でRoleを適用しています。Responseクラスはrenderメソッドを持っているので、問題なくコンパイルできます。

Roleの効果を確認する

試しに、renderメソッドを持たないクラスにResponseRoleを適用してみましょう。

1
2
3
4
5
package BrokenResponse {
    use Moo;
    with 'ResponseRole';
    # render メソッドがない!
}

このコードを実行すると、以下のようなエラーが発生します。

1
Can't apply ResponseRole to BrokenResponse - missing render

Roleのおかげで、必要なメソッドが実装されていないことをすぐに検出できます。

別のResponseを作ってみる

Roleがあれば、異なる形式のレスポンスも作れます。例えば、HTMLレスポンス用のクラスを作ってみましょう。

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

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

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

    sub render($self) {
        return sprintf(
            "HTTP/1.1 %s\nContent-Type: text/html\n\n%s",
            $self->status,
            $self->html,
        );
    }
}

HtmlResponseもrenderメソッドを持っているので、ResponseRoleを適用できます。そして、Scenarioクラスから使うことができます。

完成コード

今回の完成コードを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
#!/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;
    }
}

package SuccessScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return Response->new(
            status => '200 OK',
            body   => {
                success => JSON::true,
                message => 'リクエストが正常に処理されました',
                data    => { id => 1, name => 'サンプルアイテム' },
            },
        );
    }
}

package NotFoundScenario {
    use Moo;
    extends 'Scenario';

    sub create_response($self) {
        return Response->new(
            status => '404 Not Found',
            body   => {
                success => JSON::false,
                error   => 'リソースが見つかりません',
                code    => 'NOT_FOUND',
            },
        );
    }
}

my $success = SuccessScenario->new;
say "=== Success ===";
say $success->execute;
say "";

my $not_found = NotFoundScenario->new;
say "=== Not Found ===";
say $not_found->execute;

まとめ

今回は、Moo::Roleを使ってレスポンスの共通インターフェースを定義しました:

  • ResponseRole: renderメソッドを必須にするRole
  • with 'ResponseRole'でクラスにRoleを適用
  • Roleは「契約」として機能し、必要なメソッドの実装を保証する

次回は、実際にcreate_responseをオーバーライドして、シナリオごとに異なるレスポンスを生成する仕組みを詳しく見ていきます。

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