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 が統合されている主要なプラットフォームをまとめたものです。
| # | プラットフォーム | 統合方法 | 利用形態 |
|---|---|---|---|
| 1 | Google AI Studio | 直接アクセス | Web UI での操作 |
| 2 | Vertex AI | Google Cloud API | エンタープライズ向け API |
| 3 | Adobe Photoshop | プラグイン統合 | デスクトップアプリ内での利用 |
| 4 | Leonardo.ai | プラットフォーム統合 | Web サービス経由 |
| 5 | OpenRouter | API プロキシ | 統一 API 経由でのアクセス |
| 6 | CometAPI | マルチモデル 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 パイプラインの遅延 |
| 2 | API 利用料金が発生 | テストのたびにコストがかかる |
| 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 ステータス | 再試行 | 対処方法 |
|---|---|---|---|---|
| 1 | INVALID_REQUEST | 400 | 不可 | リクエストパラメータを修正 |
| 2 | AUTHENTICATION_FAILED | 401, 403 | 不可 | API キーを確認 |
| 3 | QUOTA_EXCEEDED | 429 | 可能 | レート制限解除まで待機 |
| 4 | SERVICE_UNAVAILABLE | 500-599 | 可能 | バックオフ戦略で再試行 |
| 5 | TIMEOUT | - | 可能 | タイムアウト時間を延長して再試行 |
モニタリングとロギング
実運用では、パフォーマンスとエラーをモニタリングする必要があります。
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 サービスだけでなく、あらゆる外部サービスとの統合に応用できます。ぜひ、ご自身のプロジェクトに適用してみてください。
関連リンク
- Google AI Studio - Nano Banana 公式利用環境
- Gemini API Documentation - Nano Banana API 公式ドキュメント
- Hexagonal Architecture (Ports and Adapters) - ヘキサゴナルアーキテクチャの原典
- Design Patterns: Elements of Reusable Object-Oriented Software - GoF デザインパターン
- Clean Architecture - クリーンアーキテクチャの解説
- Martin Fowler's Patterns of Enterprise Application Architecture - エンタープライズアプリケーションパターン集
articleNano Banana で拡張可能なモジュール設計:プラグイン/アダプタ/ポート&ドライバ
articleNano Banana チートシート:よく使う CLI/API/設定の一枚まとめ
articleNano Banana のインストール完全ガイド:macOS/Windows/Linux 別の最短手順
articleNano Banana とは?ゼロからわかる特徴・できること・向いている用途【2025 年版】
articleNext.js を Bun で動かす開発環境:起動速度・互換性・落とし穴
articleObsidian Properties 速見表:型・表示名・テンプレ連携の実例カタログ
articleNuxt useHead/useSeoMeta 定番スニペット集:OGP/構造化データ/国際化メタ
articleMermaid で描ける図の種類カタログ:flowchart/class/state/journey/timeline ほか完全整理
articleMCP サーバーを活用した AI チャットボット構築:実用的な事例と実装
articleNginx 変数 100 選:$request_id/$upstream_status/$ssl_protocol ほか即戦力まとめ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来