189 lines
4.9 KiB
Markdown
189 lines
4.9 KiB
Markdown
---
|
|
name: Swift Protocol-Oriented Programming
|
|
description: Protocol-first architecture patterns for reusability and testability
|
|
globs: ["**/*.swift"]
|
|
---
|
|
|
|
# Protocol-Oriented Programming (POP)
|
|
|
|
**Protocol-first architecture is a priority.** When designing new features, always think about protocols and composition before concrete implementations.
|
|
|
|
## When Architecting New Code
|
|
|
|
1. **Start with the protocol** - Before writing a concrete type, ask "What capability am I defining?" and express it as a protocol.
|
|
|
|
2. **Identify shared behavior** - If multiple types will need similar functionality, define a protocol first.
|
|
|
|
3. **Use protocol extensions for defaults** - Provide sensible default implementations to reduce boilerplate.
|
|
|
|
4. **Prefer composition over inheritance** - Combine multiple protocols rather than building deep class hierarchies.
|
|
|
|
## When Reviewing Existing Code
|
|
|
|
1. **Look for duplicated patterns** - Similar logic across files is a candidate for protocol extraction.
|
|
|
|
2. **Identify common interfaces** - Types that expose similar properties/methods should conform to a shared protocol.
|
|
|
|
3. **Check before implementing** - Search for existing protocols that could be adopted or extended.
|
|
|
|
4. **Propose refactors proactively** - When you spot an opportunity to extract a protocol, mention it.
|
|
|
|
## Protocol Design Guidelines
|
|
|
|
### Naming Conventions
|
|
|
|
Use capability-based suffixes:
|
|
|
|
- `-able`: `Persistable`, `Shareable`, `Validatable`
|
|
- `-ing`: `DataProviding`, `ErrorHandling`, `Loading`
|
|
- `-Provider`: `ContentProvider`, `DataProvider`
|
|
- `-Delegate`: `NavigationDelegate`, `FormDelegate`
|
|
|
|
### Keep Protocols Focused
|
|
|
|
Each protocol should represent one capability (Interface Segregation Principle):
|
|
|
|
```swift
|
|
// GOOD - Focused protocols
|
|
// Note: Swift provides Identifiable already — adopt it, don't redefine it.
|
|
protocol Nameable {
|
|
var displayName: String { get }
|
|
}
|
|
|
|
protocol Timestamped {
|
|
var createdAt: Date { get }
|
|
var updatedAt: Date { get }
|
|
}
|
|
|
|
// Compose as needed (Identifiable comes from Swift standard library)
|
|
struct User: Identifiable, Nameable, Timestamped {
|
|
let id: UUID
|
|
var displayName: String
|
|
var createdAt: Date
|
|
var updatedAt: Date
|
|
}
|
|
```
|
|
|
|
```swift
|
|
// BAD - Kitchen sink protocol
|
|
protocol Entity {
|
|
var id: UUID { get }
|
|
var displayName: String { get }
|
|
var createdAt: Date { get }
|
|
var updatedAt: Date { get }
|
|
func save() async throws
|
|
func delete() async throws
|
|
func validate() -> Bool
|
|
}
|
|
```
|
|
|
|
### Associated Types
|
|
|
|
Use sparingly. Prefer concrete types or generics at the call site when possible:
|
|
|
|
```swift
|
|
// Prefer this for simple cases
|
|
protocol DataFetching {
|
|
func fetch<T: Decodable>(from url: URL) async throws -> T
|
|
}
|
|
|
|
// Use associated types when the type is fundamental to the protocol
|
|
protocol Repository {
|
|
associatedtype Entity
|
|
func fetch(id: UUID) async throws -> Entity?
|
|
func save(_ entity: Entity) async throws
|
|
}
|
|
```
|
|
|
|
### Value vs Reference Semantics
|
|
|
|
Constrain to `AnyObject` only when reference semantics are required:
|
|
|
|
```swift
|
|
// Default - allows structs and classes
|
|
protocol Configurable {
|
|
mutating func configure(with options: Options)
|
|
}
|
|
|
|
// When you need reference semantics (delegates, observers)
|
|
protocol NavigationDelegate: AnyObject {
|
|
func didNavigate(to destination: Destination)
|
|
}
|
|
```
|
|
|
|
## Protocol Extensions
|
|
|
|
Provide default implementations for common behavior:
|
|
|
|
```swift
|
|
protocol Validatable {
|
|
var validationErrors: [String] { get }
|
|
var isValid: Bool { get }
|
|
}
|
|
|
|
extension Validatable {
|
|
var isValid: Bool {
|
|
validationErrors.isEmpty
|
|
}
|
|
}
|
|
```
|
|
|
|
## Dependency Injection with Protocols
|
|
|
|
Define protocols for services to enable testing:
|
|
|
|
```swift
|
|
protocol NetworkServiceProtocol {
|
|
func fetch<T: Decodable>(from url: URL) async throws -> T
|
|
}
|
|
|
|
// Production implementation
|
|
final class NetworkService: NetworkServiceProtocol { ... }
|
|
|
|
// Test mock
|
|
final class MockNetworkService: NetworkServiceProtocol { ... }
|
|
```
|
|
|
|
## Benefits
|
|
|
|
- **Reusability** - Shared protocols work across features
|
|
- **Testability** - Mock types can conform to protocols for unit testing
|
|
- **Flexibility** - New features can adopt existing protocols immediately
|
|
- **Maintainability** - Fix a bug in a protocol extension, fix it everywhere
|
|
- **Discoverability** - Protocols document the expected interface clearly
|
|
|
|
## Common Patterns
|
|
|
|
### Repository Pattern
|
|
|
|
```swift
|
|
protocol Repository {
|
|
associatedtype Entity: Identifiable
|
|
|
|
func fetch(id: Entity.ID) async throws -> Entity?
|
|
func fetchAll() async throws -> [Entity]
|
|
func save(_ entity: Entity) async throws
|
|
func delete(_ entity: Entity) async throws
|
|
}
|
|
```
|
|
|
|
### Service Pattern
|
|
|
|
```swift
|
|
protocol AuthServiceProtocol {
|
|
var isAuthenticated: Bool { get }
|
|
func signIn(email: String, password: String) async throws
|
|
func signOut() async throws
|
|
}
|
|
```
|
|
|
|
### Coordinator/Navigation Pattern
|
|
|
|
```swift
|
|
protocol NavigationCoordinating: AnyObject {
|
|
func navigate(to destination: Destination)
|
|
func dismiss()
|
|
func presentSheet(_ sheet: SheetType)
|
|
}
|
|
```
|