目次を表示する

コードの考古学 ── 設計思想に宿る哲学の源流

第7話 洞窟の影 ── プラトンのイデアがインターフェースを先行させた

場面

紀元前380年頃のアテネ。ソクラテスが死んで19年が経ち、プラトンはアカデメイアで弟子たちに問いかけていた。

「地下の洞窟を想像しなさい。生まれたときから鎖で縛られた囚人たちが、壁だけを見つめて生きている。彼らの背後には火が燃えており、その前を人々が像を持って通り過ぎていく。囚人たちが見るのは、壁に映る影だけだ。彼らにとって、その影こそが現実だ。」

プラトンはここで止まらなかった。一人の囚人が鎖を解かれて振り返る。眩しさに目をしかめながら、実物の像を初めて見る。そして洞窟の外に出て、太陽の光の下で本物の世界を見る。彼はいま気づく——洞窟の影は実在ではなかった。実在するものが壁に投影されたに過ぎなかった、と。

この寓話の背後にプラトンの哲学の核心がある。イデア論(Forms / Eidos)だ。

私たちが目にする個々の馬は、いずれも特定の馬だ。色があり、年齢があり、傷がある。いつか死ぬ。しかしプラトンに言わせれば、それらはすべて「馬のイデア(Form of Horse)」という永遠の雛形の不完全な模倣に過ぎない。「馬のイデア」は、実在する馬を観察して頭の中で作った抽象概念ではない。それは論理的に先行して実在し、個々の馬がそこから派生してくる。

これは直感に反する。私たちは経験から抽象を作ると思っている。たくさんの馬を見て「馬とはこういうものだ」と学ぶ、と。しかしプラトンは逆を主張した。イデアの方が先で、個物はその影だ。普遍が特殊に先行する。抽象が具体より根本的だ。

洞窟の比喩はこの関係を視覚化している。囚人(私たち)が見る影(個物・具体的なもの)は、実物の像(イデア)が火(存在の源)に照らされて投影されたものだ。影を研究しても本質には近づけない。本質は源にある。

答え

抽象は具体の後に来るのではなく、論理的に先行する。「何であるべきか」の定義(イデア・インターフェース)が先にあり、具体的な実装はその影として後から来る。設計において「何をすべきか」の仕様が「どうやるか」の実装より根本的だ。

CSへの翻訳

1994年。エーリヒ・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシデス——通称「Gang of Four(GoF)」——が『Design Patterns』を出版した。その中核にある格言はプラトン的だ。「インターフェースに対してプログラムせよ、実装に対してではなく(Program to an interface, not an implementation)」。インターフェース(Form)に対してコードを書け。具体的なクラス(影)に依存するな。

2年後の1996年、ロバート・C・マーティン(通称 “Uncle Bob”)がSOLID原則の「D」を定式化した。依存性逆転原則(Dependency Inversion Principle, DIP)だ。「上位モジュールは下位モジュールに依存してはならない。両者とも抽象に依存すべきだ。」

これはプラトンの転倒そのものだ。

直感的な依存の方向:ビジネスロジック(上位)がデータベース実装(下位)に依存する。「ビジネスロジックはPostgreSQLを使う」。 DIPの転倒:ビジネスロジックがインターフェースを定義する。PostgreSQLの実装がそのインターフェースを実装する。ビジネスロジックは影(具体的DB)を見ず、イデア(インターフェース)だけを見る。

インターフェース分離原則(Interface Segregation Principle, ISP)もプラトン的な整合性を持つ。イデアはその利用者に必要な本質だけを含むべきだ。関係のない能力を詰め込んだ「太ったイデア」は、純粋なFormではなく、何かと何かが混じった混沌だ。

さらに2005年、アリスター・コバーンが「ヘキサゴナルアーキテクチャ(Ports & Adapters)」を発表した。アプリケーションのコアがポート(インターフェース)を定義する。外部システム(データベース、APIクライアント、UIフレームワーク)はアダプター(インターフェースの実装)を提供する。洞窟の比喩そのままだ。コア(囚人)はイデア(インターフェース)の世界に生き、外の世界との接続は影のシステム(アダプター)が担う。

設計への示唆

プラトン的な設計の実践で最も直接的なのはTDD(テスト駆動開発)だ。テストを先に書くとは、インターフェース(Form)を先に定義することだ。「このモジュールはこういう問いに、こういう答えを返すべきだ」——これがテストであり、イデアだ。その後に書く実装コードは、そのテスト(Form)を満たす影に過ぎない。

依存の方向を設計するとき、問いはこうなる。


❌ 影から始める(実装優先)

// まず SendGrid クライアントを使う実装クラスを書く
class SendGridNotificationService {
  async notify(userId: string, message: string): Promise<void> {
    await sendGridClient.sendEmail({ to: userId, body: message });
  }
}

