@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が必ず持つべきインターフェースを定義します。