Skip to content
AgileLogics
6 min read

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.