Featured image of post 第5回-Facadeパターン実装 - 窓口の一本化

第5回-Facadeパターン実装 - 窓口の一本化

PerlでFacadeパターンを実装し、複数APIを統一インターフェースで管理。フォールバック機能で可用性向上。実践的な設計を学びます。

前回の振り返り

前回は、Adapterパターンだけでは解決できない課題を整理しました。呼び出し側にAdapter管理の責任が残り、フォールバック処理を毎回実装する必要がありました。

今回は、この問題を「Facadeパターン」で解決します。

今回の目標

第5回となる今回は、Facadeパターンを使って複数のAdapterを統合管理します。呼び出し側は「天気を取得」と言うだけでOK。内部で自動フォールバックする仕組みを実装します。

Facadeパターンとは

Facadeパターンは、複雑なサブシステムに対してシンプルなインターフェースを提供するデザインパターンです。

「Facade」はフランス語で「建物の正面」を意味します。建物の裏側がどんなに複雑でも、正面から見れば美しい一枚の壁に見える。利用者は建物の内部構造を知る必要がありません。

日常生活での例

ホテルのコンシェルジュを想像してください。

  • 「レストランを予約して」と言えば、コンシェルジュが複数のレストランを調べ、空き状況を確認し、予約を取ってくれます
  • 利用者は「どのレストランがあるか」「電話番号は何か」「予約方法は何か」を知る必要がありません
  • コンシェルジュ = Facade

WeatherFacadeの設計

WeatherFacadeは以下の責務を持ちます。

  1. 複数のAdapterを内部で管理する
  2. 天気情報の取得リクエストを受け付ける
  3. 順番にAdapterを試し、成功したら結果を返す(フォールバック)
  4. すべて失敗した場合はundefを返す

クラス図

	classDiagram
    class WeatherFacade {
        -adapters: Array
        +new(adapters)
        +get_weather(city)
    }
    
    class WeatherAdapter::Role {
        <<interface>>
        +get_weather(city)*
        +name*
        +show_weather(city)
    }
    
    class WeatherAdapter::OpenWeatherMap {
        +name
        +get_weather(city)
    }
    
    class WeatherAdapter::WeatherStack {
        +name
        +get_weather(city)
    }
    
    WeatherFacade --> WeatherAdapter::Role : uses
    WeatherAdapter::OpenWeatherMap ..|> WeatherAdapter::Role
    WeatherAdapter::WeatherStack ..|> WeatherAdapter::Role

WeatherFacadeの基本実装

まず、WeatherFacadeの基本構造を実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package WeatherFacade {
    use v5.36;
    use Moo;

    # 複数のAdapterを保持
    has adapters => (
        is       => 'ro',
        required => 1,
    );

    # 天気情報を取得(フォールバック付き)
    sub get_weather ($self, $city) {
        for my $adapter ($self->adapters->@*) {
            my $weather = $adapter->get_weather($city);
            return $weather if $weather;
        }
        return;  # すべて失敗
    }
}

たったこれだけです。シンプルな設計がFacadeの特徴です。

動作の流れ

  1. get_weatherが呼ばれると、adapters配列を順番に走査
  2. 各Adapterのget_weatherを呼び出し
  3. 成功(truthy な値が返る)したら、その結果を返して終了
  4. 失敗(undefやfalseが返る)したら、次のAdapterを試す
  5. すべて失敗したらundefを返す

フォールバック機能の動作確認

実際にフォールバックが動作することを確認しましょう。

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

# 天気サービスの共通インターフェース(Role)
package WeatherAdapter::Role {
    use v5.36;
    use Moo::Role;

    requires 'get_weather';
    requires 'name';

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        if ($weather) {
            say "$weather->{city}: $weather->{condition}(気温 $weather->{temperature}℃、湿度 $weather->{humidity}%)";
        }
        else {
            say "$city: データを取得できませんでした";
        }
    }
}

# OpenWeatherMap用Adapter
package WeatherAdapter::OpenWeatherMap {
    use v5.36;
    use Moo;

    has name => (is => 'ro', default => 'OpenWeatherMap');

    with 'WeatherAdapter::Role';

    sub _get_raw_data ($self, $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 => '曇り' }] },
            # Sapporoは意図的にデータなし(フォールバックをテスト)
        );
        return $mock_data{$city};
    }

    sub get_weather ($self, $city) {
        my $data = $self->_get_raw_data($city);
        return unless $data;
        return {
            city        => $data->{name},
            temperature => $data->{main}{temp},
            humidity    => $data->{main}{humidity},
            condition   => $data->{weather}[0]{description},
            source      => $self->name,
        };
    }
}

