Featured image of post コードドクター【Factory Method】癒着分離手術〜疎結合の誕生〜

コードドクター【Factory Method】癒着分離手術〜疎結合の誕生〜

BtoB SaaSのデータ連携基盤で頻発する障害。if-else分岐の増殖が招いた「癒着」に、コードドクターのFactory Method手術が挑む。Moo×Perlで学ぶ疎結合設計。

深夜2時。Slackの通知音が鳴り止まない。

僕は画面を見つめながら、頭を抱えていた。また連携系の障害だ。kintone連携モジュールのリリース後、なぜかSalesforce連携まで動かなくなっている。

「なんでだ……全く関係ないはずなのに」

スタートアップの技術責任者として3年。顧客が増えるたびに外部システムとの連携先も増えていった。Salesforce、kintone、Slack、独自API——それぞれの接続コードをメインロジックに直接書いてきた。

「動けばいい」。そう思っていた。思っていたんだ、最初は。

でも今は違う。新しい連携先を追加するたびに、どこかが壊れる。テストは全部やり直し。コードレビューも地獄。若手エンジニアは「また連携系ですか……」と目を伏せる。

責任は僕にある。わかってる。でも今さら設計からやり直す余裕なんて——

その時、オフィスのドアが静かに開いた。

往診

深夜のオフィスに現れたドクターとナナコ。黒い往診鞄を持つ長身の男と、パステルカラーの白衣を着た若い女性が、暗いオフィスの入口に立っている

振り返ると、黒い往診鞄を持った長身の男と、パステルカラーの白衣を着た若い女性が立っていた。深夜のオフィスに、どうやって入ってきたんだ。

「どちら様……ですか?」

白衣の方が微笑んだ。

「大丈夫ですよ、ここはコード診療所です……あ、いえ、往診ですね。私はナナコ、こちらがドクターです」

「往診? 呼んでませんけど」

男——ドクターと呼ばれた人物は、僕の背後のモニターを一瞥した。

「……コードの、悲鳴」

意味がわからなかった。

触診

ドクターは無言で僕の席に近づき、IDEを開いた。DataSyncService.pm——連携処理の中核モジュールだ。

スクロールが始まる。if、elsif、elsif、elsif……。僕の3年分の判断が、条件分岐として積み重なっている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
sub sync_data($self, $target, $data) {
    if ($target eq 'salesforce') {
        my $client = SalesforceClient->new(
            api_key  => $ENV{SF_API_KEY},
            endpoint => $ENV{SF_ENDPOINT},
        );
        $client->push_records($data);
    }
    elsif ($target eq 'kintone') {
        my $client = KintoneClient->new(
            token  => $ENV{KINTONE_TOKEN},
            app_id => $ENV{KINTONE_APP_ID},
        );
        $client->upsert($data);
    }
    elsif ($target eq 'slack') {
        # ...以下続く
    }
    # 新規連携先を追加するたびにここを修正
}

ドクターの指がスクロールを止めた。

「癒着」

一言だった。

「え?」

ナナコさんが穏やかに、しかし確信を持った声で補足した。

「先生がおっしゃっているのは、生成ロジックと利用ロジックが同じ場所に癒着している状態ですね。臓器と神経が絡まり合っていて、一つを動かすと全身が痙攣するようなもの——かなり深刻な状態だと、先生は嘆いていらっしゃいます」

僕は思わず声を上げた。

「それ! まさにそれです! kintoneを触っただけでSalesforceが壊れたのは——」

「癒着分離。必要」

ドクターは既にキーボードに手を伸ばしていた。

診断

ナナコがホワイトボードに症状を書き出しながら説明するシーン。傍らでドクターがモニターを見つめている

ナナコさんがモニターを指差した。

「症状を整理しますね。このコードの問題点は3つあります」

ナナコさんはホワイトボードに書き始めた(いつ用意したんだ、そのホワイトボード)。

  1. 生成と利用の混在:クライアントの new とデータ同期のロジックが同じ場所にある
  2. 変更の波及:新規連携先の追加で、メインロジックへの手術が毎回必要
  3. テストの困難:条件分岐をすべて網羅しないとテストできない

「これを医療用語で言うと……」

「癒着性創傷症候群」

ドクターが呟いた。

「え、なんですかそれ」

ナナコさんが、深刻さを噛みしめるように補足した。

「つまり、何かを作る処理と使う処理がべったり癒着していて、あらゆる変更が全身に波及してしまう状態です。先生の表情を見る限り、これはかなりの重症ですよ。処置としては Factory Method による癒着分離手術を提案します」

Factory Method。名前は聞いたことがある。でも実際に使ったことは……。

処方箋

ドクターが黙々とコードを書き始めた。驚くべき速度だ。

ナナコさんが横で解説してくれた。

「まず、すべての接続先クライアントが実装すべき共通インターフェースを定義します」

1
2
3
4
5
6
package Role::SyncClient {
    use v5.36;
    use Moo::Role;
    
    requires 'sync';
}

sync メソッドを持つことが絶対条件——ドクター流に言えば 必須の生体反応 ですね。これさえ守れば、どの接続先でも同じ方法で呼び出せます」

