Featured image of post 第2回-複数API統合の課題 - 2つ目のAPIで破綻する

第2回-複数API統合の課題 - 2つ目のAPIで破綻する

複数API統合で発生する課題(インターフェース不一致、条件分岐の複雑化)を解説。Perlで実装し、なぜ設計パターンが必要になるかを体験します。

前回の振り返り

前回は、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との違いに注目してください。

項目OpenWeatherMapWeatherStack
都市名namelocation.name
気温main.tempcurrent.temperature
湿度main.humiditycurrent.humidity
天気weather[0].descriptioncurrent.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: 同じ処理の重複

各サービスの処理をよく見ると、以下の部分が重複しています。

  1. データを取得する
  2. 気温・湿度・天気を取り出す
  3. 整形して表示する

違いは「データの取得方法」と「データ構造」だけです。

問題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の異なるインターフェースを共通の形式に変換することで、コードをスッキリ整理します。お楽しみに!

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