# WeatherStack用Adapter
package WeatherAdapter::WeatherStack {
    use v5.36;
    use Moo;

    has name => (is => 'ro', default => 'WeatherStack');

    with 'WeatherAdapter::Role';

    sub _get_raw_data ($self, $city) {
        my %mock_data = (
            'Tokyo'   => { location => { name => 'Tokyo' },   current => { temperature => 26, humidity => 58, weather_descriptions => ['Sunny'] } },
            'Sapporo' => { location => { name => 'Sapporo' }, current => { temperature => 19, humidity => 68, weather_descriptions => ['Rain'] } },
            # Osakaは意図的にデータなし
        );
        return $mock_data{$city};
    }

    sub get_weather ($self, $city) {
        my $data = $self->_get_raw_data($city);
        return unless $data;
        return {
            city        => $data->{location}{name},
            temperature => $data->{current}{temperature},
            humidity    => $data->{current}{humidity},
            condition   => $data->{current}{weather_descriptions}[0],
            source      => $self->name,
        };
    }
}

# Facade
package WeatherFacade {
    use v5.36;
    use Moo;

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

    sub get_weather ($self, $city) {
        for my $adapter ($self->adapters->@*) {
            my $weather = $adapter->get_weather($city);
            return $weather if $weather;
        }
        return;
    }
}

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

    say "=== 天気予報アグリゲーター(Facade版) ===";
    say "";

    # Facadeを作成(Adapterを渡す)
    my $facade = WeatherFacade->new(
        adapters => [
            WeatherAdapter::OpenWeatherMap->new,
            WeatherAdapter::WeatherStack->new,
        ],
    );

    # シンプルに使える!
    my @cities = qw(Tokyo Osaka Sapporo Fukuoka);

    for my $city (@cities) {
        my $weather = $facade->get_weather($city);
        if ($weather) {
            say "$city: $weather->{condition}(気温 $weather->{temperature}℃)[from: $weather->{source}]";
        }
        else {
            say "$city: データを取得できませんでした";
        }
    }
}

実行結果:

1
2
3
4
5
6
=== 天気予報アグリゲーター(Facade版) ===

Tokyo: 晴れ(気温 25.5℃)[from: OpenWeatherMap]
Osaka: 曇り(気温 27.2℃)[from: OpenWeatherMap]
Sapporo: Rain(気温 19℃)[from: WeatherStack]
Fukuoka: データを取得できませんでした

注目すべき点:

  • Tokyo: OpenWeatherMapから取得(最初に成功)
  • Osaka: OpenWeatherMapから取得(最初に成功)
  • Sapporo: OpenWeatherMapになかったので、WeatherStackから取得(フォールバック成功)
  • Fukuoka: どちらにもなかったので失敗

Facadeの威力: 呼び出し側の変化

Facade導入前と導入後で、呼び出し側のコードを比較してみましょう。

Before(Facadeなし)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Adapterを個別に作成・管理
my $owm = WeatherAdapter::OpenWeatherMap->new;
my $ws  = WeatherAdapter::WeatherStack->new;

# フォールバック処理を自分で実装
my $weather = $owm->get_weather($city);
if (!$weather) {
    $weather = $ws->get_weather($city);
}

# 表示
if ($weather) {
    say "$city: $weather->{condition}";
}

After(Facadeあり)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Facadeを1つ作成するだけ
my $facade = WeatherFacade->new(adapters => [...]);

# シンプルに呼び出し
my $weather = $facade->get_weather($city);

# 表示
if ($weather) {
    say "$city: $weather->{condition}";
}

呼び出し側の責任が大幅に軽減されました。

どのAPIから取得したか記録する

デバッグやログ出力のために、どのAPIから取得したかを記録する機能を追加しましょう。先ほどのコードでは、戻り値にsourceフィールドを追加していました。

1
2
3
4
5
6
7
return {
    city        => $data->{name},
    temperature => $data->{main}{temp},
    humidity    => $data->{main}{humidity},
    condition   => $data->{weather}[0]{description},
    source      => $self->name,  # ← どのAdapterから取得したか
};

これにより、フォールバックが発生したことを確認できます。

完成コード

すべてをまとめた完成コードです。

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

# 天気サービスの共通インターフェース(Role)
package WeatherAdapter::Role {
    use v5.36;
    use Moo::Role;

    requires 'get_weather';
    requires 'name';

    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        if ($weather) {
            say "$weather->{city}: $weather->{condition}(気温 $weather->{temperature}℃、湿度 $weather->{humidity}%)";
        }
        else {
            say "$city: データを取得できませんでした";
        }
    }
}

# OpenWeatherMap用Adapter
package WeatherAdapter::OpenWeatherMap {
    use v5.36;
    use Moo;

    has name => (is => 'ro', default => 'OpenWeatherMap');

    with 'WeatherAdapter::Role';

    sub _get_raw_data ($self, $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 => '曇り' }] },
            'Nagoya'  => { name => 'Nagoya',  main => { temp => 26.8, humidity => 62 }, weather => [{ description => '晴れ' }] },
        );
        return $mock_data{$city};
    }

    sub get_weather ($self, $city) {
        my $data = $self->_get_raw_data($city);
        return unless $data;
        return {
            city        => $data->{name},
            temperature => $data->{main}{temp},
            humidity    => $data->{main}{humidity},
            condition   => $data->{weather}[0]{description},
            source      => $self->name,
        };
    }
}

