Skip to Content
Course content

19: Protocols and Protocol Extensions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I once worked with a developer who was trying to build a complex payment system. He started with a massive base class called PaymentMethod, and every specific type—CreditCard, ApplePay, PayPal—inherited from it. It worked fine until we needed to add a "refund" feature. The problem? Only some payment methods were refundable. He ended up putting a refund() method in the base class that just threw an error or did nothing for non-refundable types. It was a mess. He was fighting the inheritance hierarchy because he was trying to force a "is-a" relationship when he actually needed a "can-do" relationship.

That's where protocols come in. Instead of saying a CreditCard is a PaymentMethod, we say it conforms to a protocol. It’s a shift in mindset from inheritance to composition, and in Swift, it is arguably the most powerful tool in your architectural kit.

Defining Capabilities with Protocols

A protocol is essentially a contract. It doesn't implement any logic itself; it just tells the compiler, "I don't care what this object actually is, as long as it has these specific properties and methods." This allows you to write code that is incredibly flexible. If you have a function that needs to process a payment, it shouldn't care if it's a credit card or a digital wallet—it only cares that the object conforms to a PaymentMethod protocol.

protocol PaymentMethod {
    var providerName: String { get }
    func processPayment(amount: Double)
}

protocol Refundable {
    func refund(amount: Double)
}

struct CreditCard: PaymentMethod, Refundable {
    let providerName = "Visa"
    
    func processPayment(amount: Double) {
        print("Charging $\(amount) to Visa card.")
    }
    
    func refund(amount: Double) {
        print("Refunding $\(amount) back to Visa card.")
    }
}

struct GiftCard: PaymentMethod {
    let providerName = "Store Credit"
    
    func processPayment(amount: Double) {
        print("Deducting $\(amount) from gift card balance.")
    }
    // Gift cards aren't refundable in our system, so we just don't conform to Refundable.
}

Notice how GiftCard doesn't have to implement a fake refund method. It simply doesn't opt into the Refundable contract. This keeps your types lean and your logic honest.

Providing Default Logic via Protocol Extensions

Now, here is where things get interesting. If you find yourself writing the exact same helper method in five different structs that all conform to the same protocol, you're duplicating code. In many languages, you'd be forced back into using a base class. But in Swift, we have protocol extensions.

A protocol extension allows you to provide a default implementation for a protocol method. Any type that conforms to the protocol automatically gets this behavior, but they can still override it if they need something more specific. I use this constantly to create "standard" behavior that can be customized on a case-by-case basis.

extension PaymentMethod {
    func printReceipt(amount: Double) {
        print("Receipt: Paid $\(amount) using \(providerName). Thank you!")
    }
}

// Now, both CreditCard and GiftCard have printReceipt() 
// without us having to write it inside the structs.
let myCard = CreditCard()
myCard.printReceipt(amount: 50.0) 

This is a game changer for API design. You can define a protocol with a few required properties, and then use extensions to build a whole library of functionality on top of those properties. You're essentially adding capabilities to any type that fits the blueprint, regardless of where that type sits in your class hierarchy.




📋 Practical Task

Building a Smart Home Device Controller

You are tasked with building a system to manage various smart home devices. Not all devices behave the same way: some can be toggled on and off, while others (like smart bulbs) can also be dimmed.

Your Requirements:

  • Create a protocol called Powerable that requires a boolean property isOn and a method togglePower().
  • Create a protocol called Dimmable that requires a property brightness (a Double from 0.0 to 1.0) and a method setBrightness(level: Double).
  • Implement a protocol extension for Powerable that adds a method statusReport() which prints whether the device is currently "On" or "Off".
  • Create a SmartLight struct that conforms to both Powerable and Dimmable.
  • Create a SmartPlug struct that conforms only to Powerable.

Testing your code: Create an array of Powerable objects containing both a light and a plug. Loop through them and call statusReport() on each to verify that the protocol extension is working across different types.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.