T-CREATOR

Nano Banana で拡張可能なモジュール設計:プラグイン/アダプタ/ポート&ドライバ

Nano Banana で拡張可能なモジュール設計:プラグイン/アダプタ/ポート&ドライバ

Nano Banana(Gemini 2.5 Flash Image)は、Adobe Photoshop、Leonardo.ai、Figma、そして様々な API プロバイダーを通じて利用できる画像生成 AI です。このような広範な統合を実現するには、拡張可能なモジュール設計が不可欠でした。

本記事では、Nano Banana のような AI サービスを既存システムに統合する際に役立つ、3 つの重要な設計パターン「プラグイン」「アダプタ」「ポート&ドライバ(ヘキサゴナルアーキテクチャ)」について、実践的なコード例とともに解説します。これらのパターンを理解することで、保守性が高く、拡張しやすいシステムを構築できるようになるでしょう。

背景

AI サービス統合の現状

2025 年現在、Nano Banana のような高性能な AI サービスは、様々なプラットフォームで利用できるようになっています。しかし、これらのサービスを自社のアプリケーションに統合する際には、多くの技術的課題が存在します。

以下の表は、Nano Banana が統合されている主要なプラットフォームをまとめたものです。

#プラットフォーム統合方法利用形態
1Google AI Studio直接アクセスWeb UI での操作
2Vertex AIGoogle Cloud APIエンタープライズ向け API
3Adobe Photoshopプラグイン統合デスクトップアプリ内での利用
4Leonardo.aiプラットフォーム統合Web サービス経由
5OpenRouterAPI プロキシ統一 API 経由でのアクセス
6CometAPIマルチモデル API複数 AI モデルへの単一インターフェース

これだけ多様な統合が実現できているのは、適切なアーキテクチャパターンが採用されているからです。

モジュール設計の重要性

現代のソフトウェア開発では、単一のモノリシックなシステムではなく、独立したモジュールを組み合わせる設計が主流になっています。

モジュール設計のメリットは以下の通りです。

保守性の向上

各モジュールが独立しているため、1 つのモジュールの変更が他のモジュールに影響を与えにくくなります。Nano Banana の API 仕様が変更されても、アダプタ層のみを修正すれば良いのです。

テストの容易さ

モジュールごとに独立してテストできるため、品質保証がしやすくなります。実際の Nano Banana API を呼び出さなくても、モック(テスト用のダミー)を使ってテストできるでしょう。

チーム開発の効率化

各モジュールを異なるチームメンバーが担当できるため、並行開発が可能になります。フロントエンドチームは UI を、バックエンドチームは API 統合を、それぞれ独立して開発できます。

ソフトウェアアーキテクチャパターンの進化

ソフトウェアアーキテクチャは、長年の開発経験から生まれたベストプラクティスの集積です。

以下の図は、アーキテクチャパターンの進化を示しています。

mermaidflowchart LR
  era1["モノリシック<br/>1つの巨大システム"] -->|課題| issue1["保守困難<br/>変更の影響範囲が広い"]
  issue1 -->|改善| era2["レイヤードアーキテクチャ<br/>層で分離"]
  era2 -->|課題| issue2["層間の密結合<br/>ビジネスロジックの再利用困難"]
  issue2 -->|改善| era3["プラグイン/アダプタ/<br/>ポート&ドライバ"]

  era3 --> benefit1["疎結合"]
  era3 --> benefit2["テスタビリティ"]
  era3 --> benefit3["拡張性"]

  style era3 fill:#90EE90
  style issue1 fill:#ffcccc
  style issue2 fill:#ffcccc

図で理解できる要点:

  • モノリシックな設計は変更の影響範囲が広く、保守が困難
  • レイヤード(層状)アーキテクチャでは層間の密結合が問題になる
  • プラグイン/アダプタ/ポート&ドライバパターンにより疎結合を実現

課題

AI サービス統合における 3 つの主要課題

Nano Banana のような外部 AI サービスを直接システムに組み込むと、以下のような問題が発生します。

1. API 仕様変更への脆弱性

外部サービスの API 仕様は、予告なく変更されることがあります。

typescript// 問題のある実装例:API呼び出しがアプリケーション全体に散在
class ProductImageGenerator {
  async generateImage(prompt: string) {
    // Nano Banana APIを直接呼び出し
    const response = await fetch(
      'https://api.google.com/nano-banana/v1/generate',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.API_KEY}`,
        },
        body: JSON.stringify({ prompt }),
      }
    );

    return response.json();
  }
}
typescript// 別のクラスでも同じAPI呼び出し
class UserAvatarGenerator {
  async createAvatar(description: string) {
    // 同じAPIを別の場所でも呼び出し(重複コード)
    const response = await fetch(
      'https://api.google.com/nano-banana/v1/generate',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.API_KEY}`,
        },
        body: JSON.stringify({ prompt: description }),
      }
    );

    return response.json();
  }
}

このコードの問題点は、API エンドポイントが変更されると、すべてのクラスを修正する必要がある点です。修正箇所が多いほど、バグが混入するリスクも高まります。

2. テストの困難さ

外部 API に直接依存すると、ユニットテストが困難になります。

javascript// テストしにくいコード
describe('ProductImageGenerator', () => {
  it('should generate product image', async () => {
    const generator = new ProductImageGenerator();

    // 実際のAPI呼び出しが発生してしまう
    // - テストが遅い(ネットワーク通信が発生)
    // - APIの利用料金が発生
    // - ネットワークエラーでテストが失敗する可能性
    const result = await generator.generateImage(
      '赤いワンピース'
    );

    expect(result).toBeDefined();
  });
});

実際の API を呼び出すテストには、以下の問題があります。

#問題点影響
1テスト実行が遅い開発速度の低下、CI/CD パイプラインの遅延
2API 利用料金が発生テストのたびにコストがかかる
3ネットワーク依存オフライン環境でテストできない
4レート制限に引っかかるテストが途中で失敗する
5予測不可能な結果AI の出力は毎回異なるため検証が困難

3. サービスの切り替えが困難

Nano Banana から別の画像生成サービス(例:OpenAI の DALL-E、Stability AI)に切り替えたい場合、大規模な書き直しが必要になります。

javascript// Nano Bananaに強く依存したコード
async function generateProductImages(products) {
  const results = [];

  for (const product of products) {
    // Nano Banana特有のパラメータ
    const response = await nanoBananaAPI.generate({
      prompt: product.description,
      model: 'gemini-2.5-flash-image', // Nano Banana固有
      consistencyMode: 'character', // Nano Banana固有
      outputFormat: 'google-format', // Nano Banana固有
    });

    results.push(response);
  }

  return results;
}

このコードでは、Nano Banana 固有のパラメータが使われているため、別のサービスに切り替える際には全面的な書き直しが必要です。

密結合がもたらす技術的負債

これらの課題の根本原因は「密結合」です。

以下の図は、密結合なアーキテクチャの問題点を示しています。

