Lesson 77Protocols

Protocol-Oriented Programming

What is POP?

Protocol-Oriented Programming is Swift's preferred paradigm. Instead of inheriting from a base class, you compose behavior by conforming to multiple protocols!

OOP vs POP

OOP Problems

  • • Single inheritance only
  • • Tight coupling
  • • Fragile base class problem
  • • Reference types only

POP Benefits

  • • Multiple conformance
  • • Loose coupling
  • • Composition over inheritance
  • • Value types supported

Key POP Patterns

Protocol Composition

Combine protocols with &

Flyable & Swimmable

Default Implementations

Share code via extensions

Retroactive Modeling

Add protocols to existing types

Dependency Injection

Swap implementations easily

Apple's Mantra

"Start with a protocol"

Design your APIs around protocols first, then provide implementations. This makes code more testable and flexible!

When to Use Classes

  • Need reference semantics
  • Need deinitializers
  • Interop with Objective-C
  • Identity is important (===)
main.swift
// PROTOCOL-ORIENTED PROGRAMMING (POP)
// Swift's preferred paradigm over traditional OOP

// TRADITIONAL OOP APPROACH
class Animal {
    func eat() { print("Eating") }
}

class Bird: Animal {
    func fly() { print("Flying") }
}

class Penguin: Bird {
    // Problem: Penguins can't fly but inherit fly()!
}

// PROTOCOL-ORIENTED APPROACH
protocol Eatable {
    func eat()
}

protocol Flyable {
    func fly()
}

protocol Swimmable {
    func swim()
}

// Default implementations
extension Eatable {
    func eat() { print("Eating food") }
}

extension Flyable {
    func fly() { print("Flying in the sky") }
}

extension Swimmable {
    func swim() { print("Swimming in water") }
}

// Compose behaviors as needed!
struct Sparrow: Eatable, Flyable { }
struct PenguinPOP: Eatable, Swimmable { }  // No fly!
struct Duck: Eatable, Flyable, Swimmable { }

let sparrow = Sparrow()
sparrow.eat()  // Eating food
sparrow.fly()  // Flying in the sky

let penguin = PenguinPOP()
penguin.eat()  // Eating food
penguin.swim() // Swimming in water
// penguin.fly() // Error! No fly method

// PROTOCOL COMPOSITION
typealias FlyingSwimmer = Flyable & Swimmable

func describeFlyingSwimmer(_ creature: FlyingSwimmer) {
    creature.fly()
    creature.swim()
}

describeFlyingSwimmer(Duck())

// BUILDING REUSABLE COMPONENTS
protocol Identifiable {
    var id: String { get }
}

protocol Validatable {
    func validate() -> Bool
}

protocol Persistable {
    func save()
    func delete()
}

// Default implementations
extension Identifiable {
    var id: String { UUID().uuidString }
}

extension Persistable {
    func save() {
        print("Saving to database...")
    }

    func delete() {
        print("Deleting from database...")
    }
}

// Compose as needed
struct User: Identifiable, Validatable, Persistable {
    var email: String

    func validate() -> Bool {
        return email.contains("@")
    }
}

// VALUE SEMANTICS WITH PROTOCOLS
protocol Copyable {
    func copy() -> Self
}

struct Document: Copyable {
    var title: String
    var content: String

    func copy() -> Document {
        return Document(title: "Copy of \(title)", content: content)
    }
}

// GENERIC CONSTRAINTS WITH PROTOCOLS
func process<T: Identifiable & Validatable>(_ item: T) {
    print("Processing item: \(item.id)")
    if item.validate() {
        print("Item is valid!")
    }
}

// RETROACTIVE MODELING
// Add protocol conformance to existing types
extension Int: Identifiable {
    var id: String { String(self) }
}

extension String: Validatable {
    func validate() -> Bool {
        return !isEmpty
    }
}

// PROTOCOL WITNESSES
protocol Renderer {
    associatedtype Output
    func render() -> Output
}

struct HTMLRenderer: Renderer {
    func render() -> String {
        return "<html>...</html>"
    }
}

struct JSONRenderer: Renderer {
    func render() -> [String: Any] {
        return ["status": "ok"]
    }
}

// DEPENDENCY INJECTION WITH PROTOCOLS
protocol NetworkService {
    func fetch(url: String) async -> Data?
}

struct RealNetworkService: NetworkService {
    func fetch(url: String) async -> Data? {
        // Real network call
        return nil
    }
}

struct MockNetworkService: NetworkService {
    func fetch(url: String) async -> Data? {
        // Return mock data for testing
        return "Mock data".data(using: .utf8)
    }
}

class ViewModel {
    let networkService: NetworkService

    init(networkService: NetworkService) {
        self.networkService = networkService
    }
}

// Use real service in production
let prodVM = ViewModel(networkService: RealNetworkService())

// Use mock service in tests
let testVM = ViewModel(networkService: MockNetworkService())

Try It Yourself!

Design a game character system using POP. Create protocols for Attackable, Defendable, Healable and compose different character types!