// ユースケースが具体的な実装に依存する
class WelcomeUserUseCase {
  constructor(private notifier: SendGridNotificationService) {} // 影に依存
}

WelcomeUserUseCase は SendGrid を知っている。テストで実際のメールが飛ぶか、複雑なモックが必要になる。SendGrid を別のサービスに切り替えればユースケースも変わる。洞窟の囚人が影と鎖でつながっている状態だ。


✅ Formから始める(インターフェース優先)

// まずインターフェース(Form)を定義する
interface NotificationPort {
  notify(userId: string, message: string): Promise<void>;
}

// ユースケースはFormだけを知る
class WelcomeUserUseCase {
  constructor(private notifier: NotificationPort) {} // Formに依存

  async execute(userId: string): Promise<void> {
    const message = `ようこそ!アカウント登録が完了しました。`;
    await this.notifier.notify(userId, message);
  }
}

// テスト用スタブ:メールは飛ばない
class InMemoryNotification implements NotificationPort {
  public sent: { userId: string; message: string }[] = [];
  async notify(userId: string, message: string) {
    this.sent.push({ userId, message });
  }
}

// 本番用 SendGrid 版
class SendGridNotification implements NotificationPort { /* ... */ }
// Slack 通知版(新要件でも追加するだけ)
class SlackNotification implements NotificationPort { /* ... */ }

ユースケースは NotificationPort というFormを見ている。通知の実装が SendGrid であろうと Slack であろうと、ユースケースは一行も変わらない。テストには InMemoryNotification を使えるから、メール送信サーバーなしに高速に動かせる。


実用上の最大の利益は、ビジネスロジックのテストがインフラを必要としなくなることだ。洞窟の比喩の囚人がどの影を見ていようと、ビジネスの本質(ウェルカムメッセージを送るロジック)は変わらない。それが依存性逆転の実在感だ。

graph TB
    subgraph "❌ 伝統的な依存方向(影が現実)"
        UC1[WelcomeUserUseCase] -->|依存| SG[SendGridNotificationService<br/>具体的な実装]
        SG -->|直結| API[SendGrid API]
        note1[テストに本物のAPIが必要\n実装変更でユースケースが壊れる]
    end

    subgraph "✅ 依存性逆転(Formが現実)"
        UC2[WelcomeUserUseCase] -->|依存| IF[NotificationPort<br/>interface]
        SG2[SendGridNotification] -.->|implements| IF
        MEM[InMemoryNotification] -.->|implements| IF
        SL[SlackNotification] -.->|implements| IF
        UC2 -.->|テスト時| MEM
        UC2 -.->|本番時| SG2
        note2[ユースケースはFormだけを知る\n実装は何でも差し替え可能]
    end

    style IF fill:#fffacd,stroke:#d4a017
    style UC2 fill:#e8f5e9
    style note2 fill:#e8f5e9
    style note1 fill:#ffebee

プラトンが洞窟で語ったのは認識論の話だったが、GoFとマーティンとコバーンはそれをソフトウェア設計に変換した。インターフェースはイデアだ。具体的なクラスは影だ。囚人(ビジネスロジック)は太陽(ドメインの真実)を直接見る必要はない——正確な影(インターフェース)を通じて、ドメインの本質を操作できる。

プラトンにはもう一つ重要な概念がある。**想起(アナムネーシス)**だ。「学びとは新しい知識を獲得することではなく、魂が忘れていたイデアを思い出すことだ」とプラトンは『メノン』で語った。経験豊富な設計者がいい抽象化を「思いつく」とき、彼らはしばしば「これは前から存在していた気がする」と感じる。それは錯覚ではない——ドメインのイデアは、ソフトウェアを書く前から論理的に存在していた。設計者がやっているのは創造ではなく、発見だ。

このプラトン的「方向の転倒」(具体に先行する抽象、影が依存するイデア)は、第10話のカントによってさらに鋭く定式化される。カントは認識論で「対象が心に従う」というコペルニクス的転回を起こす。プラトンの「個物がイデアに従う」は、その先駆だ。第10話を読むと、ヘキサゴナルアーキテクチャや TDD が同じ「方向の転倒」の異なる表現であることが見えてくる。

設計するときの問いをひとつ変えるだけでいい。「このクラスを書こう、それからインターフェースを引き出せる」ではなく、「このユースケースが必要とするのは何か、それをインターフェースにしよう」。影から入るのをやめ、Formから入る。それだけで依存の矢印が逆転し、テストが軽くなり、将来の変更が局所化される。


問い:今あなたが設計しているクラスは、インターフェース(Form)から始まっているか、それとも実装(影)から始まっているか。後者なら、洞窟の中にいるということだ。