mermaidflowchart TB
  ui["UI Layer<br/>ユーザーインターフェース"]
  business["Business Logic<br/>ビジネスロジック"]
  nano1["Nano Banana API<br/>直接呼び出し"]
  nano2["Nano Banana API<br/>直接呼び出し"]
  nano3["Nano Banana API<br/>直接呼び出し"]

  ui -->|直接依存| nano1
  business -->|直接依存| nano2
  ui -->|直接依存| nano3

  nano1 -.->|仕様変更| change["API仕様変更"]
  nano2 -.->|仕様変更| change
  nano3 -.->|仕様変更| change

  change -->|影響| impact["すべての呼び出し箇所を<br/>修正する必要がある"]

  style change fill:#ff6b6b
  style impact fill:#ffcccc

図で理解できる要点:

  • 複数の箇所から直接 API を呼び出している
  • API 仕様変更の影響がアプリケーション全体に広がる
  • 修正箇所が多く、バグ混入のリスクが高い

このような密結合を解消するために、次のセクションで紹介する 3 つの設計パターンが有効です。

解決策

3 つの設計パターンによる疎結合の実現

Nano Banana のような外部サービスを統合する際に有効な、3 つの設計パターンを紹介します。

パターン 1:プラグインパターン

プラグインパターンは、システムの機能を動的に追加・削除できる設計手法です。

コンセプト

コアシステムは安定したインターフェースを提供し、プラグインがその仕様に従って機能を実装します。Adobe Photoshop に Nano Banana が統合されているのも、このパターンの一例でしょう。

以下の図は、プラグインパターンの構造を示しています。

mermaidflowchart TB
  core["コアシステム<br/>Core Application"]
  interface["プラグインインターフェース<br/>Plugin Interface"]

  plugin1["Nano Banana<br/>プラグイン"]
  plugin2["DALL-E<br/>プラグイン"]
  plugin3["Stable Diffusion<br/>プラグイン"]

  core --> interface
  interface -.-> plugin1
  interface -.-> plugin2
  interface -.-> plugin3

  plugin1 --> api1["Nano Banana API"]
  plugin2 --> api2["OpenAI API"]
  plugin3 --> api3["Stability AI API"]

  style interface fill:#87CEEB
  style plugin1 fill:#90EE90
  style plugin2 fill:#90EE90
  style plugin3 fill:#90EE90

図で理解できる要点:

  • コアシステムは共通インターフェースのみを知っている
  • 各プラグインがインターフェースを実装
  • プラグインの追加・削除がコアシステムに影響しない

実装例:インターフェース定義

まず、すべての画像生成プラグインが従うべきインターフェースを定義します。

typescript// プラグインが実装すべきインターフェース
interface ImageGeneratorPlugin {
  // プラグイン名
  name: string;

  // プラグインのバージョン
  version: string;

  // 画像生成メソッド
  generate(
    prompt: string,
    options?: GenerationOptions
  ): Promise<GeneratedImage>;

  // 画像編集メソッド
  edit(
    image: ImageData,
    instruction: string
  ): Promise<GeneratedImage>;

  // プラグインの初期化
  initialize(config: PluginConfig): Promise<void>;

  // プラグインのクリーンアップ
  cleanup(): Promise<void>;
}
typescript// 共通の型定義
interface GenerationOptions {
  width?: number;
  height?: number;
  quality?: 'low' | 'medium' | 'high';
  style?: string;
}

interface GeneratedImage {
  imageData: Buffer;
  format: 'png' | 'jpg' | 'webp';
  metadata: {
    width: number;
    height: number;
    generatedAt: Date;
  };
}

interface PluginConfig {
  apiKey: string;
  endpoint?: string;
  timeout?: number;
}

このインターフェースにより、どんな画像生成サービスでも統一的に扱えるようになります。

実装例:Nano Banana プラグイン

インターフェースに従って、Nano Banana 用のプラグインを実装します。

typescript// Nano Bananaプラグインの実装
class NanoBananaPlugin implements ImageGeneratorPlugin {
  name = 'nano-banana';
  version = '1.0.0';

  private apiKey: string = '';
  private endpoint: string =
    'https://generativelanguage.googleapis.com/v1';

  // 初期化処理
  async initialize(config: PluginConfig): Promise<void> {
    this.apiKey = config.apiKey;

    if (config.endpoint) {
      this.endpoint = config.endpoint;
    }

    // 接続テスト
    await this.testConnection();
  }

  // 接続テスト
  private async testConnection(): Promise<void> {
    try {
      const response = await fetch(
        `${this.endpoint}/models`,
        {
          headers: {
            Authorization: `Bearer ${this.apiKey}`,
          },
        }
      );

      if (!response.ok) {
        throw new Error(
          `Connection test failed: ${response.status}`
        );
      }
    } catch (error) {
      throw new Error(
        `Failed to connect to Nano Banana: ${error.message}`
      );
    }
  }

  // 画像生成
  async generate(
    prompt: string,
    options?: GenerationOptions
  ): Promise<GeneratedImage> {
    const requestBody = {
      prompt: prompt,
      model: 'gemini-2.5-flash-image',
      width: options?.width || 1024,
      height: options?.height || 1024,
    };

    const response = await fetch(
      `${this.endpoint}/generate`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
      }
    );

    if (!response.ok) {
      throw new Error(
        `Generation failed: ${response.status}`
      );
    }

    const data = await response.json();

    return {
      imageData: Buffer.from(data.image, 'base64'),
      format: 'png',
      metadata: {
        width: data.width,
        height: data.height,
        generatedAt: new Date(),
      },
    };
  }

  // 画像編集
  async edit(
    image: ImageData,
    instruction: string
  ): Promise<GeneratedImage> {
    // Nano Banana特有の編集機能を実装
    const imageBase64 = image.toString('base64');

    const response = await fetch(`${this.endpoint}/edit`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        image: imageBase64,
        instruction: instruction,
        model: 'gemini-2.5-flash-image',
      }),
    });

    const data = await response.json();

    return {
      imageData: Buffer.from(data.image, 'base64'),
      format: 'png',
      metadata: {
        width: data.width,
        height: data.height,
        generatedAt: new Date(),
      },
    };
  }

  // クリーンアップ
  async cleanup(): Promise<void> {
    // 必要に応じてリソースの解放処理
    this.apiKey = '';
  }
}

実装例:プラグインマネージャー

プラグインを管理するマネージャークラスを実装します。

typescript// プラグインを管理するクラス
class PluginManager {
  private plugins: Map<string, ImageGeneratorPlugin> =
    new Map();
  private activePlugin: string | null = null;

  // プラグインの登録
  registerPlugin(plugin: ImageGeneratorPlugin): void {
    if (this.plugins.has(plugin.name)) {
      throw new Error(
        `Plugin ${plugin.name} is already registered`
      );
    }

    this.plugins.set(plugin.name, plugin);
    console.log(
      `Registered plugin: ${plugin.name} v${plugin.version}`
    );
  }

  // プラグインの初期化
  async activatePlugin(
    pluginName: string,
    config: PluginConfig
  ): Promise<void> {
    const plugin = this.plugins.get(pluginName);

    if (!plugin) {
      throw new Error(`Plugin ${pluginName} not found`);
    }

    await plugin.initialize(config);
    this.activePlugin = pluginName;
    console.log(`Activated plugin: ${pluginName}`);
  }

