swift-protocol

CAPHTECH's avatarfrom CAPHTECH

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

⭐0stars🔀0forks📁View on GitHub🕐Updated Jan 4, 2026

When & Why to Use This Skill

This Claude skill facilitates Protocol-Oriented Programming (POP) in Swift, offering expert guidance on protocol extensions, associated types, and dependency injection. It helps developers build highly modular, testable, and maintainable Swift applications by providing structured design patterns and implementation strategies that align with modern Swift standards.

Use Cases

  • Designing decoupled system architectures by defining clear protocol interfaces and single-responsibility boundaries.
  • Implementing robust Dependency Injection (DI) patterns to enhance unit testing through mockable protocol implementations.
  • Refactoring complex class inheritance hierarchies into flexible, protocol-based compositions to reduce code coupling.
  • Leveraging advanced Swift features like Associated Types and Primary Associated Types (Swift 5.7+) for type-safe generic programming.
  • Optimizing SwiftUI applications by managing environment-based dependencies and analytics tracking through protocols.
nameswift-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`: 並行処理対応