Adapter Pattern
プロンプトに「Adapter」を含めることで、ClaudeCodeは外部ライブラリやAPIのインターフェースを内部仕様に適合させる実装を生成できます。弊社では、サードパーティAPI統合、レガシーシステムとの連携、データフォーマット変換などで既存のインターフェースを変更せずに互換性を持たせたい場合にAdapterパターンを使用します。
弊社の推奨ルール
- 外部依存の分離 - 外部ライブラリへの依存を抽象化
- インターフェースの統一 - 内部仕様に合わせたメソッド提供
- データ変換の責務 - 外部フォーマットと内部フォーマットの相互変換
- テスト容易性 - モックやスタブでのテスト実行を可能にする
- 既存コードの保護 - 外部システム変更時の影響を最小化
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: 変換処理のオーバーヘッドがありますが、保守性の向上によるメリットの方が大きいです。