  // アクティブなプラグインを取得
  getActivePlugin(): ImageGeneratorPlugin {
    if (!this.activePlugin) {
      throw new Error('No plugin is active');
    }

    const plugin = this.plugins.get(this.activePlugin);

    if (!plugin) {
      throw new Error(
        `Active plugin ${this.activePlugin} not found`
      );
    }

    return plugin;
  }

  // 利用可能なプラグイン一覧
  listPlugins(): string[] {
    return Array.from(this.plugins.keys());
  }
}

使用例

プラグインシステムを使って画像を生成します。

typescript// プラグインシステムの使用例
async function demonstratePluginPattern() {
  const manager = new PluginManager();

  // Nano Bananaプラグインを登録
  const nanoBanana = new NanoBananaPlugin();
  manager.registerPlugin(nanoBanana);

  // プラグインを有効化
  await manager.activatePlugin('nano-banana', {
    apiKey: process.env.NANO_BANANA_API_KEY!,
  });

  // 画像生成(どのプラグインを使っているか意識しない)
  const plugin = manager.getActivePlugin();
  const image = await plugin.generate(
    '赤いワンピースを着た女性'
  );

  console.log('Generated image:', image.metadata);
}

このパターンの利点は、新しい画像生成サービスを追加する際に、既存のコードを変更する必要がない点です。

パターン 2:アダプターパターン

アダプターパターンは、互換性のないインターフェースを持つクラス同士を連携させる設計手法です。

コンセプト

既存のシステムが期待するインターフェースと、外部サービスの実際のインターフェースが異なる場合に、その間を「翻訳」する層を設けます。

以下の図は、アダプターパターンの役割を示しています。

mermaidflowchart LR
  client["クライアント<br/>アプリケーション"] -->|期待する<br/>インターフェース| adapter["アダプター<br/>翻訳層"]

  adapter -->|実際の<br/>インターフェース| service1["Nano Banana<br/>API"]
  adapter -->|実際の<br/>インターフェース| service2["DALL-E<br/>API"]
  adapter -->|実際の<br/>インターフェース| service3["Stable Diffusion<br/>API"]

  style adapter fill:#ffd700
  style client fill:#87CEEB

図で理解できる要点:

  • クライアントは共通インターフェースのみを知っている
  • アダプターが各サービスの違いを吸収
  • サービス追加時はアダプターを追加するだけ

実装例:共通インターフェース

アプリケーションが期待する画像生成インターフェースを定義します。

typescript// アプリケーションが期待するインターフェース
interface ImageGenerationService {
  createImage(description: string): Promise<ImageResult>;
  modifyImage(
    imageId: string,
    changes: string
  ): Promise<ImageResult>;
  getImageStatus(imageId: string): Promise<ImageStatus>;
}
typescript// 共通の戻り値型
interface ImageResult {
  id: string;
  url: string;
  width: number;
  height: number;
  createdAt: Date;
}

interface ImageStatus {
  id: string;
  status: 'pending' | 'completed' | 'failed';
  progress: number;
  errorMessage?: string;
}

実装例:Nano Banana アダプター

Nano Banana の実際の API を、共通インターフェースに適合させるアダプターを実装します。

typescript// Nano BananaのAPIレスポンス型(実際のAPI仕様)
interface NanoBananaResponse {
  imageData: string; // base64エンコードされた画像
  dimensions: {
    width: number;
    height: number;
  };
  generationTime: string;
  modelVersion: string;
}
typescript// Nano Banana用アダプター
class NanoBananaAdapter implements ImageGenerationService {
  private apiKey: string;
  private endpoint: string =
    'https://generativelanguage.googleapis.com/v1';
  private imageCache: Map<string, string> = new Map();

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  // 画像生成(共通インターフェースに適合)
  async createImage(
    description: string
  ): Promise<ImageResult> {
    // Nano Banana特有のAPIを呼び出し
    const response = await fetch(
      `${this.endpoint}/models/gemini-2.5-flash-image:generate`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          contents: [
            {
              parts: [{ text: description }],
            },
          ],
        }),
      }
    );

    if (!response.ok) {
      throw new Error(
        `Nano Banana API error: ${response.status}`
      );
    }

    const data: NanoBananaResponse = await response.json();

    // Nano Bananaのレスポンスを共通形式に変換
    const imageId = this.generateImageId();
    const imageUrl = await this.saveImage(
      imageId,
      data.imageData
    );

    return {
      id: imageId,
      url: imageUrl,
      width: data.dimensions.width,
      height: data.dimensions.height,
      createdAt: new Date(),
    };
  }

  // 画像編集(共通インターフェースに適合)
  async modifyImage(
    imageId: string,
    changes: string
  ): Promise<ImageResult> {
    const originalImageData = this.imageCache.get(imageId);

    if (!originalImageData) {
      throw new Error(`Image ${imageId} not found`);
    }

    // Nano Bananaの編集APIを呼び出し
    const response = await fetch(
      `${this.endpoint}/models/gemini-2.5-flash-image:edit`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          image: originalImageData,
          instruction: changes,
        }),
      }
    );

    const data: NanoBananaResponse = await response.json();

    // 新しい画像IDで保存
    const newImageId = this.generateImageId();
    const imageUrl = await this.saveImage(
      newImageId,
      data.imageData
    );

    return {
      id: newImageId,
      url: imageUrl,
      width: data.dimensions.width,
      height: data.dimensions.height,
      createdAt: new Date(),
    };
  }

  // 画像ステータスの取得
  async getImageStatus(
    imageId: string
  ): Promise<ImageStatus> {
    // Nano Bananaは同期的に結果を返すため、常にcompletedを返す
    const exists = this.imageCache.has(imageId);

    return {
      id: imageId,
      status: exists ? 'completed' : 'failed',
      progress: exists ? 100 : 0,
      errorMessage: exists ? undefined : 'Image not found',
    };
  }

  // ヘルパーメソッド:画像IDの生成
  private generateImageId(): string {
    return `img_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`;
  }

  // ヘルパーメソッド:画像の保存
  private async saveImage(
    id: string,
    base64Data: string
  ): Promise<string> {
    this.imageCache.set(id, base64Data);
    // 実際の実装では、S3やCloud Storageに保存
    return `https://storage.example.com/images/${id}.png`;
  }
}

実装例:別のサービス用アダプター

同じインターフェースで、別のサービス(DALL-E)のアダプターも実装できます。

typescript// DALL-E用アダプター(同じインターフェースを実装)
class DallEAdapter implements ImageGenerationService {
  private apiKey: string;
  private endpoint: string =
    'https://api.openai.com/v1/images';

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async createImage(
    description: string
  ): Promise<ImageResult> {
    // OpenAI DALL-EのAPIを呼び出し
    const response = await fetch(
      `${this.endpoint}/generations`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          prompt: description,
          n: 1,
          size: '1024x1024',
        }),
      }
    );

    const data = await response.json();

    // DALL-Eのレスポンスを共通形式に変換
    return {
      id: `dalle_${Date.now()}`,
      url: data.data[0].url,
      width: 1024,
      height: 1024,
      createdAt: new Date(),
    };
  }

  async modifyImage(
    imageId: string,
    changes: string
  ): Promise<ImageResult> {
    // DALL-Eの編集API実装
    throw new Error('Not implemented yet');
  }

  async getImageStatus(
    imageId: string
  ): Promise<ImageStatus> {
    // DALL-Eのステータス確認実装
    return {
      id: imageId,
      status: 'completed',
      progress: 100,
    };
  }
}

