前回の振り返り
前回は、OpenWeatherMap APIを直接叩くシンプルなコードを作成しました。1つのAPIだけを使う分には、問題なく動作していました。
今回は、別の天気情報サービス(WeatherStack API)を追加します。2つのAPIを同時に使おうとすると、どのような問題が発生するのでしょうか?
今回の目標
第2回となる今回は、2つ目のAPI(WeatherStack)を追加し、コードが破綻していく様子を体験します。なぜ設計パターンが必要なのかを実感することが目標です。
WeatherStack APIを追加する
WeatherStackは別の天気情報サービスです。OpenWeatherMapとは異なるインターフェースを持っています。
WeatherStack APIの仕様
- エンドポイント:
http://api.weatherstack.com/current - パラメータ:
access_key - APIキーquery - 都市名(例: Tokyo)
- レスポンス構造がOpenWeatherMapとは異なる
WeatherStack APIのレスポンス構造
1
2
3
4
5
6
7
8
9
10
11
| {
"location": {
"name": "Tokyo",
"country": "Japan"
},
"current": {
"temperature": 25,
"humidity": 60,
"weather_descriptions": ["Sunny"]
}
}
|
OpenWeatherMapとの違いに注目してください。
| 項目 | OpenWeatherMap | WeatherStack |
|---|
| 都市名 | name | location.name |
| 気温 | main.temp | current.temperature |
| 湿度 | main.humidity | current.humidity |
| 天気 | weather[0].description | current.weather_descriptions[0] |
素朴な実装: if文で分岐する
「2つのAPIがあるなら、if文で分岐すればいいじゃないか」と考えるのは自然なことです。実際にやってみましょう。
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
| #!/usr/bin/env perl
use v5.36;
# OpenWeatherMap APIモック
sub get_openweathermap_data ($city) {
my %mock_data = (
'Tokyo' => {
name => 'Tokyo',
main => { temp => 25.5, humidity => 60 },
weather => [{ description => '晴れ' }],
},
'Osaka' => {
name => 'Osaka',
main => { temp => 27.2, humidity => 65 },
weather => [{ description => '曇り' }],
},
);
return $mock_data{$city};
}
# WeatherStack APIモック
sub get_weatherstack_data ($city) {
my %mock_data = (
'Tokyo' => {
location => { name => 'Tokyo', country => 'Japan' },
current => { temperature => 26, humidity => 58, weather_descriptions => ['Sunny'] },
},
'Osaka' => {
location => { name => 'Osaka', country => 'Japan' },
current => { temperature => 28, humidity => 63, weather_descriptions => ['Partly cloudy'] },
},
);
return $mock_data{$city};
}
# 天気情報を表示(2つのAPIに対応)
sub show_weather ($city, $service) {
if ($service eq 'openweathermap') {
my $data = get_openweathermap_data($city);
if ($data) {
my $temp = $data->{main}{temp};
my $humidity = $data->{main}{humidity};
my $desc = $data->{weather}[0]{description};
say "[$service] $city: $desc(気温 $temp℃、湿度 $humidity%)";
}
}
elsif ($service eq 'weatherstack') {
my $data = get_weatherstack_data($city);
if ($data) {
my $temp = $data->{current}{temperature};
my $humidity = $data->{current}{humidity};
my $desc = $data->{current}{weather_descriptions}[0];
say "[$service] $city: $desc(気温 $temp℃、湿度 $humidity%)";
}
}
else {
say "不明なサービス: $service";
}
}
# メイン処理
say "=== 天気予報アグリゲーター(2サービス対応) ===";
say "";
show_weather('Tokyo', 'openweathermap');
show_weather('Tokyo', 'weatherstack');
show_weather('Osaka', 'openweathermap');
show_weather('Osaka', 'weatherstack');
|
実行結果:
1
2
3
4
5
6
| === 天気予報アグリゲーター(2サービス対応) ===
[openweathermap] Tokyo: 晴れ(気温 25.5℃、湿度 60%)
[weatherstack] Tokyo: Sunny(気温 26℃、湿度 58%)
[openweathermap] Osaka: 曇り(気温 27.2℃、湿度 65%)
[weatherstack] Osaka: Partly cloudy(気温 28℃、湿度 63%)
|
動きました。しかし、このコードには問題があります。
破綻の始まり
問題1: if文の肥大化
今は2つのサービスですが、3つ目、4つ目のAPIを追加すると、show_weather関数はどうなるでしょうか?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| sub show_weather ($city, $service) {
if ($service eq 'openweathermap') {
# OpenWeatherMap用の処理...
}
elsif ($service eq 'weatherstack') {
# WeatherStack用の処理...
}
elsif ($service eq 'weatherapi') { # 3つ目
# WeatherAPI用の処理...
}
elsif ($service eq 'visualcrossing') { # 4つ目
# VisualCrossing用の処理...
}
elsif ($service eq 'tomorrow') { # 5つ目
# Tomorrow.io用の処理...
}
else {
say "不明なサービス: $service";
}
}
|
if-elsif の連鎖が際限なく伸びていきます。これは保守性の低下を招きます。
問題2: 同じ処理の重複
各サービスの処理をよく見ると、以下の部分が重複しています。
- データを取得する
- 気温・湿度・天気を取り出す
- 整形して表示する
違いは「データの取得方法」と「データ構造」だけです。
問題3: 新しいAPIの追加が困難
新しいAPIを追加するたびに以下が必要になります。
- if文の追加
- 新しいデータ取得関数の作成
- データ構造に合わせた整形処理の追加
これらをすべて1つの関数で管理し続けるのは、バグの温床です。
3つ目のAPIを追加してみる
実際に3つ目のAPI(WeatherAPI)を追加してみましょう。
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
| #!/usr/bin/env perl
use v5.36;
# OpenWeatherMap APIモック
sub get_openweathermap_data ($city) {
my %mock_data = (
'Tokyo' => {
name => 'Tokyo',
main => { temp => 25.5, humidity => 60 },
weather => [{ description => '晴れ' }],
},
);
return $mock_data{$city};
}
# WeatherStack APIモック
sub get_weatherstack_data ($city) {
my %mock_data = (
'Tokyo' => {
location => { name => 'Tokyo' },
current => { temperature => 26, humidity => 58, weather_descriptions => ['Sunny'] },
},
);
return $mock_data{$city};
}
# WeatherAPI APIモック(3つ目)
sub get_weatherapi_data ($city) {
my %mock_data = (
'Tokyo' => {
location => { name => 'Tokyo' },
current => {
temp_c => 25.8,
humidity => 59,
condition => { text => 'Clear' },
},
},
);
return $mock_data{$city};
}
# 天気情報を表示(3つのAPIに対応)
sub show_weather ($city, $service) {
if ($service eq 'openweathermap') {
my $data = get_openweathermap_data($city);
if ($data) {
my $temp = $data->{main}{temp};
my $humidity = $data->{main}{humidity};
my $desc = $data->{weather}[0]{description};
say "[$service] $city: $desc(気温 $temp℃、湿度 $humidity%)";
}
}
elsif ($service eq 'weatherstack') {
my $data = get_weatherstack_data($city);
if ($data) {
my $temp = $data->{current}{temperature};
my $humidity = $data->{current}{humidity};
my $desc = $data->{current}{weather_descriptions}[0];
say "[$service] $city: $desc(気温 $temp℃、湿度 $humidity%)";
}
}
elsif ($service eq 'weatherapi') {
my $data = get_weatherapi_data($city);
if ($data) {
my $temp = $data->{current}{temp_c}; # temp_c !
my $humidity = $data->{current}{humidity};
my $desc = $data->{current}{condition}{text}; # condition.text !
say "[$service] $city: $desc(気温 $temp℃、湿度 $humidity%)";
}
}
else {
say "不明なサービス: $service";
}
}
# メイン処理
say "=== 天気予報アグリゲーター(3サービス対応) ===";
show_weather('Tokyo', 'openweathermap');
show_weather('Tokyo', 'weatherstack');
show_weather('Tokyo', 'weatherapi');
|
APIが増えるたびに、関数が長くなっていきます。
設計上の問題点
この「if文で分岐する」アプローチには、オブジェクト指向設計の原則に反する問題があります。
単一責任の原則(SRP)違反
show_weather関数は、以下の責任を同時に持っています。
- サービスの判定
- データの取得
- データ構造の解析
- 表示の整形
1つの関数が多くの責任を持ちすぎています。
開放閉鎖の原則(OCP)違反
新しいAPIを追加するたびに、既存のshow_weather関数を修正する必要があります。
理想的な設計では「拡張に対して開いており、修正に対して閉じている」べきです。つまり、新しいAPIを追加するときに、既存のコードを変更せずに済むべきです。
まとめ
今回は、2つ目(そして3つ目)のAPIを追加することで、素朴なif文分岐アプローチの限界を体験しました。
- 複数のAPIを統合しようとすると、if文が肥大化する
- 同じような処理が重複してしまう
- 新しいAPIの追加が困難になる
- 単一責任の原則(SRP)に違反している
- 開放閉鎖の原則(OCP)に違反している
この問題を解決するには、異なるインターフェースを持つAPIを「統一的に扱う」仕組みが必要です。
次回予告
次回は、この問題を解決するために「Adapterパターン」を導入します。各APIの異なるインターフェースを共通の形式に変換することで、コードをスッキリ整理します。お楽しみに!