次にドクターは個別のクライアントを作り始めた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package SyncClient::Salesforce {
    use v5.36;
    use Moo;
    with 'Role::SyncClient';
    
    has api_key  => (is => 'ro', required => 1);
    has endpoint => (is => 'ro', required => 1);
    
    sub sync($self, $data) {
        # Salesforce固有の同期処理
        ...
    }
}

「Kintone も Slack も同じ構造ですよ。それぞれが Role::SyncClientwith して、sync メソッドを実装するんです。臓器の規格を統一するイメージですね」

僕は黙って見ていた。今まで if-else で無理やり押し込んでいたものが、きれいに分離されていく。

外科手術

そしてドクターは「工場」を作り始めた。

 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
package SyncClientFactory {
    use v5.36;
    use Moo;
    
    my $configs = {
        salesforce => sub {
            return {
                api_key  => $ENV{SF_API_KEY}  // 'demo_key',
                endpoint => $ENV{SF_ENDPOINT} // 'https://api.salesforce.com',
            };
        },
        kintone => sub {
            return {
                token  => $ENV{KINTONE_TOKEN}  // 'demo_token',
                app_id => $ENV{KINTONE_APP_ID} // '123',
            };
        },
        slack => sub {
            return {
                webhook_url => $ENV{SLACK_WEBHOOK} // 'https://hooks.slack.com/demo',
            };
        },
    };
    
    sub create($self, $target) {
        my $config_fn = $configs->{$target}
            or die "Unknown target: $target";
        
        my $class = 'SyncClient::' . ucfirst($target);
        my $file = $class =~ s{::}{/}gr . '.pm';
        require $file;
        
        return $class->new($config_fn->()->%*);
    }
}

「これが Factory Method の核心ですよ」とナナコさんが目を輝かせた。「どのクライアントを作るかという判断を、この『工場』に閉じ込めたんです。先生、この手際……何度見ても惚れ惚れしますね」

最後に、DataSyncService がこう変わった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package DataSyncService {
    use v5.36;
    use Moo;
    use SyncClientFactory;

    has factory => (
        is      => 'ro',
        default => sub { SyncClientFactory->new },
    );

    sub sync_data($self, $target, $data) {
        my $client = $self->factory->create($target);
        $client->sync($data);
    }
}

僕は思わず声を上げた。

「え……あの100行以上あった if-else が……たった3行に?」

ドクターは静かに頷いた。

「分離完了」

術後経過

ドクターが小さなメモをデスクに置き、ドアに向かう背中。ナナコが患者に微笑みかけている

テストを実行した。すべてグリーン。

「新しい連携先を追加したい場合はどうすれば……?」

ナナコさんが答えた。

SyncClient::NewTarget.pm を作成して、SyncClientFactory の設定に1行追加するだけです。DataSyncService には一切触れません」

僕は改めてコードを眺めた。メインロジックは「何を同期するか」だけに集中している。どのクライアントを使うかは工場任せ。

「これなら……既存のテストに影響が出ない……」

ドクターが鞄から小さなメモを取り出し、僕のデスクに置いた。

——これは、連絡先だろうか。また困った時に相談していい、という意味だろうか。僕は思わずメモを受け取る手に力が入った。

「それ、設計パターンの参考書籍リストですよ。先生のおすすめだそうです」

ナナコさんの声で我に返った。ナナコさんはいたずらっぽく微笑んでいるように見えた。

「あ、ああ……そうですよね」

顔が熱い。何を期待していたんだ、僕は。

ドクターはすでにドアに向かっていた。

「リハビリ……自力」

「今後は新しい接続先が増えても、工場に任せてくださいね」とナナコさんが振り返って言った。「メインロジックは、もう臓器移植のリスクを背負わなくて済みますよ。安心してください」

「ありがとうございます……!」

ドアが閉まった。

僕は新しいコードを眺めていた。明日からの開発がずっと楽になる——そんな確信があった。そして今度連携先が増えたら、真っ先に工場クラスに向かおう。もう二度と、あの if-else の地獄には戻らない。


処方箋まとめ

症状適用すべき経過観察
オブジェクト生成と利用ロジックが混在している
新規種類の追加でメインロジックの修正が必要
条件分岐(if-else/switch)が増殖している
生成するオブジェクトの種類が固定されている
単純な生成処理で拡張予定がない

治療のステップ

  1. 共通インターフェース(Role)の定義Role::SyncClient で必須メソッドを宣言
  2. 具象クラスの実装 — 各連携先ごとに SyncClient::* を作成し Role を適用
  3. Factory クラスの作成 — 生成判断と設定を一箇所に集約
  4. 利用側の浄化 — メインロジックから条件分岐を削除し、Factory 経由でインスタンス取得

助手より

新しい連携先が増えても、もう本体に手を入れる必要はありませんよ。工場に新しいレシピを教えるだけで、システムは健やかに成長していきます。

先生は無口ですが、あのメモの書籍リスト、かなり厳選されたものなんです。お時間のある時に、ぜひ読んでみてくださいね。きっと、次に先生にお会いする頃には、もう患者さんではなく同僚として話せるようになっていると思いますよ。

——ナナコ

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