使用例

アダプターを使うことで、サービスの切り替えが簡単になります。

typescript// アダプターパターンの使用例
async function demonstrateAdapterPattern() {
  // Nano Bananaアダプターを使用
  let imageService: ImageGenerationService =
    new NanoBananaAdapter(process.env.NANO_BANANA_API_KEY!);

  // 画像生成
  let result = await imageService.createImage(
    '赤いワンピースを着た女性'
  );
  console.log('Nano Banana result:', result);

  // 簡単にサービスを切り替えられる
  imageService = new DallEAdapter(
    process.env.OPENAI_API_KEY!
  );

  // 同じインターフェースで別のサービスを使用
  result = await imageService.createImage(
    '赤いワンピースを着た女性'
  );
  console.log('DALL-E result:', result);
}

クライアントコードは ImageGenerationService インターフェースのみに依存しており、実際にどのサービスを使っているかは意識する必要がありません。

パターン 3:ポート&アダプタ(ヘキサゴナルアーキテクチャ)

ポート&アダプタパターンは、ビジネスロジックを外部の技術的詳細から完全に分離する設計手法です。別名「ヘキサゴナル(六角形)アーキテクチャ」とも呼ばれます。

コンセプト

システムの中心にビジネスロジック(ドメイン層)を配置し、外部とのやり取りはすべて「ポート(インターフェース)」と「アダプター(実装)」を経由します。

以下の図は、ヘキサゴナルアーキテクチャの構造を示しています。

mermaidflowchart TB
  subgraph domain["ドメイン層(ビジネスロジック)"]
    core["画像生成<br/>ビジネスロジック"]
    portIn["入力ポート<br/>UseCase Interface"]
    portOut["出力ポート<br/>Repository Interface"]
  end

  subgraph adapters["アダプター層"]
    direction TB
    apiAdapter["REST API<br/>アダプター"]
    cliAdapter["CLI<br/>アダプター"]
    nanoAdapter["Nano Banana<br/>アダプター"]
    dalleAdapter["DALL-E<br/>アダプター"]
  end

  apiAdapter -->|入力| portIn
  cliAdapter -->|入力| portIn

  portIn --> core
  core --> portOut

  portOut -->|出力| nanoAdapter
  portOut -->|出力| dalleAdapter

  nanoAdapter --> nanoAPI["Nano Banana<br/>API"]
  dalleAdapter --> dalleAPI["DALL-E<br/>API"]

  style domain fill:#e1f5ff
  style core fill:#87CEEB

図で理解できる要点:

  • ビジネスロジック(中心)は外部技術に依存しない
  • 入力ポート経由で様々な UI から利用できる
  • 出力ポート経由で様々なサービスに切り替え可能

実装例:ドメイン層(ポート定義)

まず、ドメイン層でポート(インターフェース)を定義します。

typescript// ドメイン層:入力ポート(ユースケース)
interface ImageGenerationUseCase {
  generateProductImage(
    request: ProductImageRequest
  ): Promise<ProductImageResponse>;
  editProductImage(
    request: EditImageRequest
  ): Promise<ProductImageResponse>;
}
typescript// ドメイン層:出力ポート(リポジトリインターフェース)
interface ImageGenerationPort {
  generate(
    prompt: string,
    options: GenerationOptions
  ): Promise<GeneratedImageData>;
  edit(
    imageData: ImageData,
    instruction: string
  ): Promise<GeneratedImageData>;
}

interface ImageStoragePort {
  save(image: GeneratedImageData): Promise<string>;
  load(imageId: string): Promise<GeneratedImageData>;
  delete(imageId: string): Promise<void>;
}
typescript// ドメインモデル
interface ProductImageRequest {
  productName: string;
  productDescription: string;
  style: 'realistic' | 'artistic' | 'minimalist';
  backgroundColor: string;
}

interface EditImageRequest {
  imageId: string;
  changes: string[];
}

interface ProductImageResponse {
  imageId: string;
  imageUrl: string;
  generatedAt: Date;
}

interface GeneratedImageData {
  data: Buffer;
  format: string;
  width: number;
  height: number;
}

実装例:ドメイン層(ビジネスロジック)

ポートを使ってビジネスロジックを実装します。外部技術には一切依存しません。

typescript// ドメイン層:ビジネスロジックの実装
class ProductImageGenerator
  implements ImageGenerationUseCase
{
  constructor(
    private readonly imageGenerator: ImageGenerationPort,
    private readonly imageStorage: ImageStoragePort
  ) {}

  async generateProductImage(
    request: ProductImageRequest
  ): Promise<ProductImageResponse> {
    // ビジネスルール:プロンプトの最適化
    const optimizedPrompt = this.optimizePrompt(request);

    // 画像生成(実装の詳細は知らない)
    const generatedImage =
      await this.imageGenerator.generate(optimizedPrompt, {
        width: 1024,
        height: 1024,
        quality: 'high',
        style: request.style,
      });

    // ビジネスルール:品質チェック
    this.validateImageQuality(generatedImage);

    // 画像保存(実装の詳細は知らない)
    const imageUrl = await this.imageStorage.save(
      generatedImage
    );

    // 結果を返す
    return {
      imageId: this.generateImageId(),
      imageUrl: imageUrl,
      generatedAt: new Date(),
    };
  }

  async editProductImage(
    request: EditImageRequest
  ): Promise<ProductImageResponse> {
    // 元画像をロード
    const originalImage = await this.imageStorage.load(
      request.imageId
    );

    // 複数の変更を順次適用
    let currentImage = originalImage;
    for (const change of request.changes) {
      currentImage = await this.imageGenerator.edit(
        currentImage.data,
        change
      );
    }

    // 編集済み画像を保存
    const imageUrl = await this.imageStorage.save(
      currentImage
    );

    return {
      imageId: this.generateImageId(),
      imageUrl: imageUrl,
      generatedAt: new Date(),
    };
  }

  // ビジネスロジック:プロンプトの最適化
  private optimizePrompt(
    request: ProductImageRequest
  ): string {
    const styleDescriptions = {
      realistic: '写真のようにリアルな',
      artistic: 'アーティスティックで創造的な',
      minimalist: 'ミニマルでシンプルな',
    };

    return `${styleDescriptions[request.style]}${
      request.backgroundColor
    }の背景に映える、
            ${request.productName}${
      request.productDescription
    })の商品写真`;
  }

  // ビジネスルール:画質検証
  private validateImageQuality(
    image: GeneratedImageData
  ): void {
    if (image.width < 512 || image.height < 512) {
      throw new Error(
        'Generated image resolution is too low'
      );
    }

    if (image.data.length === 0) {
      throw new Error('Generated image is empty');
    }
  }

  private generateImageId(): string {
    return `prod_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`;
  }
}

