Featured image of post 第3回-インターフェースを変換する橋渡しクラスの実装 - 天気情報ツールで覚えるPerl

第3回-インターフェースを変換する橋渡しクラスの実装 - 天気情報ツールで覚えるPerl

異なるインターフェースを持つサービスを統一する「橋渡し役」を作成。委譲とラッピングを学びます。Perl/Moo第3回。

はじめに

前回は、WeatherServiceOldWeatherAPI という2つの天気サービスを扱おうとして、インターフェースの違いによる問題を体験しました。

今回は、この問題を解決するための「橋渡しクラス」を作成します。OldWeatherAPIWeatherService と同じインターフェースで使えるようにしましょう。

前回の振り返り

前回発生した問題を整理します。

問題詳細
メソッド名の違いget_weather vs fetch_weather_info
戻り値形式の違いハッシュリファレンス vs 文字列
統一処理の困難さループで同じように扱えない

今回の目標

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

新しい概念: 委譲とラッピング

今回学ぶ新しい概念は「委譲とラッピング」です。

  • 委譲(delegation): 処理を別のオブジェクトに任せること
  • ラッピング: 既存のオブジェクトを包み込み、別のインターフェースを提供すること

橋渡しクラスは、内部で OldWeatherAPI のインスタンスを保持し(委譲先)、外部には WeatherService と同じインターフェースを提供します(ラッピング)。

橋渡しクラスの設計

橋渡しクラス OldWeatherAdapter は、以下の役割を持ちます。

  1. 内部で OldWeatherAPI のインスタンスを保持する
  2. get_weather メソッドを実装し、内部で fetch_weather_info を呼び出す
  3. 戻り値を文字列からハッシュリファレンスに変換する
  4. show_weather メソッドを実装する

OldWeatherAdapterクラスの定義

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

    # OldWeatherAPIのインスタンスを保持(委譲先)
    has 'old_api' => (
        is       => 'ro',
        required => 1,
    );

    # WeatherServiceと同じインターフェースを提供
    sub get_weather ($self, $city) {
        # OldWeatherAPIのメソッドを呼び出し
        my $info = $self->old_api->fetch_weather_info($city);

        # 戻り値を変換: '晴れ/25度' -> { condition => '晴れ', temperature => 25 }
        if ($info eq '情報なし') {
            return { condition => '不明', temperature => 0 };
        }

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

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

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

このクラスのポイントを解説します。

ポイント1: 委譲先の保持

1
2
3
4
has 'old_api' => (
    is       => 'ro',
    required => 1,
);

has を使って OldWeatherAPI のインスタンスを保持します。これが「委譲」の基盤となります。コンストラクタで OldWeatherAPI のインスタンスを受け取ります。

ポイント2: インターフェースの変換

1
2
3
4
sub get_weather ($self, $city) {
    my $info = $self->old_api->fetch_weather_info($city);
    # ... 変換処理 ...
}

外部からは get_weather として呼び出されますが、内部では fetch_weather_info を呼び出しています。これがインターフェースの「橋渡し」です。

ポイント3: 戻り値の変換

'晴れ/25度' という文字列を { condition => '晴れ', temperature => 25 } というハッシュリファレンスに変換しています。これにより、呼び出し元は WeatherService と同じ形式でデータを受け取れます。

橋渡しクラスを使った統一的な呼び出し

橋渡しクラスを使うことで、2つのサービスを同じように扱えるようになります。

 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
#!/usr/bin/env perl
use v5.36;

# WeatherService クラス(第1回と同じ)
package WeatherService {
    use v5.36;
    use Moo;

    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 クラス(第2回と同じ)
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,
    );

    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}℃)";
    }
}

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

    # 新サービス
    my $new_service = WeatherService->new;

    # 旧APIを橋渡しクラスでラップ
    my $old_api = OldWeatherAPI->new;
    my $adapted_old = OldWeatherAdapter->new(old_api => $old_api);

    say "=== 統一インターフェースでの呼び出し ===";
    say "";

    # どちらも同じメソッドで呼び出せる!
    say "【新サービス】";
    $new_service->show_weather('東京');
    $new_service->show_weather('大阪');

    say "";
    say "【旧API(橋渡しクラス経由)】";
    $adapted_old->show_weather('東京');
    $adapted_old->show_weather('名古屋');
}

実行結果:

1
2
3
4
5
6
7
8
9
=== 統一インターフェースでの呼び出し ===

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

【旧API(橋渡しクラス経由)】
東京 の天気: 晴れ(気温: 25℃)
名古屋 の天気: 晴れ(気温: 26℃)

橋渡しクラスの効果

橋渡しクラスを導入したことで、以下の効果が得られました。

問題解決策
メソッド名の違い橋渡しクラスが get_weather を提供
戻り値形式の違い橋渡しクラスが変換処理を実行
統一処理の困難さ同じメソッドで呼び出せるようになった

重要なポイント

橋渡しクラスは、既存のクラス(OldWeatherAPI)を変更せずに、新しいインターフェースを提供しています。これは非常に重要な特徴です。

  • レガシーコードに手を加える必要がない
  • 既存の動作を壊すリスクがない
  • 新旧のシステムを共存させられる

今回の完成コード

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

  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
#!/usr/bin/env perl
use v5.36;

# WeatherService クラス
# 新しい天気情報サービス(統一インターフェースの基準となる)
package WeatherService {
    use v5.36;
    use Moo;

    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をWeatherServiceと同じインターフェースで使えるようにする橋渡しクラス
package OldWeatherAdapter {
    use v5.36;
    use Moo;

    # 委譲先: OldWeatherAPIのインスタンス
    has 'old_api' => (
        is       => 'ro',
        required => 1,
    );

    # WeatherServiceと同じインターフェース
    sub get_weather ($self, $city) {
        # OldWeatherAPIのメソッドを呼び出し(委譲)
        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}℃)";
    }
}

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

    say "=== 天気情報ツール(橋渡しクラス版)===";
    say "";

    # 新サービス(そのまま使用)
    my $new_service = WeatherService->new;

    # 旧APIを橋渡しクラスでラップ
    my $old_api = OldWeatherAPI->new;
    my $adapted_old = OldWeatherAdapter->new(old_api => $old_api);

    say "【新サービス】";
    $new_service->show_weather('東京');
    $new_service->show_weather('大阪');
    $new_service->show_weather('札幌');

    say "";
    say "【旧API(橋渡しクラス経由)】";
    $adapted_old->show_weather('東京');
    $adapted_old->show_weather('大阪');
    $adapted_old->show_weather('名古屋');

    say "";
    say "--- 統一インターフェースの効果 ---";
    say "・どちらも show_weather メソッドで呼び出せる";
    say "・どちらも同じ形式の戻り値を返す";
    say "・既存のOldWeatherAPIは変更していない";
}

実行方法:

1
perl weather_adapter.pl

まとめ

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

  • 委譲を使って既存のオブジェクトを内部に保持した
  • ラッピングによって新しいインターフェースを提供した
  • 既存のコードを変更せずに、統一的な呼び出しが可能になった

次回予告

次回は、3つ目のサービスを追加して、複数のサービスをループで処理する方法を学びます。統一インターフェースの真価が発揮されるシーンを体験しましょう!

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