Featured image of post 第4回-複数サービスを統一インターフェースで扱う設計 - 天気情報ツールで覚えるPerl

第4回-複数サービスを統一インターフェースで扱う設計 - 天気情報ツールで覚えるPerl

3つ目のサービスを追加し複数サービスをループ処理。統一インターフェースの効果を体験します。Perl/Moo連載第4回。

はじめに

前回は、OldWeatherAPIWeatherService と同じインターフェースで使えるようにする橋渡しクラス OldWeatherAdapter を作成しました。

今回は、3つ目の天気サービスを追加し、複数のサービスをループで統一的に処理する方法を学びます。

前回の振り返り

前回作成した橋渡しクラスにより、以下が実現しました。

  • OldWeatherAPIWeatherService と同じ get_weather / show_weather メソッドで呼び出せる
  • 戻り値の形式も統一された
  • 既存のコードを変更せずに対応できた

今回の目標

第4回となる今回は、海外の天気サービス ForeignWeatherService を追加し、3つのサービスをループで処理します。統一インターフェースの真価を体験しましょう。

新しい概念: 複数サービスの統一処理(多態性)

今回学ぶ新しい概念は「複数サービスの統一処理」、つまり多態性(ポリモーフィズム)の実践です。

多態性とは、異なるクラスのオブジェクトを同じインターフェースで扱える性質のことです。橋渡しクラスを使って複数のサービスを同じ形式に揃えることで、この多態性を実現します。

ForeignWeatherServiceの登場

新たに、海外の天気情報を提供する ForeignWeatherService を追加します。このサービスも独自のインターフェースを持っています。

ForeignWeatherServiceクラスの定義

 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
package ForeignWeatherService {
    use v5.36;
    use Moo;

    # メソッド名: retrieve_conditions
    # 戻り値: 配列リファレンス ['Sunny', 25]
    sub retrieve_conditions ($self, $city_code) {
        my %data = (
            'NYC' => ['Sunny',  20],
            'LON' => ['Cloudy', 15],
            'PAR' => ['Rainy',  12],
        );

        return $data{$city_code} // ['Unknown', 0];
    }

    # 都市コードと日本語名のマッピング
    sub city_codes ($self) {
        return {
            'ニューヨーク' => 'NYC',
            'ロンドン'     => 'LON',
            'パリ'         => 'PAR',
        };
    }
}

このサービスの特徴を見てみましょう。

  • メソッド名が異なる: retrieve_conditions
  • 戻り値が異なる: 配列リファレンス ['Sunny', 25]
  • 引数が異なる: 都市名ではなく都市コード('NYC' など)
  • 天気が英語: 'Sunny', 'Cloudy', 'Rainy'

ForeignWeatherAdapterの作成

ForeignWeatherService を統一インターフェースで使えるようにする橋渡しクラスを作成します。

 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
package ForeignWeatherAdapter {
    use v5.36;
    use Moo;

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

    # 英語から日本語への変換テーブル
    my %CONDITION_MAP = (
        'Sunny'   => '晴れ',
        'Cloudy'  => '曇り',
        'Rainy'   => '雨',
        'Unknown' => '不明',
    );

    sub get_weather ($self, $city) {
        # 都市名を都市コードに変換
        my $codes = $self->foreign_service->city_codes;
        my $city_code = $codes->{$city};

        unless ($city_code) {
            return { condition => '不明', temperature => 0 };
        }

        # ForeignWeatherServiceのメソッドを呼び出し
        my $result = $self->foreign_service->retrieve_conditions($city_code);

        # 戻り値を変換
        my ($condition_en, $temp) = @$result;
        my $condition_ja = $CONDITION_MAP{$condition_en} // '不明';

        return {
            condition   => $condition_ja,
            temperature => $temp,
        };
    }

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        say "$city の天気: $weather->{condition}(気温: $weather->{temperature}℃)";
    }
}

この橋渡しクラスは、前回の OldWeatherAdapter と同様の構造を持っています。

  • 委譲先(foreign_service)を保持
  • get_weather で元のメソッドを呼び出し、戻り値を変換
  • 都市名→都市コードの変換、英語→日本語の変換を実行

複数サービスをループで処理

3つのサービスを配列に格納し、ループで統一的に処理してみましょう。

  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
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
#!/usr/bin/env perl
use v5.36;

# WeatherService クラス
package WeatherService {
    use v5.36;
    use Moo;

    has 'name' => (is => 'ro', default => sub { '国内天気サービス' });

    sub get_weather ($self, $city) {
        my %weather_data = (
            '東京' => { condition => '晴れ', temperature => 25 },
            '大阪' => { condition => '曇り', temperature => 23 },
            '札幌' => { condition => '雨',   temperature => 18 },
        );
        return $weather_data{$city} // { condition => '不明', temperature => 0 };
    }

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        say "$city の天気: $weather->{condition}(気温: $weather->{temperature}℃)";
    }
}

