Protocol-Oriented Programming
Prerequisites: Protocols, Extensions, Generics
Protocol-oriented programming is Swift's preferred way to share behavior: instead of building class hierarchies, you describe capabilities as small protocols, give them default implementations in extensions, and let any type — struct, enum, or class — adopt exactly the capabilities it needs.
Composing capabilities
protocol Identifiable {
var id: String { get }
}
protocol Priced {
var price: Double { get }
}
protocol Discountable: Priced {
var discountRate: Double { get }
}
extension Discountable {
var salePrice: Double { price * (1 - discountRate) }
}
struct Book: Identifiable, Discountable {
let id: String
let price: Double
let discountRate = 0.1
}
struct GiftCard: Identifiable, Priced {
let id: String
let price: Double
}
let book = Book(id: "b1", price: 20)
print(book.salePrice)
18.0
GiftCard is priced but never discounted, and nothing forces it to
carry discount logic it doesn't need — something a single Product
superclass couldn't express as cleanly. (Swift's standard library
actually has an Identifiable protocol too; this example defines its
own for illustration.)
Constrained extensions
An extension can apply only when a condition holds:
extension Collection where Element: Priced {
var totalPrice: Double { reduce(0) { $0 + $1.price } }
}
let basket: [any Priced] = [Book(id: "b1", price: 20), GiftCard(id: "g1", price: 25)]
let books = [Book(id: "b1", price: 20), Book(id: "b2", price: 15)]
print(books.totalPrice)
print(basket.map(\.price).reduce(0, +))
35.0
45.0
Every array, set, or other collection of a Priced type now has
totalPrice — without modifying Array for all element types.
Protocols for testability
Depending on a protocol instead of a concrete type lets you swap in a fake for tests:
protocol WeatherProviding {
func temperature(in city: String) async throws -> Double
}
struct WeatherReport {
let provider: any WeatherProviding
func headline(for city: String) async -> String {
guard let temp = try? await provider.temperature(in: city) else {
return "Weather unavailable"
}
return temp > 25 ? "Hot in \(city)" : "Mild in \(city)"
}
}
struct FakeWeather: WeatherProviding {
func temperature(in city: String) async throws -> Double { 30 }
}
print(await WeatherReport(provider: FakeWeather()).headline(for: "Rome"))
Hot in Rome
The real app passes a network-backed provider; tests pass FakeWeather
and run instantly, with no network.
Guidelines
- Keep protocols small and focused on one capability.
- Start with concrete types. Introduce a protocol when you have a second conforming type, or need one for testing.