Interactive

Adapter Pattern

プロンプトに「Adapter」を含めることで、ClaudeCodeは外部ライブラリやAPIのインターフェースを内部仕様に適合させる実装を生成できます。弊社では、サードパーティAPI統合、レガシーシステムとの連携、データフォーマット変換などで既存のインターフェースを変更せずに互換性を持たせたい場合にAdapterパターンを使用します。

弊社の推奨ルール

  1. 外部依存の分離 - 外部ライブラリへの依存を抽象化
  2. インターフェースの統一 - 内部仕様に合わせたメソッド提供
  3. データ変換の責務 - 外部フォーマットと内部フォーマットの相互変換
  4. テスト容易性 - モックやスタブでのテスト実行を可能にする
  5. 既存コードの保護 - 外部システム変更時の影響を最小化

ClaudeCodeでの利用

APIレスポンスの変換

外部APIの形式を内部モデルに変換する場合

「このAPIレスポンスを内部モデルに変換するAdapterパターンを実装して:
[外部APIのレスポンス例をペースト]」

外部ライブラリの統合

ライブラリ固有のAPIを内部仕様に統一する場合

「外部ライブラリのインターフェースを弊社の標準インターフェースに適合させるAdapter作成」

レガシーシステムとの連携

異なるプロトコルやデータ形式を扱う場合

「レガシーシステムとの連携用Adapterを実装。データ形式とプロトコルの違いを吸収して」

直接的な外部API呼び出しではなくAdapterを使う

外部APIを直接呼び出すと、APIの仕様変更時に複数箇所の修正が必要になります。

// ❌ 避ける:外部APIの直接呼び出し
class UserService {
  async getUser(id: string): Promise<User> {
    const response = await fetch(`https://api.github.com/users/${id}`);
    const githubUser = await response.json();
    
    // データ変換ロジックがサービス層に混在
    return {
      id: githubUser.id.toString(),
      name: githubUser.name || githubUser.login,
      email: githubUser.email || ''
    };
  }
}

Adapterパターンで外部依存を一箇所に集約し、内部システムへの影響を最小化します。

// ✅ 推奨:内部システム用のインターフェース
interface UserRepository {
  getUser(id: string): Promise<InternalUser>;
  listUsers(limit: number): Promise<readonly InternalUser[]>;
}

type InternalUser = {
  readonly id: string;
  readonly name: string;
  readonly email: string;
};

外部API用のAdapter実装でインターフェースを統一します。GitHubの具体的なAPIの詳細を隠蔽し、内部システムで使いやすい形式に変換します。

// GitHub API用のAdapter実装
class GitHubUserAdapter implements UserRepository {
  constructor(private readonly apiToken: string) {}
  
  async getUser(id: string): Promise<InternalUser> {
    const response = await fetch(`https://api.github.com/users/${id}`, {
      headers: { 'Authorization': `token ${this.apiToken}` }
    });
    
    const githubUser = await response.json();
    return this.convertToInternalUser(githubUser);
  }
  
  private convertToInternalUser(githubUser: any): InternalUser {
    return {
      id: githubUser.id.toString(),
      name: githubUser.name || githubUser.login,
      email: githubUser.email || ''
    };
  }
}

サービス層では統一されたインターフェースを使用します。

// サービス層では統一されたインターフェース
class UserService {
  constructor(private readonly userRepository: UserRepository) {}
  
  async getUser(id: string): Promise<InternalUser> {
    return this.userRepository.getUser(id);
  }
}

依存性注入によりAdapterを注入することで、外部システムの変更に対する柔軟性を確保します。テストでもモックAdapterを簡単に使用できます。

// 異なる外部システムへの切り替えが容易
const githubAdapter = new GitHubUserAdapter(process.env.GITHUB_TOKEN);
const userService = new UserService(githubAdapter);

このパターンにより、外部API仕様の変更時はAdapterクラスのみを修正すれば済み、サービス層のコードは変更不要になります。

データベースライブラリのAdapter

異なるORMライブラリを統一インターフェースで扱う実装例です。

// 統一されたデータアクセスインターフェース
interface PostRepository {
  findById(id: string): Promise<Post>;
  create(data: CreatePostData): Promise<Post>;
  update(id: string, data: UpdatePostData): Promise<Post>;
}

// Prisma用のAdapter
class PrismaPostAdapter implements PostRepository {
  constructor(private readonly prisma: PrismaClient) {}
  
  async findById(id: string): Promise<Post> {
    const prismaPost = await this.prisma.post.findUnique({
      where: { id: parseInt(id) }
    });
    return this.convertToDomainModel(prismaPost);
  }
  
  async create(data: CreatePostData): Promise<Post> {
    const prismaPost = await this.prisma.post.create({
      data: {
        title: data.title,
        content: data.content,
        authorId: parseInt(data.authorId)
      }
    });
    return this.convertToDomainModel(prismaPost);
  }
}

// TypeORM用のAdapter(同じインターフェースで実装)
class TypeORMPostAdapter implements PostRepository {
  constructor(private readonly repository: Repository<PostEntity>) {}
  
  async findById(id: string): Promise<Post> {
    const entity = await this.repository.findOne({ where: { id: parseInt(id) } });
    return this.convertToDomainModel(entity);
  }
}

よくある問題

過度な変換処理

Adapterで不要なデータ変換を行ってしまう問題です。必要最小限の変換に留めることで解決できます。

// ❌ 過度な変換
class OverComplexAdapter implements UserRepository {
  async getUser(id: string): Promise<InternalUser> {
    const response = await this.fetchFromAPI(id);
    
    // 不要な処理まで含めてしまう
    const processedData = this.validateData(response);
    const enrichedData = await this.enrichWithExternalData(processedData);
    const cachedData = await this.cacheData(enrichedData);
    
    return this.convertToInternalUser(cachedData);
  }
}

// ✅ 適切な変換(最小限の責務)
class CleanAdapter implements UserRepository {
  async getUser(id: string): Promise<InternalUser> {
    const response = await this.fetchFromAPI(id);
    return this.convertToInternalUser(response); // 変換のみに集中
  }
  
  private convertToInternalUser(apiUser: any): InternalUser {
    return {
      id: apiUser.id.toString(),
      name: apiUser.name,
      email: apiUser.email
    };
  }
}

インターフェースの設計ミス

Adapterのインターフェースが特定の外部システムに依存してしまう問題です。汎用的なインターフェースを設計することで解決できます。

// ❌ 特定システムに依存したインターフェース
interface GitHubSpecificRepository {
  getGitHubUser(login: string): Promise<GitHubUser>;
  getGitHubRepos(login: string): Promise<GitHubRepo[]>;
}

// ✅ 汎用的なインターフェース
interface UserRepository {
  getUser(identifier: string): Promise<User>;
  getUserProjects(userId: string): Promise<readonly Project[]>;
}

Q: いつAdapterパターンを使うべき?

A: 弊社では外部システムとのインターフェースが異なる場合、またはレガシーコードとの統合時に使用します。

Q: Facadeパターンとの違いは?

A: Adapterは既存インターフェースを変換、Facadeは複雑なシステムを簡単なインターフェースで提供します。

Q: パフォーマンスへの影響は?

A: 変換処理のオーバーヘッドがありますが、保守性の向上によるメリットの方が大きいです。