# OldWeatherAPI クラス
package OldWeatherAPI {
    use v5.36;
    use Moo;

    sub fetch_weather_info ($self, $location) {
        my %data = (
            '東京' => '晴れ/25度',
            '大阪' => '曇り/23度',
            '名古屋' => '晴れ/26度',
        );
        return $data{$location} // '情報なし';
    }
}

# OldWeatherAdapter クラス
package OldWeatherAdapter {
    use v5.36;
    use Moo;

    has 'old_api' => (is => 'ro', required => 1);
    has 'name' => (is => 'ro', default => sub { 'レガシー天気API' });

    sub get_weather ($self, $city) {
        my $info = $self->old_api->fetch_weather_info($city);

        if ($info eq '情報なし') {
            return { condition => '不明', temperature => 0 };
        }

        my ($condition, $temp_str) = split '/', $info;
        $temp_str =~ s/度$//;

        return {
            condition   => $condition,
            temperature => int($temp_str),
        };
    }

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        say "$city の天気: $weather->{condition}(気温: $weather->{temperature}℃)";
    }
}

# ForeignWeatherService クラス
package ForeignWeatherService {
    use v5.36;
    use Moo;

    sub retrieve_conditions ($self, $city_code) {
        my %data = (
            'NYC' => ['Sunny',  20],
            'LON' => ['Cloudy', 15],
            'PAR' => ['Rainy',  12],
        );
        return $data{$city_code} // ['Unknown', 0];
    }

    sub city_codes ($self) {
        return {
            'ニューヨーク' => 'NYC',
            'ロンドン'     => 'LON',
            'パリ'         => 'PAR',
        };
    }
}

# ForeignWeatherAdapter クラス
package ForeignWeatherAdapter {
    use v5.36;
    use Moo;

    has 'foreign_service' => (is => 'ro', required => 1);
    has 'name' => (is => 'ro', default => sub { '海外天気サービス' });

    my %CONDITION_MAP = (
        'Sunny'   => '晴れ',
        'Cloudy'  => '曇り',
        'Rainy'   => '雨',
        'Unknown' => '不明',
    );

    sub get_weather ($self, $city) {
        my $codes = $self->foreign_service->city_codes;
        my $city_code = $codes->{$city};

        unless ($city_code) {
            return { condition => '不明', temperature => 0 };
        }

        my $result = $self->foreign_service->retrieve_conditions($city_code);
        my ($condition_en, $temp) = @$result;
        my $condition_ja = $CONDITION_MAP{$condition_en} // '不明';

        return {
            condition   => $condition_ja,
            temperature => $temp,
        };
    }

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        say "$city の天気: $weather->{condition}(気温: $weather->{temperature}℃)";
    }
}

# メイン処理
package main {
    use v5.36;

    # 3つのサービスを準備
    my $service1 = WeatherService->new;
    my $service2 = OldWeatherAdapter->new(old_api => OldWeatherAPI->new);
    my $service3 = ForeignWeatherAdapter->new(foreign_service => ForeignWeatherService->new);

    # 配列にまとめる
    my @services = ($service1, $service2, $service3);

    say "=== 複数サービス統合 天気情報ツール ===";
    say "";

    # 各サービスの天気を表示
    for my $service (@services) {
        say "【" . $service->name . "】";

        # 全サービス共通のメソッドで呼び出せる!
        if ($service->name eq '海外天気サービス') {
            $service->show_weather('ニューヨーク');
            $service->show_weather('ロンドン');
            $service->show_weather('パリ');
        } else {
            $service->show_weather('東京');
            $service->show_weather('大阪');
        }

        say "";
    }
}

実行結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
=== 複数サービス統合 天気情報ツール ===

【国内天気サービス】
東京 の天気: 晴れ(気温: 25℃)
大阪 の天気: 曇り(気温: 23℃)

【レガシー天気API】
東京 の天気: 晴れ(気温: 25℃)
大阪 の天気: 曇り(気温: 23℃)

【海外天気サービス】
ニューヨーク の天気: 晴れ(気温: 20℃)
ロンドン の天気: 曇り(気温: 15℃)
パリ の天気: 雨(気温: 12℃)

統一インターフェースの効果

3つのサービスを統一インターフェースで扱えるようになったことで、以下の効果が得られました。

効果1: ループ処理が可能に

1
2
3
for my $service (@services) {
    $service->show_weather('東京');
}

どのサービスも同じ show_weather メソッドを持っているため、ループで統一的に処理できます。

効果2: サービスの追加が容易に

新しいサービスが追加されても、橋渡しクラスを作成するだけで対応できます。メイン処理のロジックを変更する必要がありません。

効果3: 既存コードへの影響がない

元のサービスクラス(OldWeatherAPI, ForeignWeatherService)は一切変更していません。

今回の完成コード

今回作成した完成コードを以下に示します。

  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
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#!/usr/bin/env perl
use v5.36;

