Featured image of post コード探偵ロックの事件簿【Adapter】互換性なきシステムの壁〜探偵は通訳に頼らない〜

コード探偵ロックの事件簿【Adapter】互換性なきシステムの壁〜探偵は通訳に頼らない〜

新旧システムのAPI連携で発生するif文地獄。互換性のないインターフェースを繋ぐ「Adapterパターン」を、コード探偵ロックが鮮やかに解説します。

雑居ビルの一室、むせ返るような排熱とエナジードリンクの甘ったるい匂いが漂うその部屋のドアに、「レガシー・コード・インベスティゲーション(LCI)」という怪しげな看板が掛かっている。

「違うシステムのAPI同士を繋ぐコードを書いたんですが、仕様変更のたびにif文が増えて、もうわけがわかりません! ChatGPTに直させようとしたら文字数制限で途切れて……」

私はノートPCを抱え、文字通りすがる思いでこの奇妙な探偵事務所に駆け込んだ。私はコピペと最新AIツールを駆使してなんとか現場を生き抜いてきた新人プログラマだ。つい先日、自社の新しいシステムと、買収先企業の古いシステムを連携させるという無茶振りをされた結果、完全に泥沼にハマっていた。

部屋の奥から、年代物のメカニカルキーボードをけたたましく叩く音が止まった。

「AIに頼る前に、美しい設計という名の真実に目を向けたまえ、ワトソン君。初歩的なにおいだよ」

アンティーク調の椅子でふんぞり返るその男――自称「コード探偵」のロックは、気取った仕草でコーヒーカップを持ち上げた。

(ワトソン? 誰のことだろ……まあいいか)「はい、じゃあとりあえずこれ見てください!」

私は構わずPCを開き、問題のコードを見せた。

現場検証:互換性なき泥沼のAPI

「ご覧の通りです。新しいシステムの UserService と、古いシステムの LegacyCustomerSystem があるんですが……」

【Before】問題の呼び出し元コード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use strict;
use warnings;
use lib '.';
use UserService;
use LegacyCustomerSystem;

# Client code with Incompatible Interfaces Anti-pattern
sub get_normalized_user {
    my ($system_type, $id, $system_instance) = @_;
    
    if ($system_type eq 'new') {
        return $system_instance->get_user_info($id);
    } elsif ($system_type eq 'legacy') {
        # ここで無理やりデータ構造を変換している
        my $raw_data = $system_instance->fetch_customer_data($id);
        return {
            id    => $raw_data->{customer_id},
            name  => $raw_data->{full_name},
            email => $raw_data->{mail_address},
        };
    }
    die "Unknown system type: $system_type";
}

「新しいシステムは『User』、古いシステムは『Customer』って呼んでいて、データを取得するメソッドも get_user_infofetch_customer_data でバラバラなんです。だから使う側で if 文を書いて、無理やりデータ構造を揃えてるんですが……」

ロックは顔をしかめ、大きく息を吐いた。

「言語も文化も違う者同士を、無理やり同じ部屋に押し込めて会話させようとしている。これではいずれ暴動が起きる(バグが出る)のは当然だ」 「えっ、暴動? いや、DeepLのAPIとか繋げば自動翻訳できるんじゃないですか?」

私の現代的な提案に、探偵は冷ややかな視線を送ってきた。

「君は、プログラムの構造的欠陥を外部サービスで誤魔化そうというのかい? このコードの真犯人はシステムの違いではない。**『互換性のないインターフェース(Incompatible Interfaces)』**をそのまま使おうとしているクライアントコードの怠慢だ」

推理披露:探偵は「通訳(Adapter)」を雇う

探偵は私の手からPCを奪い取ると、ターミナルを開いておもむろに新しいファイルを作り始めた。

「レガシーな彼に、直接我々の言葉を喋らせるのではない。彼の言葉を我々の言葉に翻訳する『通訳』を間に入れるのだ」

【After】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
package LegacyCustomerAdapter;
use Moo;

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

# 新しいシステム(UserService)と同じインターフェースを提供する
sub get_user_info {
    my ($self, $id) = @_;
    
    # 内部で古いシステムに処理を委譲する
    my $raw_data = $self->legacy_system->fetch_customer_data($id);
    
    # 呼び出し元が期待するデータ構造に変換して返す
    return {
        id    => $raw_data->{customer_id},
        name  => $raw_data->{full_name},
        email => $raw_data->{mail_address},
    };
}

1;

探偵は得意げにエンターキーを叩いた。

「これがAdapterパターンだ。古いシステム(Adaptee)をこの通訳クラス(Adapter)で包み込むことで、外から見れば新しいシステムと全く同じように振る舞わせることができる」 「なるほど! ただのラッパークラスですね! 最初からそう言ってくださいよ!」

私が簡潔に要約すると、探偵は少し不服そうな顔をした。

「……身も蓋もない言い方だが、まあ間違ってはいない。これによって、呼び出し元(クライアント)のコードは美しく生まれ変わるのだよ」

