久しぶりにCodex(gpt-5.1-Codex-Max)とClaude Code(Opus4.5)を比較してみた
目次
- はじめに:Claude Opus 4.5のリリースをきっかけに
- 比較の前提条件とテスト環境
- 生成されたコードの技術スタック比較
- アーキテクチャ設計の違い
- ネットワーク実装の詳細比較
- コード品質と保守性の観点
- 開発体験と使いやすさ
- まとめ:どちらを選ぶべきか
免責事項
この記事は2025年12月4日時点での検証結果に基づいています。
- 使用したモデル、バージョン、設定により結果は異なる可能性があります
- 筆者の個人的な感覚や解釈が含まれている部分があります
- 両モデルは常に進化しており、将来的に結果が変わる可能性があります
- あくまで一つの比較事例として参考程度にご覧ください
1. はじめに:Claude Opus 4.5のリリースをきっかけに
1-1. 比較のきっかけ
2025年11月、AnthropicからClaude Opus 4.5がリリースされました。公式発表によると、コーディング、エージェント、コンピュータ使用において世界最高水準のパフォーマンスを実現したとのことです。
特に注目すべきポイント:
- SWE-bench Verified で最高スコアを達成:実際のソフトウェアエンジニアリングタスクでの性能向上
- 価格の大幅値下げ:$5/$25 per million tokensと、より実用的な価格帯に
- コーディング能力の飛躍的向上:従来モデルから大きく改善
これまでCodexは特にチーム開発において優れた選択肢とされてきました。理由は:
- 最新の技術スタックを積極的に採用
- モダンなベストプラクティスに準拠
- チーム開発で求められる構造化されたコード生成
しかし、Opus 4.5の登場で状況が大きく変わった可能性があります。 そこで、実際に同じタスクを両モデルに依頼し、詳細に比較してみることにしました。
1-2. 比較対象のモデル
-
Codex: GPT-5.1-Codex-Max(OpenAI)
- OpenAIの最新コーディング特化モデル
- GPT-5.1ベース
-
Claude Code: Claude Opus 4.5(Anthropic)
- Anthropicの最新フラッグシップモデル
- コーディングタスクで世界最高水準を主張
1-3. 比較の目的
この比較の目的は「どちらが優れているか」を決めることではなく、それぞれの特徴と適した使用場面を明らかにすることです。
具体的には:
- 同じAndroidアプリ開発要件でコード生成を依頼
- 技術スタック、アーキテクチャ、実装品質を多角的に分析
- 実際の開発現場でどちらがどのようなケースに適しているかを検証
重要な注意事項:この比較は筆者の環境・設定・タスクに基づいており、すべてのケースに当てはまるわけではありません。参考の一つとしてご覧ください。
2. 比較の前提条件とテスト環境
2-1. テストタスク
タスク概要
- プラットフォーム: Android(Jetpack Compose使用)
- アプリ種別: 業務用在庫管理アプリ
-
主な機能:
- ログイン画面
- 在庫一覧画面
- 在庫検索・フィルタリング
- ネットワーク連携(API通信)
- 状態管理
仕様書の内容
- UI/UXの詳細仕様
- データモデル定義
- API仕様
- 画面遷移フロー
- エラーハンドリング要件
2-2. 比較方法
検証プロセス
- 同一の仕様書を両モデルに提示
- それぞれにAndroidプロジェクトの生成を依頼
- 生成されたコードを以下の観点で分析:
- 技術スタックの選択
- アーキテクチャパターン
- コード品質
- 保守性
- 開発体験
評価の客観性を保つために
- 同じプロンプトを使用
- 生成されたコードをそのまま分析(人手での修正なし)
- 複数の観点から多角的に評価
2-3. 検証環境
- 開発環境: Android Studio (最新安定版)
- 検証日: 2025年12月4日
-
モデルバージョン:
- Codex: GPT-5.1-Codex-Max
- Claude Code: Opus 4.5
注意: この検証結果は上記の条件下でのものであり、異なる設定や時期では結果が変わる可能性があります。
3. 生成されたコードの技術スタック比較
まず、両モデルが選択した技術スタックを詳細に比較します。これは開発の方向性を大きく左右する重要な要素です。
3-1. ビルドツール・プラグイン
| カテゴリ | Claude Code (Opus 4.5) | Codex (GPT-5.1-Codex-Max) | 差分の意味 |
|---|---|---|---|
| Gradle | 8.4 | 8.13 | Codexはより新しいバージョン |
| AGP | 8.2.0 | 8.13.1 | Codexは最新のAndroid Gradle Plugin |
| Kotlin | 1.9.20 | 2.0.21 | Codexは大幅に新しい(Kotlin 2.0系) |
| Compose Compiler | 1.5.4 | Kotlin 2.0 組み込み | Kotlin 2.0でCompilerが統合された |
| Compile SDK | 34 | 36 | API 36はAndroid 15対応 |
| Target SDK | 34 | 36 | 同上 |
| Min SDK | 26 (Android 8.0) | 24 (Android 7.0) | より広い端末をサポート |
| Java Version | 17 | 11 | Claude Codeの方が新しいJava |
| Version Catalog | 不使用 | 使用 | Codexはモダンな依存管理 |
分析
Claude Codeの傾向
- 保守的なバージョン選択: API 34(Android 14)を選択
- 安定性重視: 検証済みの安定バージョンを優先
- プロダクション志向: 枯れた技術を採用
- より新しいJava: Java 17を選択(Gradle 8.4の推奨バージョン)
Codexの傾向
- 最新技術の積極採用: API 36(Android 15)、Kotlin 2.0.21
- モダンなツール: Version Catalogによる依存管理
- BOM活用: より新しいCompose BOM (2024.09.00)
- 先進性重視: 最新機能を活用可能
3-2. 主なライブラリ比較
| ライブラリカテゴリ | Claude Code | Codex | 特徴・差分 |
|---|---|---|---|
| DI (依存性注入) | Hilt 2.51.1 | なし | Claude Codeは業界標準DIを採用 |
| 状態管理 | ViewModel + StateFlow | Composable内State | アーキテクチャの違いを反映 |
| ネットワーク | Retrofit 2.9.0 + Gson | Retrofit 2.11.0 + Gson + RpcApiClient | Codexはより新しいRetrofit、ただしレガシーコード混在 |
| Navigation | 2.7.5 | 2.8.4 | Codexは最新Navigation |
| Compose BOM | 2024.02.00 | 2024.09.00 | Codexは最新Compose |
| Coroutines | 1.7.3 (明示的) | なし (Transitive依存のみ) | Claude Codeは明示的に管理 |
詳細分析
DI(依存性注入)フレームワーク
Claude Code: Hilt採用
@HiltAndroidApp
class InventoryApplication : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity()
@HiltViewModel
class InventoryViewModel @Inject constructor(
private val repository: InventoryRepository
) : ViewModel()
メリット:
- テスタビリティが高い
- 大規模プロジェクトに適している
- Android推奨のDIフレームワーク
デメリット:
- 学習コストが高い
- ボイラープレートが増える
- ビルド時間が増加
Codex: DIフレームワークなし
@Composable
fun LoginScreen(...) {
val repository = remember { InventoryRepository() }
// ...
}
メリット:
- シンプルで理解しやすい
- セットアップが不要
- ビルドが速い
デメリット:
- テストが困難
- スケールしにくい
- 依存関係が分散
状態管理の違い
Claude Code: ViewModel + StateFlow
@HiltViewModel
class InventoryViewModel @Inject constructor(...) : ViewModel() {
private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
val loginState: StateFlow<LoginState> = _loginState.asStateFlow()
private val _inventoryItems = MutableStateFlow<List<InventoryItem>>(emptyList())
val inventoryItems: StateFlow<List<InventoryItem>> = _inventoryItems.asStateFlow()
}
特徴:
- 状態が一元管理される
- Lifecycle-awareで安全
- テストが容易
- 大規模アプリに適している
Codex: Composable内State
@Composable
fun LoginScreen(...) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
}
特徴:
- コード量が少ない
- 学習コストが低い
- プロトタイプに最適
- 状態が分散しやすい
3-3. 技術スタック選択の哲学
Claude Code: "Production First"
- 業界標準のベストプラクティスを採用
- 長期保守を前提とした設計
- チーム開発を意識した構造
- 安定性・テスタビリティ重視
Codex: "Modern & Simple"
- 最新技術を積極的に採用
- シンプルさを優先
- 個人開発やプロトタイプに適した構造
- 先進性・学習容易性重視
3-4. Version Catalogの有無
Codex: Version Catalog使用
# gradle/libs.versions.toml
[versions]
kotlin = "2.0.21"
compose = "2024.09.00"
retrofit = "2.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
メリット:
- 依存関係のバージョン管理が一元化
- typoを防げる
- IDE補完が効く
- メンテナンス性が向上
Claude Code: 従来の方式
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
特徴:
- シンプルで直感的
- 従来のAndroid開発者には馴染み深い
- Version Catalogの学習不要
4. アーキテクチャ設計の違い
技術スタックの次に重要なのが、アーキテクチャパターンの選択です。ここに両モデルの設計哲学が最も明確に現れています。
4-1. アーキテクチャパターン
Claude Code: MVVM + Repository Pattern + DI
特徴:
- ✅ 明確な責務分離
- ✅ テストが容易
- ✅ スケーラブル
- ❌ ボイラープレートが多い
- ❌ 学習コストが高い
Codex: シンプルなComposable中心アーキテクチャ
特徴:
- ✅ シンプルで理解しやすい
- ✅ コード量が少ない
- ✅ セットアップが簡単
- ❌ テストが困難
- ❌ スケーラビリティに課題
4-2. 状態管理の実装詳細
Claude Code: 集中管理されたViewModel
@HiltViewModel
class InventoryViewModel @Inject constructor(
private val repository: InventoryRepository
) : ViewModel() {
// ログイン状態
private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
val loginState: StateFlow<LoginState> = _loginState.asStateFlow()
// 在庫アイテム
private val _inventoryItems = MutableStateFlow<List<InventoryItem>>(emptyList())
val inventoryItems: StateFlow<List<InventoryItem>> = _inventoryItems.asStateFlow()
// 検索クエリ
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
// UIからの操作
fun login(username: String, password: String) {
viewModelScope.launch {
_loginState.value = LoginState.Loading
val result = repository.login(username, password)
_loginState.value = when {
result.isSuccess -> LoginState.Success(result.getOrNull()!!)
else -> LoginState.Error(result.exceptionOrNull()?.message ?: "Unknown error")
}
}
}
fun loadInventoryItems() {
viewModelScope.launch {
_inventoryItems.value = repository.getInventoryItems()
}
}
fun updateSearchQuery(query: String) {
_searchQuery.value = query
}
}
// UI側での使用
@Composable
fun LoginScreen(viewModel: InventoryViewModel = hiltViewModel()) {
val loginState by viewModel.loginState.collectAsState()
when (loginState) {
is LoginState.Loading -> LoadingIndicator()
is LoginState.Success -> NavigateToHome()
is LoginState.Error -> ShowError((loginState as LoginState.Error).message)
else -> LoginForm()
}
}
メリット:
- 状態がすべて一箇所に集約
- Configuration変更に対して安全
- テストが容易(ViewModelを単独でテスト可能)
- 複雑な状態遷移を管理しやすい
デメリット:
- ボイラープレートコードが増える
- 小規模なアプリには過剰
- StateFlow/Flowの理解が必要
Codex: Composable内での状態管理
@Composable
fun LoginScreen(
onLoginSuccess: () -> Unit
) {
// ローカル状態
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
// Repositoryを直接インスタンス化
val repository = remember { InventoryRepository() }
val scope = rememberCoroutineScope()
Column {
TextField(
value = username,
onValueChange = { username = it }
)
TextField(
value = password,
onValueChange = { password = it }
)
Button(
onClick = {
scope.launch {
isLoading = true
errorMessage = null
try {
val result = repository.login(username, password)
onLoginSuccess()
} catch (e: Exception) {
errorMessage = e.message
} finally {
isLoading = false
}
}
}
) {
Text("Login")
}
if (isLoading) CircularProgressIndicator()
errorMessage?.let { Text(it, color = Color.Red) }
}
}
メリット:
- コードが短く、一目で理解できる
- Compose初心者にも分かりやすい
- セットアップが不要
- プロトタイプ開発に最適
デメリット:
- Configuration変更(画面回転等)で状態が失われる可能性
- テストが困難(UI層とロジックが密結合)
- 状態が複雑になると管理が難しい
- 複数画面で状態を共有する場合に課題
4-3. プロジェクト構造の違い
Claude Code: レイヤー分離された構造
app/src/main/java/com/example/inventoryapp/
├── InventoryApplication.kt # Hilt Application
├── MainActivity.kt # @AndroidEntryPoint
│
├── di/ # 依存性注入
│ └── AppModule.kt # Hiltモジュール
│ # (Retrofit, Repository等の提供)
│
├── viewmodel/ # プレゼンテーション層
│ └── InventoryViewModel.kt # 状態管理とビジネスロジック
│
├── data/ # データ層
│ ├── api/
│ │ ├── InventoryApiService.kt # API定義
│ │ └── interceptor/
│ │ └── AuthInterceptor.kt
│ ├── model/
│ │ ├── LoginRequest.kt
│ │ ├── LoginResponse.kt
│ │ └── InventoryItem.kt
│ └── repository/
│ └── InventoryRepository.kt # データアクセス抽象化
│
└── ui/ # UI層
├── components/ # 再利用可能なコンポーネント
│ ├── LoadingIndicator.kt
│ └── InventoryItemCard.kt
├── navigation/
│ └── Navigation.kt # ナビゲーショングラフ
├── screens/
│ ├── LoginScreen.kt
│ └── InventoryListScreen.kt
└── theme/
├── Color.kt
├── Theme.kt
└── Type.kt
特徴:
- 明確な関心の分離: 各層が独立
- スケーラブル: 機能追加が容易
- チーム開発向け: 担当分けがしやすい
- テストしやすい: 各層を独立してテスト可能
Codex: フラットでシンプルな構造
app/src/main/java/com/example/inventoryapp/
├── MainActivity.kt # 標準Activity
│
├── navigation/
│ └── AppNavigation.kt # ナビゲーション
│
└── ui/
├── components/ # 共通コンポーネント
│ └── InventoryCard.kt
├── data/ # データ層がUIパッケージ内
│ ├── ApiClientProvider.kt # Retrofitシングルトン
│ ├── InventoryApiService.kt
│ └── InventoryRepository.kt
├── screens/
│ ├── LoginScreen.kt
│ ├── InventoryListScreen.kt
│ └── AllScreensPreview.kt # 全画面プレビュー集約
└── theme/
├── Color.kt
├── Theme.kt
└── Type.kt
特徴:
- フラット構造: 階層が少なく見通しが良い
- シンプル: すぐに理解できる
- 個人開発向け: 小規模プロジェクトに適している
- データ層がUI内: やや関心の分離が弱い
4-4. アーキテクチャ選択の影響
チーム開発の場合
| 観点 | Claude Code | Codex |
|---|---|---|
| 新メンバーのオンボーディング | 時間がかかるがパターンが明確 | 早いが将来的な拡張で混乱 |
| 並行開発 | 各層を独立して開発可能 | UIとデータが密結合で衝突しやすい |
| コードレビュー | 変更範囲が明確 | 広範囲に影響が出やすい |
| 機能追加 | 既存構造に沿って追加 | 構造が不明確になりがち |
個人開発の場合
| 観点 | Claude Code | Codex |
|---|---|---|
| 初期開発速度 | セットアップに時間がかかる | 非常に速い |
| プロトタイプ作成 | やや重い | 最適 |
| 長期保守 | メンテナンスしやすい | 複雑化すると困難 |
5. ネットワーク実装の詳細比較
API通信は多くのアプリで重要な要素です。ここでは、両モデルがどのようにネットワーク層を実装したかを詳しく見ていきます。
5-1. ネットワーク層の実装アプローチ
Claude Code: Hilt DIによる Retrofit管理
// di/AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer mock-token")
.build()
chain.proceed(request)
}
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideInventoryApiService(retrofit: Retrofit): InventoryApiService {
return retrofit.create(InventoryApiService::class.java)
}
@Provides
@Singleton
fun provideInventoryRepository(apiService: InventoryApiService): InventoryRepository {
return InventoryRepository(apiService)
}
}
特徴:
- ✅ シングルトンパターンで適切に管理
- ✅ すべてのリクエストに統一的に認証ヘッダーを追加
- ✅ タイムアウト設定が統一(30秒)
- ✅ DI経由でテスト時にモック化が容易
- ❌ BASE_URLが固定(環境切り替えの仕組みなし)
- ❌ インターセプターですべてに認証を追加(ログインにも)
**Codex: シングルトンProvider **
// ui/data/ApiClientProvider.kt
object ApiClientProvider {
private const val BASE_URL = "https://api.example.com/"
private val FALLBACK_URLS = listOf(
"https://api-backup1.example.com/",
"https://api-backup2.example.com/"
)
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService: InventoryApiService = retrofit.create(InventoryApiService::class.java)
}
特徴:
- ✅ フォールバックURL対応(冗長性)
- ✅ 異なるタイムアウト設定(接続・読み取り・書き込み)
- ✅ Objectパターンでシンプルにシングルトン化
- ❌ 手動JSONパースのコードが含まれる
- ❌ テスト時のモック化が困難
5-2. 認証処理の実装
Claude Code: インターセプターによる統一認証
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor { chain ->
val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer mock-token")
.build()
chain.proceed(newRequest)
}
.build()
}
問題点:
- すべてのリクエストに認証ヘッダーが付く
- ログインリクエストにも
Authorizationヘッダーが付いてしまう - リフレッシュトークンの仕組みがない
改善案:
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// ログインエンドポイントは認証不要
if (request.url.encodedPath.contains("/connect/token")) {
return chain.proceed(request)
}
// トークンを追加
val token = tokenProvider.getToken()
val newRequest = request.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(newRequest)
}
}
Codex: エンドポイント別の認証
interface InventoryApiService {
// 認証不要
@POST("connect/token")
suspend fun login(
@Body request: LoginRequest
): Response<LoginResponse>
// 認証ヘッダーをパラメータで指定
@GET("api/stock/items")
suspend fun getInventoryItems(
@Header("Authorization") auth: String
): Response<List<InventoryItem>>
}
// 使用例
class InventoryRepository {
suspend fun getInventoryItems(): List<InventoryItem> {
val token = "Bearer ${getStoredToken()}"
val response = apiService.getInventoryItems(token)
return response.body() ?: emptyList()
}
}
特徴:
- エンドポイントごとに認証を制御
- ログインには認証ヘッダーを付けない
- より柔軟だがコードが分散
5-3. エラーハンドリングの違い
Claude Code: Result型によるラッピング
class InventoryRepository @Inject constructor(
private val apiService: InventoryApiService
) {
suspend fun login(username: String, password: String): Result<LoginResponse> {
return try {
val response = apiService.login(LoginRequest(username, password))
if (response.isSuccessful && response.body() != null) {
Result.success(response.body()!!)
} else {
Result.failure(Exception("Login failed: ${response.code()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getInventoryItems(): List<InventoryItem> {
return try {
val response = apiService.getInventoryItems()
if (response.isSuccessful) {
response.body() ?: emptyList()
} else {
emptyList()
}
} catch (e: Exception) {
emptyList()
}
}
}
特徴:
- Kotlin標準の
Result型を使用 - 成功・失敗を型で表現
- エラーケースを明示的に処理
- ViewModelで適切にハンドリング可能
Codex: 直接的な例外処理
class InventoryRepository {
private val apiService = ApiClientProvider.apiService
suspend fun login(username: String, password: String): LoginResponse {
val response = apiService.login(LoginRequest(username, password))
if (!response.isSuccessful) {
throw Exception("Login failed: ${response.code()}")
}
return response.body() ?: throw Exception("Empty response body")
}
suspend fun getInventoryItems(): List<InventoryItem> {
val response = apiService.getInventoryItems("Bearer token")
return response.body() ?: emptyList()
}
}
// Composable側での使用
@Composable
fun LoginScreen() {
val repository = remember { InventoryRepository() }
LaunchedEffect(Unit) {
try {
val result = repository.login(username, password)
// 成功処理
} catch (e: Exception) {
// エラー処理
errorMessage = e.message
}
}
}
特徴:
- シンプルで直感的
- try-catchで分かりやすい
- エラーメッセージがそのまま使える
6. コード品質と保守性の観点
実際の開発現場では、初期開発速度だけでなく、長期的な保守性が重要です。
6-1. テスタビリティ
Claude Code: 高いテスタビリティ
// ViewModelの単体テスト例
class InventoryViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: InventoryViewModel
private lateinit var mockRepository: InventoryRepository
@Before
fun setup() {
mockRepository = mock()
viewModel = InventoryViewModel(mockRepository)
}
@Test
fun `login success updates state correctly`() = runTest {
// Given
val mockResponse = LoginResponse(token = "test-token")
whenever(mockRepository.login(any(), any())).thenReturn(Result.success(mockResponse))
// When
viewModel.login("user", "pass")
// Then
val state = viewModel.loginState.value
assert(state is LoginState.Success)
assert((state as LoginState.Success).response.token == "test-token")
}
}
// Repositoryの単体テスト例
class InventoryRepositoryTest {
private lateinit var repository: InventoryRepository
private lateinit var mockApiService: InventoryApiService
@Before
fun setup() {
mockApiService = mock()
repository = InventoryRepository(mockApiService)
}
@Test
fun `getInventoryItems returns list on success`() = runTest {
// Given
val mockItems = listOf(InventoryItem(id = "1", name = "Item"))
val mockResponse = Response.success(mockItems)
whenever(mockApiService.getInventoryItems()).thenReturn(mockResponse)
// When
val result = repository.getInventoryItems()
// Then
assertEquals(mockItems, result)
}
}
テスト可能な理由:
- DI経由で依存を注入できる
- ViewModelとRepositoryが分離
- StateFlowでテストしやすい
- Hiltのテストサポート
Codex: 低いテスタビリティ
// Composableのテスト(困難)
@Composable
fun LoginScreen() {
val repository = remember { InventoryRepository() } // 直接インスタンス化
var username by remember { mutableStateOf("") }
// どうやってrepositoryをモックする?
// どうやって内部状態をテストする?
}
テストが困難な理由:
- Repositoryが直接インスタンス化される
- 状態がComposable内に分散
- DI機構がない
- UI層とロジックが密結合
比較表
| テストの種類 | Claude Code | Codex | 差分 |
|---|---|---|---|
| ViewModelの単体テスト | 容易 | ViewModelがないため不可 | - |
| Repositoryの単体テスト | 容易(DI) | 可能だが手間がかかる | モック化の難易度 |
| UIテスト | 中程度 | 困難 | 状態管理の差 |
| 統合テスト | 容易(Hiltサポート) | 困難 | DIの有無 |
| 全体のテストカバレッジ | 80%+ 達成可能 | 50%程度が限界 | アーキテクチャの差 |
6-2. コードの保守性
Claude Code: 高い保守性
メリット:
- ✅ 明確な責務分離: 各クラスの役割が明確
- ✅ 単一責任原則: 各クラスが一つのことだけを行う
- ✅ 変更の局所化: 修正が一箇所で済む
- ✅ パターンの一貫性: Android推奨のパターン
- ✅ 拡張性: 新機能追加が容易
デメリット:
- ❌ ボイラープレート: コード量が多い
- ❌ 学習コスト: Hilt、MVVM、StateFlowの理解が必要
- ❌ 初期開発速度: セットアップに時間がかかる
具体例: 新しいAPI追加
// 1. APIインターフェースに追加
interface InventoryApiService {
@GET("api/stock/search")
suspend fun searchInventory(@Query("query") query: String): Response<List<InventoryItem>>
}
// 2. Repositoryに追加
class InventoryRepository @Inject constructor(private val apiService: InventoryApiService) {
suspend fun searchInventory(query: String): List<InventoryItem> {
val response = apiService.searchInventory(query)
return response.body() ?: emptyList()
}
}
// 3. ViewModelに追加
@HiltViewModel
class InventoryViewModel @Inject constructor(private val repository: InventoryRepository) {
fun searchInventory(query: String) {
viewModelScope.launch {
_searchResults.value = repository.searchInventory(query)
}
}
}
// 4. UIで使用
@Composable
fun SearchScreen(viewModel: InventoryViewModel = hiltViewModel()) {
// ...
}
→ 各層で明確に役割分担されており、影響範囲が限定的
Codex: 中程度の保守性
メリット:
- ✅ シンプル: コード量が少ない
- ✅ 学習コスト低: Compose基本のみで理解可能
- ✅ 最新技術: Kotlin 2.0、最新API
デメリット:
- ❌ 責務の混在: UIとロジックが混ざる
- ❌ 状態の分散: どこに何があるか分かりにくくなる
- ❌ スケールしにくい: 機能が増えると複雑化
具体例: 同じAPI追加
// 1. APIインターフェースに追加
interface InventoryApiService {
@GET("api/stock/search")
suspend fun searchInventory(
@Query("query") query: String,
@Header("Authorization") auth: String
): Response<List<InventoryItem>>
}
// 2. Repositoryに追加
class InventoryRepository {
suspend fun searchInventory(query: String): List<InventoryItem> {
val token = "Bearer ..." // トークンを取得
val response = apiService.searchInventory(query, token)
return response.body() ?: emptyList()
}
}
// 3. Composableに直接実装
@Composable
fun SearchScreen() {
val repository = remember { InventoryRepository() }
var searchResults by remember { mutableStateOf<List<InventoryItem>>(emptyList()) }
var query by remember { mutableStateOf("") }
LaunchedEffect(query) {
if (query.isNotEmpty()) {
searchResults = repository.searchInventory(query)
}
}
// UI実装...
}
→ Composable内にロジックが増え、テストが困難に
6-3. コードレビューのしやすさ
Claude Code: レビューしやすい
Pull Request: Add search feature
Files changed: 4
1. InventoryApiService.kt (+3 lines)
- searchInventory APIの追加
2. InventoryRepository.kt (+5 lines)
- searchInventory repositoryメソッドの追加
3. InventoryViewModel.kt (+10 lines)
- searchInventory ViewModelメソッドの追加
- _searchResults StateFlowの追加
4. SearchScreen.kt (+50 lines)
- 検索UI の実装
→ 変更が各層に明確に分かれており、レビューしやすい
Codex: レビューが複雑
Pull Request: Add search feature
Files changed: 2
1. InventoryApiService.kt (+3 lines)
- searchInventory APIの追加
2. SearchScreen.kt (+100 lines)
- APIとの通信ロジック
- 状態管理
- エラーハンドリング
- UI実装
すべてが混在
→ 一つのファイルに多くのロジックが含まれ、レビューが大変
6-4. 長期保守の観点
3年後のプロジェクトを想定
| 観点 | Claude Code | Codex |
|---|---|---|
| コードベースのサイズ | 大きいが整理されている | 小さいが混沌としている |
| 新規メンバーの理解 | パターンが明確で理解しやすい | シンプルだが一貫性がない |
| バグの発見 | 層ごとに切り分けしやすい | どこが原因か特定しにくい |
| リファクタリング | 局所的に可能 | 広範囲に影響 |
| 技術スタックの更新 | 段階的に可能 | 全体的に影響 |
7. 開発体験と使いやすさ
コードの品質だけでなく、開発者の体験も重要です。
7-1. 学習コストの比較
Claude Code: 中〜高
必要な知識:
- Jetpack Compose基礎 (必須)
- Kotlin Coroutines (必須)
- ViewModel + StateFlow (必須)
- Hilt DI (必須)
- Repository Pattern (推奨)
- MVVM Architecture (推奨)
学習曲線:
難易度
高 ┤ ████
┤ ████
┤ ████
中 ┤ ████
┤ ████
低 ┤ ████
└────────────────────────> 時間
初日 1週間 2週間 1ヶ月
初心者の感想(想定):
"最初は難しいけど、パターンが分かれば迷わない。
大規模プロジェクトでも安心して書ける。"
Codex: 低〜中
必要な知識:
- Jetpack Compose基礎 (必須)
- Kotlin Coroutines (基本のみ)
- remember / mutableStateOf (必須)
学習曲線:
難易度
高 ┤
┤
┤ ████
中 ┤ ████
┤ ████
低 ┤██
└────────────────────────> 時間
初日 1週間 2週間 1ヶ月
初心者の感想(想定):
"すぐに書き始められる!でも、アプリが大きくなると
どう構造化すればいいか分からなくなった..."
7-2. プレビュー機能の違い
Claude Code: 個別プレビュー
// screens/LoginScreen.kt
@Preview(showBackground = true)
@Composable
fun LoginScreenPreview() {
InventoryTheme {
LoginScreen(
onLoginSuccess = {}
)
}
}
// screens/InventoryListScreen.kt
@Preview(showBackground = true)
@Composable
fun InventoryListScreenPreview() {
InventoryTheme {
InventoryListScreen(
inventoryItems = listOf(
InventoryItem("1", "Item 1", 10),
InventoryItem("2", "Item 2", 5)
)
)
}
}
特徴:
- 各画面ファイルに個別にプレビュー
- 該当ファイルを開けばすぐプレビュー確認可能
- 画面ごとに異なるテストデータを設定しやすい
Codex: 集約プレビュー
// screens/AllScreensPreview.kt
@Preview(showBackground = true, name = "Login Screen")
@Composable
fun LoginScreenPreview() {
InventoryTheme {
LoginScreen(onLoginSuccess = {})
}
}
@Preview(showBackground = true, name = "Stock List Screen")
@Composable
fun InventoryListScreenPreview() {
InventoryTheme {
InventoryListScreen(
inventoryItems = listOf(
InventoryItem("1", "Item 1", 10),
InventoryItem("2", "Item 2", 5)
)
)
}
}
@Preview(showBackground = true, name = "Search Screen")
@Composable
fun SearchScreenPreview() {
InventoryTheme {
SearchScreen()
}
}
// ... すべての画面のプレビューが一箇所に
特徴:
- ✅ すべてのプレビューを一覧で確認できる
- ✅ 画面間の一貫性を確認しやすい
- ✅ デザイン全体の把握が容易
- ❌ ファイルが大きくなりがち
- ❌ 画面固有のファイルを見てもプレビューがない
7-3. コード生成の特徴
Claude Code: "Production Ready"志向
// 生成されるコードの特徴
@HiltViewModel
class InventoryViewModel @Inject constructor(
private val repository: InventoryRepository,
private val savedStateHandle: SavedStateHandle // 状態保存も考慮
) : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
init {
loadData()
}
private fun loadData() {
viewModelScope.launch {
_state.value = UiState.Loading
try {
val data = repository.getData()
_state.value = UiState.Success(data)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
// エラーハンドリング、ログ、状態保存などが含まれる
}
特徴:
- エッジケースを考慮
- エラーハンドリングが丁寧
- 状態遷移が明確
- すぐにプロダクションに投入できるレベル
Codex: "Quick Start"志向
// 生成されるコードの特徴
@Composable
fun Screen() {
var data by remember { mutableStateOf<List<Item>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
isLoading = true
data = repository.getData()
isLoading = false
}
// シンプルで分かりやすい
if (isLoading) LoadingIndicator()
else ItemList(data)
}
特徴:
- 最小限のコード
- すぐに動く
- プロトタイプに最適
- エッジケース対応は自分で追加が必要
7-4. エラーメッセージとデバッグ
Claude Code: 明確なエラー(フロントエンド開発に強い)
sealed class LoginState {
object Idle : LoginState()
object Loading : LoginState()
data class Success(val response: LoginResponse) : LoginState()
data class Error(val message: String) : LoginState() // エラーメッセージが型で表現
}
// エラーが発生した場所が明確
viewModel.loginState.collect { state ->
when (state) {
is LoginState.Error -> {
// state.message でエラー内容がわかる
// ViewModelのどのメソッドで発生したかも明確
}
}
}
特徴:
- 型システムによる明確なエラー管理
- フロントエンドの状態管理に最適
- UI層でのエラー表示が容易
Codex: エラー追跡がやや困難(ただしバグ解析は得意)
@Composable
fun Screen() {
var error by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
try {
// いろいろな処理
} catch (e: Exception) {
error = e.message
// どこで発生したエラーか特定しにくい
}
}
}
特徴:
- シンプルなエラー処理
- ただし、複雑なバグの根本原因解析ではCodexの論理的推論が強み
- スタックトレースやログから原因を特定する能力に優れる
7-5. IDE サポート
Claude Code: 優れたIDE補完
// Hiltによる型安全な注入
@Inject constructor(
private val repository: InventoryRepository // IDEが自動補完
)
// StateFlowの型が明確
val state: StateFlow<LoginState> // IDEが状態遷移を追跡可能
Codex: 基本的な補完のみ
// 直接インスタンス化
val repository = InventoryRepository() // IDEは依存を追跡できない
// 型推論に頼る
var state by remember { mutableStateOf(...) } // 型が不明確な場合も
8. まとめ:どちらを選ぶべきか
8-1. それぞれの強みと適したケース
Claude Code (Opus 4.5) が向いている場面
✅ チーム開発
- 複数人で並行開発
- コードレビューが必須
- 長期的な保守が必要
✅ プロダクション品質が必要
- エンタープライズアプリ
- 大規模なアプリケーション
- 高い品質基準が求められる
✅ テストが重要
- TDD/BDDを実践
- 高いテストカバレッジが必要
- CI/CDパイプラインがある
✅ 長期プロジェクト
- 1年以上の開発期間
- 継続的な機能追加
- リファクタリングが予定されている
具体例:
- 金融アプリ
- 医療系アプリ
- 大企業の業務アプリ
- 長期運用が前提のサービス
Codex (GPT-5.1-Codex-Max) が向いている場面
✅ プロトタイプ・MVP開発
- アイデアの検証
- デモ作成
- 最小限の機能で素早くリリース
✅ 個人開発
- 一人で開発
- 小〜中規模のアプリ
- 趣味のプロジェクト
✅ 学習目的
- Compose を学んでいる
- Android開発の初心者
- シンプルな構造で理解したい
✅ 最新技術の採用
- Kotlin 2.0の機能を使いたい
- 最新のAPIを試したい
- 最先端の開発スタイルを学びたい
具体例:
- スタートアップのMVP
- ハッカソンのプロジェクト
- 個人の学習アプリ
- 技術検証プロジェクト
8-2. Opus 4.5登場による状況の変化
以前(Opus 4.5以前)の状況
Codexの優位性:
- チーム開発での構造化されたコード
- モダンなベストプラクティス
- より実用的な実装
現在(Opus 4.5登場後)
Claude Codeが大きく改善:
- ✅ プロダクション品質のコード生成
- ✅ 業界標準のアーキテクチャ採用
- ✅ テスタビリティの高い実装
- ✅ エンタープライズレディなコード
Codexの独自の強み:
- ✅ 最新技術スタックの採用
- ✅ シンプルさを保つ設計
- ✅ プロトタイプ開発での速度
結論:
Opus 4.5の登場で、チーム開発・プロダクション品質が必要なケースではClaude Codeが優位に立った印象です。特にフロントエンド開発においてClaude Codeの構造化能力は際立っています。
一方、Codexは最新技術の採用とシンプルさで引き続き魅力的です。また、複雑なバグの解析や原因特定においてはCodexが強みを発揮する場面も多く見られます。
8-4. 実践的な選択基準
プロジェクト規模で選ぶ
品質要件で選ぶ
| 要件 | Claude Code | Codex | 理由 |
|---|---|---|---|
| テストカバレッジ > 70% | ◎ | △ | アーキテクチャの違い |
| コードレビュー必須 | ◎ | ○ | 責務分離の明確さ |
| 新人が多い | ○ | ◎ | 学習コストの違い |
| 納期が短い | △ | ◎ | 初期開発速度 |
| 長期保守 | ◎ | △ | 保守性の違い |
| 最新技術必須 | ○ | ◎ | バージョン選択の傾向 |
| フロントエンド開発 | ◎ | ○ | 構造化された状態管理 |
| 複雑なバグ解析 | ○ | ◎ | 論理的推論能力 |
8-5. ハイブリッドアプローチ
実は両方を使い分けることも可能
フェーズ1(プロトタイプ): Codex
├─ 素早くMVP作成
├─ アイデア検証
└─ ユーザーフィードバック取得
↓ 移行
フェーズ2(プロダクション): Claude Code
├─ アーキテクチャ再設計
├─ テスト追加
├─ チーム開発体制構築
└─ 長期運用開始
8-6. 最終的な結論
プロジェクトの成功のために
どちらが「優れている」かではなく、プロジェクトの性質・チームの方針・要件に応じて適切に選択することが重要です。
Claude Code (Opus 4.5):
- 保守性・テスタビリティを重視
- プロダクション品質のコード
- チーム開発に最適
- 長期プロジェクト向け
Codex (GPT-5.1-Codex-Max):
- シンプルさ・最新技術を重視
- 素早いプロトタイピング
- 個人開発に最適
- 学習にも適している
Opus 4.5の登場により、従来Codexが持っていた「チーム開発での優位性」はClaude Codeに移った印象があります。特にフロントエンド開発においてClaude Codeの構造化・保守性重視の設計が光ります。
しかし、Codexも独自の強みを持ち続けています:
- 最新技術採用とシンプルさによるプロトタイピングの速さ
- 複雑なバグの解析や原因特定において、Codexの論理的な推論能力が役立つ場面が多い
重要: この比較は2025年12月4日時点のものであり、両モデルとも進化を続けています。将来的に結果が変わる可能性があることをご理解ください。また、筆者の個人的な感覚や評価基準が含まれている点もご了承ください。