# WeatherService クラス
# 国内天気サービス(統一インターフェースの基準)
package WeatherService {
    use v5.36;
    use Moo;

    has 'name' => (is => 'ro', default => sub { '国内天気サービス' });

    sub get_weather ($self, $city) {
        my %weather_data = (
            '東京' => { condition => '晴れ', temperature => 25 },
            '大阪' => { condition => '曇り', temperature => 23 },
            '札幌' => { condition => '雨',   temperature => 18 },
        );
        return $weather_data{$city} // { condition => '不明', temperature => 0 };
    }

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        say "$city の天気: $weather->{condition}(気温: $weather->{temperature}℃)";
    }
}

# OldWeatherAPI クラス
# レガシーな天気情報API
package OldWeatherAPI {
    use v5.36;
    use Moo;

    sub fetch_weather_info ($self, $location) {
        my %data = (
            '東京' => '晴れ/25度',
            '大阪' => '曇り/23度',
            '名古屋' => '晴れ/26度',
        );
        return $data{$location} // '情報なし';
    }
}

# OldWeatherAdapter クラス
# OldWeatherAPI用の橋渡しクラス
package OldWeatherAdapter {
    use v5.36;
    use Moo;

    has 'old_api' => (is => 'ro', required => 1);
    has 'name' => (is => 'ro', default => sub { 'レガシー天気API' });

    sub get_weather ($self, $city) {
        my $info = $self->old_api->fetch_weather_info($city);

        if ($info eq '情報なし') {
            return { condition => '不明', temperature => 0 };
        }

        my ($condition, $temp_str) = split '/', $info;
        $temp_str =~ s/度$//;

        return {
            condition   => $condition,
            temperature => int($temp_str),
        };
    }

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        say "$city の天気: $weather->{condition}(気温: $weather->{temperature}℃)";
    }
}

# ForeignWeatherService クラス
# 海外天気情報サービス
package ForeignWeatherService {
    use v5.36;
    use Moo;

    sub retrieve_conditions ($self, $city_code) {
        my %data = (
            'NYC' => ['Sunny',  20],
            'LON' => ['Cloudy', 15],
            'PAR' => ['Rainy',  12],
        );
        return $data{$city_code} // ['Unknown', 0];
    }

    sub city_codes ($self) {
        return {
            'ニューヨーク' => 'NYC',
            'ロンドン'     => 'LON',
            'パリ'         => 'PAR',
        };
    }
}

# ForeignWeatherAdapter クラス
# ForeignWeatherService用の橋渡しクラス
package ForeignWeatherAdapter {
    use v5.36;
    use Moo;

    has 'foreign_service' => (is => 'ro', required => 1);
    has 'name' => (is => 'ro', default => sub { '海外天気サービス' });

    my %CONDITION_MAP = (
        'Sunny'   => '晴れ',
        'Cloudy'  => '曇り',
        'Rainy'   => '雨',
        'Unknown' => '不明',
    );

    sub get_weather ($self, $city) {
        my $codes = $self->foreign_service->city_codes;
        my $city_code = $codes->{$city};

        unless ($city_code) {
            return { condition => '不明', temperature => 0 };
        }

        my $result = $self->foreign_service->retrieve_conditions($city_code);
        my ($condition_en, $temp) = @$result;
        my $condition_ja = $CONDITION_MAP{$condition_en} // '不明';

        return {
            condition   => $condition_ja,
            temperature => $temp,
        };
    }

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        say "$city の天気: $weather->{condition}(気温: $weather->{temperature}℃)";
    }
}

# メイン処理
package main {
    use v5.36;

    say "=== 複数サービス統合 天気情報ツール ===";
    say "";

    # 3つのサービスを準備(橋渡しクラスでラップ)
    my @services = (
        WeatherService->new,
        OldWeatherAdapter->new(old_api => OldWeatherAPI->new),
        ForeignWeatherAdapter->new(foreign_service => ForeignWeatherService->new),
    );

    # 各サービスの情報を表示
    for my $service (@services) {
        say "【" . $service->name . "】";

        if ($service->name eq '海外天気サービス') {
            $service->show_weather('ニューヨーク');
            $service->show_weather('ロンドン');
            $service->show_weather('パリ');
        } else {
            $service->show_weather('東京');
            $service->show_weather('大阪');
        }

        say "";
    }

    say "--- 統一インターフェースの効果 ---";
    say "・3つのサービスを同じメソッドで呼び出せる";
    say "・ループで統一的に処理できる";
    say "・新しいサービスの追加が容易";
}

実行方法:

1
perl weather_multi_service.pl

まとめ

今回は、3つ目の天気サービス ForeignWeatherService を追加し、複数のサービスをループで統一的に処理する方法を学びました。

  • 新しいサービスには新しい橋渡しクラスを作成して対応した
  • 複数のサービスを配列にまとめてループ処理できた
  • 統一インターフェースによる多態性を体験した

次回予告

次回は最終回です。ここまで作成してきた「橋渡しクラス」の設計には、実は名前があります。GoFデザインパターンの1つとして知られるその名前と、パターンの本質について解説します。お楽しみに!

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