Featured image of post シナリオ別の生成クラスに分けよう

シナリオ別の生成クラスに分けよう

if/elseの肥大化を解決するため、シナリオごとにクラスを分けます。継承を使って、成功シナリオと失敗シナリオをそれぞれ専用のクラスとして定義しましょう。

@nqounetです。

前回は、シナリオが増えるにつれてif/elseが肥大化し、管理が難しくなる問題を体験しました。今回は、継承を使ってシナリオごとにクラスを分けることで、この問題を解決していきます。

このシリーズについて

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

前回の振り返り

前回のcreate_responseメソッドは、こんな状態でした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sub create_response($self, $scenario) {
    if ($scenario eq 'success') {
        # 成功レスポンス生成
    }
    elsif ($scenario eq 'not_found') {
        # 404レスポンス生成
    }
    elsif ($scenario eq 'unauthorized') {
        # 401レスポンス生成
    }
    # ... さらに続く
}

1つのメソッドに全ての生成ロジックが詰め込まれていました。

今回のゴール

シナリオごとに専用のクラスを作り、レスポンス生成の責務を分離します。

	classDiagram
    class Scenario {
        +create_response()
        +execute()
    }
    
    class SuccessScenario {
        +create_response()
    }
    
    class NotFoundScenario {
        +create_response()
    }
    
    Scenario <|-- SuccessScenario
    Scenario <|-- NotFoundScenario

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

use v5.36;

package Response {
    use Moo;
    use JSON qw(encode_json);

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

Scenario基底クラスには2つのメソッドがあります:

  • create_response: サブクラスで実装する抽象的なメソッド(未実装だとエラー)
  • execute: レスポンスを生成してレンダリングする共通処理

SuccessScenarioを作る

成功シナリオ専用のクラスを作りましょう。Scenarioを継承してcreate_responseを実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 => 'サンプルアイテム' },
            },
        );
    }
}

extends 'Scenario'で基底クラスを継承し、create_responseメソッドをオーバーライドしています。

NotFoundScenarioを作る

同様に、404エラー用のクラスも作ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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',
            },
        );
    }
}

各シナリオクラスは、自分自身のレスポンス生成ロジックだけを持っています。他のシナリオのことは知りません。

使ってみる

作成したシナリオクラスを使ってみましょう。

1
2
3
4
5
6
7
8
my $success = SuccessScenario->new;
say "=== Success ===";
say $success->execute;
say "";

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

実行結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
=== Success ===
HTTP/1.1 200 OK
Content-Type: application/json

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

=== Not Found ===
HTTP/1.1 404 Not Found
Content-Type: application/json

{"code":"NOT_FOUND","error":"リソースが見つかりません","success":false}

この設計の利点

クラスを分けたことで、いくつかの利点が得られました:

  • 各シナリオのロジックが独立している
  • 新しいシナリオを追加しても、既存のクラスを修正する必要がない
  • テストをシナリオごとに分けて書ける
  • コードの見通しが良くなった

完成コード

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

use v5.36;

package Response {
    use Moo;
    use JSON qw(encode_json);

    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;

まとめ

今回は、継承を使ってシナリオごとのクラスに分けました:

  • Scenario基底クラス: 共通のexecuteメソッドを持つ
  • SuccessScenario/NotFoundScenario: 各シナリオ専用のcreate_responseを実装

しかし、現状ではResponseクラスに共通のルールがありません。次回は、Roleを使ってResponseが必ず持つべきインターフェースを定義します。

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