実装例:アダプター層(Nano Banana アダプター)

出力ポートを実装する Nano Banana 用アダプターを作成します。

typescript// アダプター層:Nano Banana実装
class NanoBananaImageAdapter
  implements ImageGenerationPort
{
  private apiKey: string;
  private endpoint: string =
    'https://generativelanguage.googleapis.com/v1';

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async generate(
    prompt: string,
    options: GenerationOptions
  ): Promise<GeneratedImageData> {
    const response = await fetch(
      `${this.endpoint}/models/gemini-2.5-flash-image:generateImages`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          prompt: prompt,
          imageSize: `${options.width}x${options.height}`,
          numberOfImages: 1,
        }),
      }
    );

    if (!response.ok) {
      throw new Error(
        `Nano Banana API error: ${response.status} ${response.statusText}`
      );
    }

    const result = await response.json();
    const imageBase64 = result.images[0].imageData;

    return {
      data: Buffer.from(imageBase64, 'base64'),
      format: 'png',
      width: options.width || 1024,
      height: options.height || 1024,
    };
  }

  async edit(
    imageData: ImageData,
    instruction: string
  ): Promise<GeneratedImageData> {
    const imageBase64 = imageData.toString('base64');

    const response = await fetch(
      `${this.endpoint}/models/gemini-2.5-flash-image:editImage`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          image: imageBase64,
          instruction: instruction,
        }),
      }
    );

    const result = await response.json();

    return {
      data: Buffer.from(result.editedImage, 'base64'),
      format: 'png',
      width: result.width,
      height: result.height,
    };
  }
}

実装例:アダプター層(ストレージアダプター)

画像保存用のアダプターも実装します。

typescript// アダプター層:S3ストレージ実装
class S3ImageStorageAdapter implements ImageStoragePort {
  private s3Client: S3Client;
  private bucketName: string;

  constructor(bucketName: string, region: string) {
    this.bucketName = bucketName;
    this.s3Client = new S3Client({ region });
  }

  async save(image: GeneratedImageData): Promise<string> {
    const imageId = `images/${Date.now()}-${Math.random()
      .toString(36)
      .substr(2, 9)}.${image.format}`;

    await this.s3Client.send(
      new PutObjectCommand({
        Bucket: this.bucketName,
        Key: imageId,
        Body: image.data,
        ContentType: `image/${image.format}`,
      })
    );

    return `https://${this.bucketName}.s3.amazonaws.com/${imageId}`;
  }

  async load(imageId: string): Promise<GeneratedImageData> {
    const response = await this.s3Client.send(
      new GetObjectCommand({
        Bucket: this.bucketName,
        Key: imageId,
      })
    );

    const imageData =
      await response.Body?.transformToByteArray();

    if (!imageData) {
      throw new Error(`Image ${imageId} not found`);
    }

    return {
      data: Buffer.from(imageData),
      format: imageId.split('.').pop() || 'png',
      width: 0, // メタデータから取得すべき
      height: 0, // メタデータから取得すべき
    };
  }

  async delete(imageId: string): Promise<void> {
    await this.s3Client.send(
      new DeleteObjectCommand({
        Bucket: this.bucketName,
        Key: imageId,
      })
    );
  }
}

実装例:依存性注入(DI)による組み立て

各層を組み立ててアプリケーションを構築します。

typescript// アプリケーションの組み立て
class Application {
  private useCase: ImageGenerationUseCase;

  constructor() {
    // アダプターのインスタンス化(外側の層)
    const imageGenerator = new NanoBananaImageAdapter(
      process.env.NANO_BANANA_API_KEY!
    );

    const imageStorage = new S3ImageStorageAdapter(
      process.env.S3_BUCKET_NAME!,
      process.env.AWS_REGION!
    );

    // ユースケースの作成(内側の層)
    // 依存性を外から注入(Dependency Injection)
    this.useCase = new ProductImageGenerator(
      imageGenerator,
      imageStorage
    );
  }

  getImageGenerationUseCase(): ImageGenerationUseCase {
    return this.useCase;
  }
}

使用例

ポート&アダプタパターンを使った実際のコード例です。

