Claude Code Plugins

Community-maintained marketplace

Feedback

プロトコル指向設計の支援。Protocol拡張、Associated Types設計、依存性注入パターンを提案。「プロトコルを設計して」「依存性注入を実装して」で使用。

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name swift-protocol
description プロトコル指向設計の支援。Protocol拡張、Associated Types設計、依存性注入パターンを提案。「プロトコルを設計して」「依存性注入を実装して」で使用。

Swift Protocol

Swiftのプロトコル指向プログラミングを活用した設計を支援する。

概要

プロトコル指向設計に対して以下の観点で支援を実施:

  • Protocol定義とExtensionの活用
  • Associated Types(関連型)の設計
  • Primary Associated Typesの活用(Swift 5.7+)
  • プロトコルによる依存性注入
  • テスタビリティの確保

実行条件

  • 抽象化が必要な設計時
  • テスト容易性の向上が必要な時
  • 依存性注入パターンの実装時
  • 既存クラス継承の見直し時

プロセス

Step 1: 要件分析

## 抽象化の目的

### 動機
- [ ] テスタビリティ向上
- [ ] 実装の交換可能性
- [ ] 共通インターフェースの提供
- [ ] ポリモーフィズムの実現

### 制約
- [ ] パフォーマンス要件
- [ ] ABI安定性要件
- [ ] 後方互換性要件

Step 2: Protocol設計

単一責任の原則

// Bad: 責任が多すぎる
protocol DataManager {
    func fetch() async throws -> Data
    func save(_ data: Data) async throws
    func cache(_ data: Data)
    func validate(_ data: Data) -> Bool
    func transform(_ data: Data) -> ProcessedData
}

// Good: 責任を分離
protocol DataFetching {
    func fetch() async throws -> Data
}

protocol DataPersisting {
    func save(_ data: Data) async throws
}

protocol DataCaching {
    func cache(_ data: Data)
    func cached() -> Data?
}

プロトコル合成

// 必要に応じて合成
typealias DataRepository = DataFetching & DataPersisting

// または具体的な要件として定義
protocol DataRepository: DataFetching, DataPersisting {
    // 追加のメソッドがあれば定義
}

Step 3: Protocol Extension活用

デフォルト実装

protocol Identifiable {
    var id: UUID { get }
}

extension Identifiable {
    // デフォルト実装
    var id: UUID { UUID() }
}

protocol Timestamped {
    var createdAt: Date { get }
    var updatedAt: Date { get }
}

extension Timestamped {
    var isRecent: Bool {
        updatedAt.timeIntervalSinceNow > -86400  // 24時間以内
    }
}

条件付きExtension

// Collectionに対する拡張(要素がEquatableの場合のみ)
extension Collection where Element: Equatable {
    func removingDuplicates() -> [Element] {
        var seen: [Element] = []
        return filter { element in
            if seen.contains(element) {
                return false
            }
            seen.append(element)
            return true
        }
    }
}

// 特定の型に対する拡張
extension Array where Element == Int {
    var sum: Int {
        reduce(0, +)
    }
}

Step 4: Associated Types設計

基本的なAssociated Type

protocol Repository {
    associatedtype Entity
    associatedtype ID: Hashable

    func find(id: ID) async throws -> Entity?
    func save(_ entity: Entity) async throws
    func delete(id: ID) async throws
}

// 具体的な実装
struct UserRepository: Repository {
    typealias Entity = User
    typealias ID = UUID

    func find(id: UUID) async throws -> User? {
        // 実装
    }

    func save(_ entity: User) async throws {
        // 実装
    }

    func delete(id: UUID) async throws {
        // 実装
    }
}

Primary Associated Types (Swift 5.7+)

// Primary Associated Typeを指定
protocol Repository<Entity> {
    associatedtype Entity
    associatedtype ID: Hashable = UUID

    func find(id: ID) async throws -> Entity?
    func save(_ entity: Entity) async throws
}

// 使用側で型を明示できる
func processUsers(repository: some Repository<User>) async throws {
    if let user = try await repository.find(id: UUID()) {
        // userはUser型
    }
}

// 存在型としても使用可能
let repositories: [any Repository<User>] = [
    InMemoryUserRepository(),
    RemoteUserRepository()
]

Step 5: 依存性注入パターン