【After】リファクタリングされた呼び出し元コード

 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
use strict;
use warnings;
use Test::More;
use lib '.';
use UserService;
use LegacyCustomerSystem;
use LegacyCustomerAdapter;

# もうシステムの違いを気にする必要はない
sub fetch_data {
    my ($service, $id) = @_;
    return $service->get_user_info($id);
}

my $new_system = UserService->new();
my $legacy_system = LegacyCustomerSystem->new();

# 古いシステムをAdapter(通訳)で包む
my $adapter = LegacyCustomerAdapter->new(legacy_system => $legacy_system);

# 新しいシステムも、Adapter経由の古いシステムも、全く同じように扱える!
my $user1 = fetch_data($new_system, 1);
is($user1->{name}, 'User 1', 'New system User 1');
is($user1->{email}, 'user1@example.com', 'New system User 1 email');

my $user2 = fetch_data($adapter, 2);
is($user2->{name}, 'Customer 2', 'Legacy system Customer 2 through adapter');
is($user2->{email}, 'customer2@legacy.local', 'Legacy system Customer 2 email through adapter');

done_testing;

解決:シームレスな対話

「おお……あの忌まわしい $system_type による if 文の分岐が跡形もなく消え去っている……!」

私は画面を見て思わず声を上げた。呼び出し元の fetch_data 側は、引数で渡されたのが新しいシステムなのか古いシステムなのかをまったく知る必要がない。ただ get_user_info を呼び出すだけでいいのだ。

1
2
3
4
5
6
$ perl test.pl
ok 1 - New system User 1
ok 2 - New system User 1 email
ok 3 - Legacy system Customer 2 through adapter
ok 4 - Legacy system Customer 2 email through adapter
1..4

テストコードもすべて緑色に点灯し、完璧な動作を証明していた。

「見事だ。これでシステム間の国境はなくなった。新旧のコードが平和なテーブルについて対話できるようになったのだよ、ワトソン君」

ロックは満足そうにコーヒーをすする。「通訳」という比喩は少し大げさな気もするが、確かにコードの見通しは驚くほど良くなっていた。

「ありがとうございます! これなら僕にもできます! じゃあ、このAdapterクラスのコードをベースにして、別のシステムの分もサクッとコピペで作ってみますね! AIに変換させれば一瞬だし!」 私は意気揚々とPCを閉じ、事務所を飛び出そうとした。

「ちょっと待て!! 全くわかっていないじゃないか! コピペで増やすなら結局クラス爆発が……おい、話を聞きたまえ!」

背後で探偵の叫び声が響いたが、私の足取りは羽のように軽かった。


探偵の調査報告書

容疑(アンチパターン)真実(パターン)証拠(効果)
互換性のないインターフェース(Incompatible Interfaces)。統合すべき複数のクラスがそれぞれ異なるメソッド名やデータ構造を持っており、呼び出し元で if 文による分岐と手動のデータ変換を強いられている状態。Adapter パターン。既存クラスを内部に保持するラッパークラス(Adapter)を作成し、クライアントが期待する統一されたインターフェースを提供する設計方式。呼び出し元からシステム固有の分岐と変換ロジックが完全に排除され、新旧どちらのシステムも同一のメソッドで透過的に利用可能になった。新たな外部システムの追加時もクライアントコードの変更が不要となり、Open-Closed Principle(開放閉鎖の原則)に準拠した拡張が可能になった。

推理のステップ

  1. インターフェースの違いを特定する: 統合したい既存クラス群が、それぞれどのようなメソッド名やデータ構造を要求しているかを洗い出します。
  2. 期待される標準(Target)を定義する: クライアント(呼び出し元)が本来使いたい、統一されたメソッドの形を決定します(今回の例では get_user_info)。
  3. Adapterクラスを作成する: 既存のクラスをメンバ変数として保持(カプセル化)し、Targetと同じインターフェースを持つAdapterクラスを作ります。
  4. 処理を委譲(Delegate)する: Adapterクラスのメソッド内で、既存クラスのメソッドを呼び出し、必要に応じてデータ構造の変換(翻訳)を行います。
  5. クライアントから利用する: クライアントはAdapterを経由して既存クラスにアクセスするため、内部の違いを意識せずに統一的に処理できます。

ロックより

既存の堅牢な(あるいは手を触れたくないほど脆い)レガシーコードに新しい要件を組み込む際、直接コードを修正するのは無謀だ。だからと言って、呼び出し元に if 文を散らかせば、やがてその部屋はガラクタの山になる。

Adapterは、相手に変わることを強要せず、我々が相手の言葉を理解するための優雅なクッションなのだよ。既存の資産を活かしつつ、新しい世界の秩序に従わせる。これこそがソフトウェア工学における平和外交というものさ。

君も次からは、安易なコピペや「AI通訳」に頼る前に、美しい中継ぎ役について考えてみてくれたまえ。

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