typescript// REST APIハンドラー(入力アダプター)
async function handleProductImageRequest(
  req: Request,
  res: Response
) {
  const app = new Application();
  const useCase = app.getImageGenerationUseCase();

  try {
    const result = await useCase.generateProductImage({
      productName: req.body.productName,
      productDescription: req.body.description,
      style: req.body.style,
      backgroundColor: req.body.backgroundColor,
    });

    res.json({
      success: true,
      data: result,
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
}

このパターンの最大の利点は、ビジネスロジックが外部技術に一切依存しないため、テストが容易で、技術スタックの変更にも強い点です。

3 つのパターンの比較

それぞれのパターンの特徴を整理します。

#パターン名主な用途複雑さ柔軟性テスト容易性
1プラグイン機能の動的な追加・削除★★☆★★★★★★
2アダプタ異なるインターフェースの統合★☆☆★★☆★★☆
3ポート&アダプタビジネスロジックと技術の完全分離★★★★★★★★★

選択の指針

  • プラグイン:複数のサービスを切り替えて使いたい場合
  • アダプタ:既存コードへの影響を最小限に抑えたい場合
  • ポート&アダプタ:長期的な保守性とテスト容易性を重視する場合

次のセクションでは、これらのパターンを組み合わせた実践的な例を見ていきます。

具体例

実践的な統合システムの構築

ここでは、Nano Banana を使った EC サイトの商品画像生成システムを、3 つのパターンを組み合わせて実装します。

システム要件

以下の機能を持つシステムを構築します。

#機能説明
1商品画像の自動生成テキスト説明から商品画像を生成
2画像のバッチ処理複数商品の画像を一括生成
3画像編集生成済み画像の色や背景を変更
4複数 AI サービスの切り替えNano Banana と DALL-E を状況に応じて使い分け
5エラーハンドリングAPI 障害時の再試行とフォールバック

アーキテクチャ全体像

システム全体の構成を図で示します。

mermaidflowchart TB
  subgraph ui["プレゼンテーション層"]
    api["REST API"]
    cli["CLI Tool"]
  end

  subgraph app["アプリケーション層"]
    useCase["商品画像生成<br/>UseCase"]
    batch["バッチ処理<br/>UseCase"]
  end

  subgraph domain["ドメイン層"]
    service["画像生成<br/>ドメインサービス"]
    portGen["生成ポート"]
    portStorage["保存ポート"]
  end

  subgraph infra["インフラ層(アダプター)"]
    pluginMgr["プラグイン<br/>マネージャー"]
    nanoAdapter["Nano Banana<br/>アダプター"]
    dalleAdapter["DALL-E<br/>アダプター"]
    s3["S3<br/>アダプター"]
  end

  api --> useCase
  cli --> batch
  useCase --> service
  batch --> service
  service --> portGen
  service --> portStorage
  portGen --> pluginMgr
  pluginMgr --> nanoAdapter
  pluginMgr --> dalleAdapter
  portStorage --> s3

  nanoAdapter --> nanoAPI["Nano Banana<br/>API"]
  dalleAdapter --> dalleAPI["DALL-E<br/>API"]

  style domain fill:#e1f5ff
  style infra fill:#fff4e1

図で理解できる要点:

  • 複数の入力インターフェース(API、CLI)に対応
  • ドメイン層でビジネスロジックを集約
  • プラグインマネージャーで複数の AI サービスを管理
  • 各層が疎結合で独立してテスト可能

実装例:ドメイン層

ビジネスロジックを実装します。

typescript// ドメイン層:エンティティ
class ProductImage {
  constructor(
    public readonly id: string,
    public readonly productId: string,
    public readonly url: string,
    public readonly width: number,
    public readonly height: number,
    public readonly generatedAt: Date,
    public readonly generator: string
  ) {}

  // ビジネスルール:画像が有効期限内か判定
  isValid(maxAgeInDays: number = 30): boolean {
    const ageInDays =
      (Date.now() - this.generatedAt.getTime()) /
      (1000 * 60 * 60 * 24);
    return ageInDays <= maxAgeInDays;
  }

  // ビジネスルール:画像サイズが要件を満たすか判定
  meetsRequirements(
    minWidth: number,
    minHeight: number
  ): boolean {
    return (
      this.width >= minWidth && this.height >= minHeight
    );
  }
}
typescript// ドメイン層:バリューオブジェクト
class ImageGenerationRequest {
  constructor(
    public readonly productId: string,
    public readonly description: string,
    public readonly style: ImageStyle,
    public readonly dimensions: ImageDimensions
  ) {
    this.validate();
  }

  private validate(): void {
    if (!this.productId || this.productId.trim() === '') {
      throw new Error('Product ID is required');
    }

    if (
      !this.description ||
      this.description.trim().length < 10
    ) {
      throw new Error(
        'Description must be at least 10 characters'
      );
    }
  }

  toPrompt(): string {
    return `${this.style.description}${this.description}、商品写真、${this.style.background}背景`;
  }
}

interface ImageStyle {
  name: string;
  description: string;
  background: string;
}

interface ImageDimensions {
  width: number;
  height: number;
}
typescript// ドメイン層:ドメインサービス
class ProductImageGenerationService {
  constructor(
    private readonly imageGenerator: ImageGenerationPort,
    private readonly imageStorage: ImageStoragePort,
    private readonly retryPolicy: RetryPolicy
  ) {}

  async generateImage(
    request: ImageGenerationRequest
  ): Promise<ProductImage> {
    // プロンプトの生成
    const prompt = request.toPrompt();

    // 再試行ポリシーを適用して画像生成
    const generatedImage = await this.retryPolicy.execute(
      () =>
        this.imageGenerator.generate(prompt, {
          width: request.dimensions.width,
          height: request.dimensions.height,
          quality: 'high',
        })
    );

    // ビジネスルール:品質検証
    this.validateImageQuality(generatedImage);

    // 画像を保存
    const imageUrl = await this.imageStorage.save(
      generatedImage
    );

    // エンティティを作成
    return new ProductImage(
      this.generateId(),
      request.productId,
      imageUrl,
      generatedImage.width,
      generatedImage.height,
      new Date(),
      this.imageGenerator.constructor.name
    );
  }

  private validateImageQuality(
    image: GeneratedImageData
  ): void {
    if (image.width < 512 || image.height < 512) {
      throw new Error('Image resolution too low');
    }

    if (image.data.length < 10000) {
      throw new Error(
        'Image file size too small, possibly corrupted'
      );
    }
  }

  private generateId(): string {
    return `img_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`;
  }
}

実装例:再試行ポリシー

API 障害に対応する再試行ロジックを実装します。

typescript// インフラ層:再試行ポリシー
class RetryPolicy {
  constructor(
    private readonly maxRetries: number = 3,
    private readonly initialDelayMs: number = 1000,
    private readonly backoffMultiplier: number = 2
  ) {}

  async execute<T>(
    operation: () => Promise<T>
  ): Promise<T> {
    let lastError: Error | null = null;
    let delayMs = this.initialDelayMs;

    for (
      let attempt = 1;
      attempt <= this.maxRetries;
      attempt++
    ) {
      try {
        console.log(
          `Attempt ${attempt}/${this.maxRetries}`
        );
        return await operation();
      } catch (error) {
        lastError = error as Error;
        console.error(
          `Attempt ${attempt} failed:`,
          error.message
        );

        // 最後の試行では待機しない
        if (attempt < this.maxRetries) {
          console.log(`Retrying in ${delayMs}ms...`);
          await this.delay(delayMs);
          delayMs *= this.backoffMultiplier;
        }
      }
    }

    throw new Error(
      `Operation failed after ${this.maxRetries} attempts: ${lastError?.message}`
    );
  }

  private delay(ms: number): Promise<void> {
    return new Promise((resolve) =>
      setTimeout(resolve, ms)
    );
  }
}

実装例:プラグインマネージャー with フォールバック

複数のプラグインを管理し、自動フォールバックする実装です。

typescript// インフラ層:高度なプラグインマネージャー
class SmartPluginManager {
  private plugins: Map<string, ImageGenerationPort> =
    new Map();
  private pluginPriority: string[] = [];
  private healthStatus: Map<string, boolean> = new Map();

  registerPlugin(
    name: string,
    plugin: ImageGenerationPort,
    priority: number = 0
  ): void {
    this.plugins.set(name, plugin);
    this.healthStatus.set(name, true);

    // 優先順位に従ってソート
    this.pluginPriority.push(name);
    this.pluginPriority.sort((a, b) => {
      // 実際には優先度マップを別途管理すべき
      return 0;
    });

    console.log(`Registered plugin: ${name}`);
  }

  async generateWithFallback(
    prompt: string,
    options: GenerationOptions
  ): Promise<GeneratedImageData> {
    const errors: Array<{ plugin: string; error: Error }> =
      [];

    // 優先順位に従ってプラグインを試行
    for (const pluginName of this.pluginPriority) {
      // 健全性チェックでNGならスキップ
      if (!this.healthStatus.get(pluginName)) {
        console.log(
          `Skipping unhealthy plugin: ${pluginName}`
        );
        continue;
      }

      const plugin = this.plugins.get(pluginName);
      if (!plugin) continue;

      try {
        console.log(`Trying plugin: ${pluginName}`);
        const result = await plugin.generate(
          prompt,
          options
        );

        // 成功したら健全性を記録
        this.healthStatus.set(pluginName, true);
        console.log(
          `Successfully generated with: ${pluginName}`
        );

        return result;
      } catch (error) {
        // エラーを記録
        errors.push({
          plugin: pluginName,
          error: error as Error,
        });

        // 健全性を低下させる
        this.healthStatus.set(pluginName, false);
        console.error(
          `Plugin ${pluginName} failed:`,
          (error as Error).message
        );

        // 次のプラグインにフォールバック
        continue;
      }
    }

    // すべてのプラグインが失敗
    const errorDetails = errors
      .map((e) => `${e.plugin}: ${e.error.message}`)
      .join('; ');

    throw new Error(
      `All plugins failed. Details: ${errorDetails}`
    );
  }

  // 定期的な健全性チェック
  async performHealthCheck(): Promise<void> {
    console.log(
      'Performing health check on all plugins...'
    );

    for (const [name, plugin] of this.plugins) {
      try {
        // 簡単なテスト生成を実行
        await plugin.generate('test', {
          width: 64,
          height: 64,
          quality: 'low',
        });
        this.healthStatus.set(name, true);
        console.log(`✓ Plugin ${name} is healthy`);
      } catch (error) {
        this.healthStatus.set(name, false);
        console.log(`✗ Plugin ${name} is unhealthy`);
      }
    }
  }
}

実装例:バッチ処理 UseCase

複数商品の画像を並行生成する UseCase です。

typescript// アプリケーション層:バッチ処理UseCase
class BatchImageGenerationUseCase {
  constructor(
    private readonly imageService: ProductImageGenerationService,
    private readonly maxConcurrency: number = 5
  ) {}

  async generateImages(
    requests: ImageGenerationRequest[]
  ): Promise<BatchResult> {
    const results: ProductImage[] = [];
    const errors: Array<{
      productId: string;
      error: string;
    }> = [];

    // 並行処理の制御
    const chunks = this.chunkArray(
      requests,
      this.maxConcurrency
    );

    for (const chunk of chunks) {
      const promises = chunk.map(async (request) => {
        try {
          const image =
            await this.imageService.generateImage(request);
          results.push(image);
          return {
            success: true,
            productId: request.productId,
          };
        } catch (error) {
          const errorMessage = (error as Error).message;
          errors.push({
            productId: request.productId,
            error: errorMessage,
          });
          return {
            success: false,
            productId: request.productId,
          };
        }
      });

      // チャンク内は並行実行
      await Promise.all(promises);
    }

    return {
      successCount: results.length,
      failureCount: errors.length,
      images: results,
      errors: errors,
    };
  }

  private chunkArray<T>(array: T[], size: number): T[][] {
    const chunks: T[][] = [];
    for (let i = 0; i < array.length; i += size) {
      chunks.push(array.slice(i, i + size));
    }
    return chunks;
  }
}

interface BatchResult {
  successCount: number;
  failureCount: number;
  images: ProductImage[];
  errors: Array<{ productId: string; error: string }>;
}

実装例:システムの組み立て

すべてのコンポーネントを組み立てます。

typescript// メインアプリケーション
class ECommerceImageSystem {
  private pluginManager: SmartPluginManager;
  private imageService: ProductImageGenerationService;
  private batchUseCase: BatchImageGenerationUseCase;

  async initialize() {
    // プラグインマネージャーのセットアップ
    this.pluginManager = new SmartPluginManager();

    // Nano Bananaプラグインを登録(優先度1)
    const nanoBanana = new NanoBananaImageAdapter(
      process.env.NANO_BANANA_API_KEY!
    );
    this.pluginManager.registerPlugin(
      'nano-banana',
      nanoBanana,
      1
    );

    // DALL-Eプラグインを登録(優先度2、フォールバック用)
    const dalle = new DallEImageAdapter(
      process.env.OPENAI_API_KEY!
    );
    this.pluginManager.registerPlugin('dalle', dalle, 2);

    // ストレージアダプター
    const storage = new S3ImageStorageAdapter(
      process.env.S3_BUCKET_NAME!,
      process.env.AWS_REGION!
    );

    // 再試行ポリシー
    const retryPolicy = new RetryPolicy(3, 1000, 2);

    // ドメインサービス
    this.imageService = new ProductImageGenerationService(
      this.pluginManager,
      storage,
      retryPolicy
    );

    // バッチUseCase
    this.batchUseCase = new BatchImageGenerationUseCase(
      this.imageService,
      5 // 最大5並行
    );

    // 健全性チェックを実行
    await this.pluginManager.performHealthCheck();

    console.log('System initialized successfully');
  }

  getBatchUseCase(): BatchImageGenerationUseCase {
    return this.batchUseCase;
  }
}

使用例:実際のバッチ処理

システムを使って商品画像を一括生成します。

typescript// 実際の使用例
async function generateProductCatalog() {
  // システムの初期化
  const system = new ECommerceImageSystem();
  await system.initialize();

  // 商品データの準備
  const products = [
    {
      id: 'P001',
      name: '花柄ワンピース',
      description: '春らしい花柄プリントのエレガントなワンピース',
    },
    {
      id: 'P002',
      name: 'デニムジャケット',
      description: 'ヴィンテージ加工のクラシックなデニムジャケット',
    },
    {
      id: 'P003',
      name: 'リネンブラウス',
      description: '涼しげなリネン素材の白いブラウス',
    },
  ];

  // 画像生成リクエストの作成
  const requests = products.map(
    product =>
      new ImageGenerationRequest(
        product.id,
        product.description,
        {
          name: 'realistic',
          description: 'リアルな商品写真スタイル',
          background: '白い',
        },
        { width: 1024, height: 1024 }
      )
  );

  // バッチ生成の実行
  console.log(`Generating images for ${requests.length} products...`);
  const batchUseCase = system.getBatchUseCase();
  const result = await batchUseCase.generateImages(requests);

  // 結果の表示
  console.log('\n=== Batch Generation Results ===');
  console.log(`Success: ${result.successCount}`);
  console.log(`Failure: ${result.failureCount}`);

  if (result.images.length > 0) {
    console.log('\nGenerated Images:');
    result.images.forEach(image => {
      console.log(`  - ${image.productId}: ${image.url}`);
    });
  }

  if (result.errors.length > 0) {
    console.log('\nErrors:');
    result.errors.forEach(error => {
      console.log(`  - ${error.productId}: ${error.error}`);
    });
  }
}

// 実行
generateProductCatalog().catch(console.error);
```

このコードは、3つのパターンすべてを活用しています。

- **プラグインパターン**: 複数のAI サービスを切り替え
- **アダプターパターン**: 各サービスのAPIの違いを吸収
- **ポート&アダプタ**: ビジネスロジックと技術的詳細を分離

## エラー処理とモニタリング

実運用では、エラーハンドリングとモニタリングが重要です。

### エラーコードと分類

API エラーを適切に分類し、対処します。

````typescript
// エラー分類とハンドリング
enum ImageGenerationErrorCode {
  // クライアントエラー(4xx系)
  INVALID_REQUEST = 'INVALID_REQUEST',
  AUTHENTICATION_FAILED = 'AUTHENTICATION_FAILED',
  QUOTA_EXCEEDED = 'QUOTA_EXCEEDED',

  // サーバーエラー(5xx系)
  SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
  TIMEOUT = 'TIMEOUT',
  UNKNOWN_ERROR = 'UNKNOWN_ERROR',
}
typescript// カスタムエラークラス
class ImageGenerationError extends Error {
  constructor(
    public readonly code: ImageGenerationErrorCode,
    message: string,
    public readonly originalError?: Error,
    public readonly retryable: boolean = false
  ) {
    super(message);
    this.name = 'ImageGenerationError';
  }

  static fromHttpStatus(
    status: number,
    message: string
  ): ImageGenerationError {
    if (status === 401 || status === 403) {
      return new ImageGenerationError(
        ImageGenerationErrorCode.AUTHENTICATION_FAILED,
        `Authentication failed: ${message}`,
        undefined,
        false
      );
    }

    if (status === 429) {
      return new ImageGenerationError(
        ImageGenerationErrorCode.QUOTA_EXCEEDED,
        `Rate limit exceeded: ${message}`,
        undefined,
        true // 再試行可能
      );
    }

    if (status >= 500) {
      return new ImageGenerationError(
        ImageGenerationErrorCode.SERVICE_UNAVAILABLE,
        `Service unavailable: ${message}`,
        undefined,
        true // 再試行可能
      );
    }

    return new ImageGenerationError(
      ImageGenerationErrorCode.UNKNOWN_ERROR,
      message,
      undefined,
      false
    );
  }
}

以下の表は、エラーコードと対処方法の一覧です。

#エラーコードHTTP ステータス再試行対処方法
1INVALID_REQUEST400不可リクエストパラメータを修正
2AUTHENTICATION_FAILED401, 403不可API キーを確認
3QUOTA_EXCEEDED429可能レート制限解除まで待機
4SERVICE_UNAVAILABLE500-599可能バックオフ戦略で再試行
5TIMEOUT-可能タイムアウト時間を延長して再試行

モニタリングとロギング

実運用では、パフォーマンスとエラーをモニタリングする必要があります。

typescript// モニタリング用のメトリクス収集
class ImageGenerationMetrics {
  private metrics = {
    totalRequests: 0,
    successfulRequests: 0,
    failedRequests: 0,
    totalLatencyMs: 0,
    pluginUsage: new Map<string, number>(),
  };

  recordRequest(
    pluginName: string,
    latencyMs: number,
    success: boolean
  ): void {
    this.metrics.totalRequests++;
    this.metrics.totalLatencyMs += latencyMs;

    if (success) {
      this.metrics.successfulRequests++;
    } else {
      this.metrics.failedRequests++;
    }

    // プラグイン使用状況
    const current =
      this.metrics.pluginUsage.get(pluginName) || 0;
    this.metrics.pluginUsage.set(pluginName, current + 1);
  }

  getMetrics() {
    const avgLatency =
      this.metrics.totalRequests > 0
        ? this.metrics.totalLatencyMs /
          this.metrics.totalRequests
        : 0;

    const successRate =
      this.metrics.totalRequests > 0
        ? (this.metrics.successfulRequests /
            this.metrics.totalRequests) *
          100
        : 0;

    return {
      totalRequests: this.metrics.totalRequests,
      successfulRequests: this.metrics.successfulRequests,
      failedRequests: this.metrics.failedRequests,
      averageLatencyMs: Math.round(avgLatency),
      successRate: successRate.toFixed(2) + '%',
      pluginUsage: Object.fromEntries(
        this.metrics.pluginUsage
      ),
    };
  }

  reset(): void {
    this.metrics = {
      totalRequests: 0,
      successfulRequests: 0,
      failedRequests: 0,
      totalLatencyMs: 0,
      pluginUsage: new Map(),
    };
  }
}
typescript// メトリクス収集を組み込んだアダプター
class MonitoredNanoBananaAdapter
  implements ImageGenerationPort
{
  private adapter: NanoBananaImageAdapter;
  private metrics: ImageGenerationMetrics;

  constructor(
    apiKey: string,
    metrics: ImageGenerationMetrics
  ) {
    this.adapter = new NanoBananaImageAdapter(apiKey);
    this.metrics = metrics;
  }

  async generate(
    prompt: string,
    options: GenerationOptions
  ): Promise<GeneratedImageData> {
    const startTime = Date.now();
    let success = false;

    try {
      const result = await this.adapter.generate(
        prompt,
        options
      );
      success = true;
      return result;
    } finally {
      const latency = Date.now() - startTime;
      this.metrics.recordRequest(
        'nano-banana',
        latency,
        success
      );
    }
  }

  async edit(
    imageData: ImageData,
    instruction: string
  ): Promise<GeneratedImageData> {
    const startTime = Date.now();
    let success = false;

    try {
      const result = await this.adapter.edit(
        imageData,
        instruction
      );
      success = true;
      return result;
    } finally {
      const latency = Date.now() - startTime;
      this.metrics.recordRequest(
        'nano-banana',
        latency,
        success
      );
    }
  }
}

これらの実装により、本番環境での安定運用が可能になります。

まとめ

Nano Banana のような AI サービスを既存システムに統合する際には、適切なアーキテクチャパターンを採用することが重要です。

3 つの設計パターンの要点

本記事で紹介した 3 つのパターンをまとめます。

1. プラグインパターン

  • システムの機能を動的に追加・削除できる
  • 複数の AI サービスを切り替えて使える
  • 新しいサービスの追加が容易

2. アダプターパターン

  • 異なるインターフェースを持つサービスを統一的に扱える
  • 既存コードへの影響を最小限に抑えられる
  • 比較的シンプルで導入しやすい

3. ポート&アダプタパターン(ヘキサゴナルアーキテクチャ)

  • ビジネスロジックと技術的詳細を完全に分離
  • テストが容易で品質保証しやすい
  • 長期的な保守性が非常に高い

パターン選択のガイドライン

プロジェクトの状況に応じて、適切なパターンを選択してください。

小規模プロジェクト・プロトタイプ

  • アダプターパターンから始める
  • シンプルで理解しやすい
  • 短期間で実装可能

中規模プロジェクト

  • プラグインパターンを採用
  • 拡張性とシンプルさのバランスが良い
  • 複数のサービスを切り替えたい場合に最適

大規模プロジェクト・エンタープライズ

  • ポート&アダプタパターンを採用
  • 長期的な保守性を重視
  • チーム開発に適している
  • テスト駆動開発(TDD)との相性が良い

設計パターンの恩恵

これらのパターンを適用することで、以下のメリットが得られます。

#メリット効果
1疎結合変更の影響範囲を限定、並行開発が可能
2テスト容易性モックを使った高速なテスト、品質向上
3保守性コードの理解が容易、バグ修正が簡単
4拡張性新機能の追加が既存コードに影響しない
5技術的負債の削減リファクタリングが容易、技術スタックの更新が簡単

実装時の注意点

パターンを適用する際の注意事項です。

過度な抽象化を避ける

パターンは強力ですが、過度に適用すると逆に複雑になります。プロジェクトの規模と要件に応じて、適切なレベルの抽象化を選びましょう。

チーム全体の理解を得る

アーキテクチャパターンはチーム全体で理解し、統一されたコーディングスタイルで実装することが重要です。ドキュメント化とコードレビューを徹底してください。

段階的な導入

既存システムに導入する場合は、一度にすべてをリファクタリングするのではなく、段階的に適用していくことをお勧めします。

AI サービス統合の未来

Nano Banana をはじめとする AI サービスは、今後さらに多様化し、高機能化していくでしょう。拡張可能なモジュール設計を採用することで、将来の技術変化にも柔軟に対応できるシステムを構築できます。

本記事で紹介したパターンは、AI サービスだけでなく、あらゆる外部サービスとの統合に応用できます。ぜひ、ご自身のプロジェクトに適用してみてください。

関連リンク