コンストラクタ注入

protocol NetworkClient: Sendable {
    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}

final class UserService {
    private let networkClient: NetworkClient
    private let cache: UserCache

    init(networkClient: NetworkClient, cache: UserCache) {
        self.networkClient = networkClient
        self.cache = cache
    }

    func fetchUser(id: UUID) async throws -> User {
        if let cached = cache.get(id: id) {
            return cached
        }
        let user: User = try await networkClient.request(.user(id: id))
        cache.set(user, for: id)
        return user
    }
}

環境による注入(SwiftUI)

// プロトコル定義
protocol AnalyticsTracking: Sendable {
    func track(event: AnalyticsEvent)
}

// 環境キー
struct AnalyticsKey: EnvironmentKey {
    static let defaultValue: AnalyticsTracking = NoOpAnalytics()
}

extension EnvironmentValues {
    var analytics: AnalyticsTracking {
        get { self[AnalyticsKey.self] }
        set { self[AnalyticsKey.self] = newValue }
    }
}

// 使用
struct MyView: View {
    @Environment(\.analytics) var analytics

    var body: some View {
        Button("Tap") {
            analytics.track(event: .buttonTapped("MyButton"))
        }
    }
}

Step 6: テスタビリティ確保

モック可能な設計

// プロダクションコード
protocol UserRepositoryProtocol {
    func fetchUser(id: UUID) async throws -> User
    func saveUser(_ user: User) async throws
}

final class UserRepository: UserRepositoryProtocol {
    private let networkClient: NetworkClient

    init(networkClient: NetworkClient) {
        self.networkClient = networkClient
    }

    func fetchUser(id: UUID) async throws -> User {
        try await networkClient.request(.user(id: id))
    }

    func saveUser(_ user: User) async throws {
        try await networkClient.request(.saveUser(user))
    }
}

// テストコード
final class MockUserRepository: UserRepositoryProtocol {
    var fetchUserResult: Result<User, Error> = .success(.mock)
    var savedUsers: [User] = []

    func fetchUser(id: UUID) async throws -> User {
        try fetchUserResult.get()
    }

    func saveUser(_ user: User) async throws {
        savedUsers.append(user)
    }
}

// テスト
@Test
func testUserService() async throws {
    let mockRepository = MockUserRepository()
    mockRepository.fetchUserResult = .success(User(name: "Test"))

    let service = UserService(repository: mockRepository)
    let user = try await service.getUser(id: UUID())

    #expect(user.name == "Test")
}

出力形式

# Protocol Design Document

## 概要
- 目的: ユーザーデータアクセスの抽象化
- スコープ: データ層

## プロトコル定義

### UserRepositoryProtocol
```swift
protocol UserRepositoryProtocol: Sendable {
    func find(id: UUID) async throws -> User?
    func save(_ user: User) async throws
    func delete(id: UUID) async throws
    func findAll() async throws -> [User]
}

設計判断

項目 判断 理由
Associated Type使用 No 単一のEntity型のみ使用
Sendable要件 Yes 非同期コンテキストで使用
Extension Yes findAllのデフォルト実装

依存関係図

UserService
    └── UserRepositoryProtocol
            ├── CoreDataUserRepository
            ├── InMemoryUserRepository (テスト用)
            └── RemoteUserRepository

実装例

プロダクション実装

final class CoreDataUserRepository: UserRepositoryProtocol {
    // ...
}

テスト用モック

final class MockUserRepository: UserRepositoryProtocol {
    // ...
}

## ガードレール

### 設計原則
- Interface Segregation: クライアントが使わないメソッドに依存させない
- Dependency Inversion: 具体ではなく抽象に依存する
- Protocol Composition: 大きなプロトコルより小さなプロトコルの合成

### 避けるべきパターン
- 過度な抽象化(1つの実装しかないプロトコル)
- Fat Protocol(メソッドが多すぎる)
- Protocol Witnessテーブルのオーバーヘッドを無視した設計

### 注意点
- プロトコルは値型で使う場合はexistentialのオーバーヘッドに注意
- `some Protocol`(opaque type)と`any Protocol`(existential)の使い分け
- Swift 6のStrict Concurrency対応(Sendable要件)

## 関連スキル

- `swift-code-review`: コードレビュー
- `swift-concurrency`: 並行処理対応