# WeatherStack用Adapter
package WeatherAdapter::WeatherStack {
    use v5.36;
    use Moo;

    has name => (is => 'ro', default => 'WeatherStack');

    with 'WeatherAdapter::Role';

    sub _get_raw_data ($self, $city) {
        my %mock_data = (
            'Tokyo'   => { location => { name => 'Tokyo' },   current => { temperature => 26, humidity => 58, weather_descriptions => ['Sunny'] } },
            'Sapporo' => { location => { name => 'Sapporo' }, current => { temperature => 19, humidity => 68, weather_descriptions => ['Rain'] } },
            'Sendai'  => { location => { name => 'Sendai' },  current => { temperature => 22, humidity => 65, weather_descriptions => ['Cloudy'] } },
        );
        return $mock_data{$city};
    }

    sub get_weather ($self, $city) {
        my $data = $self->_get_raw_data($city);
        return unless $data;
        return {
            city        => $data->{location}{name},
            temperature => $data->{current}{temperature},
            humidity    => $data->{current}{humidity},
            condition   => $data->{current}{weather_descriptions}[0],
            source      => $self->name,
        };
    }
}

# Facade: 複数のAdapterを統合管理
package WeatherFacade {
    use v5.36;
    use Moo;

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

    # 天気情報を取得(フォールバック付き)
    sub get_weather ($self, $city) {
        for my $adapter ($self->adapters->@*) {
            my $weather = $adapter->get_weather($city);
            return $weather if $weather;
        }
        return;
    }

    # 便利メソッド: 天気を整形表示
    sub show_weather ($self, $city) {
        my $weather = $self->get_weather($city);
        if ($weather) {
            say "$weather->{city}: $weather->{condition}(気温 $weather->{temperature}℃、湿度 $weather->{humidity}%)[via $weather->{source}]";
        }
        else {
            say "$city: データを取得できませんでした";
        }
    }
}

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

    say "=== 天気予報アグリゲーター(Facade版) ===";
    say "";

    # Facadeを作成
    my $facade = WeatherFacade->new(
        adapters => [
            WeatherAdapter::OpenWeatherMap->new,
            WeatherAdapter::WeatherStack->new,
        ],
    );

    # シンプルな呼び出し
    my @cities = qw(Tokyo Osaka Sapporo Nagoya Sendai Fukuoka);

    for my $city (@cities) {
        $facade->show_weather($city);
    }

    say "";
    say "--- 直接get_weatherを使う場合 ---";
    my $weather = $facade->get_weather('Sapporo');
    if ($weather) {
        say "Sapporo の天気データを $weather->{source} から取得しました";
    }
}

実行結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
=== 天気予報アグリゲーター(Facade版) ===

Tokyo: 晴れ(気温 25.5℃、湿度 60%)[via OpenWeatherMap]
Osaka: 曇り(気温 27.2℃、湿度 65%)[via OpenWeatherMap]
Sapporo: Rain(気温 19℃、湿度 68%)[via WeatherStack]
Nagoya: 晴れ(気温 26.8℃、湿度 62%)[via OpenWeatherMap]
Sendai: Cloudy(気温 22℃、湿度 65%)[via WeatherStack]
Fukuoka: データを取得できませんでした

--- 直接get_weatherを使う場合 ---
Sapporo の天気データを WeatherStack から取得しました

Facadeパターンのメリット

1. シンプルなインターフェース

利用者はWeatherFacadeだけを知っていれば良い。内部にどれだけのAdapterがあるかは気にする必要がありません。

2. 疎結合

呼び出し側とAdapter群が疎結合になります。Adapterの追加・削除がFacade内部で完結します。

3. 関心の分離

  • Adapter: 各APIの違いを吸収
  • Facade: 複数のAdapterを統合管理
  • 呼び出し側: 天気情報を使うだけ

それぞれの責任が明確に分離されています。

委譲(Delegation)について

Facadeパターンでは「委譲」という技術を使っています。

1
2
3
4
5
6
7
sub get_weather ($self, $city) {
    for my $adapter ($self->adapters->@*) {
        my $weather = $adapter->get_weather($city);  # ← 委譲
        return $weather if $weather;
    }
    return;
}

Facadeは自分で天気データを持っているわけではありません。Adapterに処理を「委譲」して、結果を受け取っているだけです。

これにより:

  • Facadeは「何を持っているか」ではなく「誰に頼むか」だけを管理
  • 新しいAdapterの追加が容易
  • テスト時にモックAdapterを注入可能

まとめ

今回は、Facadeパターンを使って複数のAdapterを統合管理しました。

  • WeatherFacadeクラスで複数のAdapterを一元管理
  • フォールバック機能により、失敗時に自動で次のAPIを試行
  • 呼び出し側は「天気を取得」とだけ言えばOK
  • 委譲を使って処理をAdapterに任せる

コードがさらにスッキリしました。

次回予告

基本的な機能は完成しましたが、まだ改善の余地があります。同じ都市の天気を何度も問い合わせるのは無駄です。次回は、キャッシュ機能を追加して、パフォーマンスを向上させます。お楽しみに!

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