Skip to Content
Course content

130: Common Swift Interview Questions on Protocol-Oriented Design

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

If you're heading into a Swift interview, you're almost certainly going to get hit with questions about Protocol-Oriented Programming (POP). Most candidates can define what a protocol is, but very few can actually explain why we use them over traditional class inheritance. To get this right, stop thinking about "what an object is" and start thinking about "what an object can do."

Think of it like a professional kitchen. In a traditional Object-Oriented world, you'd have a base class called Employee, then a subclass Chef, and maybe a PastryChef under that. But what happens when you hire a Dishwasher who also knows how to bake? In a class hierarchy, you're stuck. You can't inherit from both Dishwasher and PastryChef.

In a Protocol-Oriented world, we don't care about the job title. We care about capabilities. We define a Baking protocol and a Cleaning protocol. Now, anyone—be they a professional chef, a dishwasher, or the owner—can conform to Baking. You're composing a person's skill set rather than forcing them into a rigid family tree.

The "Default Implementation" Trap

One of the most common interview questions is: "How does a protocol differ from an abstract class?" Since Swift doesn't have abstract classes, we use protocol extensions. This is where the real power lies.

I've seen a lot of developers just put method signatures in a protocol and leave the implementation to the struct. That's just an interface. To make it "Protocol-Oriented," you provide a default behavior in an extension. Look at this payment example:

protocol PaymentProcessor {
    var merchantId: String { get }
    func processPayment(amount: Double)
}

extension PaymentProcessor {
    // This is the "default" behavior. 
    // Most processors will use this, but any specific one can override it.
    func processPayment(amount: Double) {
        print("Processing $\(amount) for merchant \(merchantId) via standard gateway.")
    }
}

struct StripeProcessor: PaymentProcessor {
    let merchantId = "STRIPE_123"
    // I don't need to implement processPayment here if the default works.
}

struct CryptoProcessor: PaymentProcessor {
    let merchantId = "BLOCK_99"
    
    // But for Crypto, the standard gateway doesn't work, so I override it.
    func processPayment(amount: Double) {
        print("Processing $\(amount) via Blockchain wallet \(merchantId).")
    }
}

In an interview, emphasize that this allows you to add functionality to any type that conforms to the protocol without forcing every single struct to rewrite the same boilerplate code.

Composition Over Inheritance

You'll likely be asked why you'd prefer protocols and structs over classes. The answer is usually "value semantics" and "composition." When you use classes, you're dealing with shared state and reference counting, which can lead to those nightmare retain cycles we've discussed in previous lessons.

By using protocols, you can build complex objects by mixing and matching small, focused protocols. Instead of a massive BaseViewModel that does everything, you create a LoadingStateProviding protocol and a NetworkRequesting protocol. Your ViewModel then just adopts the ones it actually needs. It keeps your code decoupled and, more importantly, makes your unit tests much easier to write because you can mock a single protocol instead of a whole class hierarchy.

Decoding 'some' vs 'any'

This is the "senior-level" question that trips people up. With recent Swift updates, the distinction between some (Opaque Types) and any (Existential Types) is a hot topic.

  • some PaymentProcessor: You're telling the compiler, "I'm returning one specific type that conforms to this protocol, but I'm not going to tell you exactly which one it is." This preserves type identity and is generally more performant.
  • any PaymentProcessor: You're saying, "This could be a StripeProcessor, a CryptoProcessor, or anything else that fits the bill." The compiler has to use a "box" to hold this value, which adds a tiny bit of overhead.

If the interviewer asks which one to use, the rule of thumb is: Use some whenever possible. Only reach for any when you truly need a collection of different types (like an array [any PaymentProcessor]) that all behave differently.




📋 Practical Task

Build a Modular Notification Dispatcher

Your task is to design a notification system using Protocol-Oriented Design. Avoid using classes entirely; use structs and protocols.

  1. Create a protocol named NotificationService that requires a serviceName: String and a method send(message: String).
  2. Create a protocol extension for NotificationService that provides a default implementation for send(message:), printing: "[\(serviceName)] Sending generic notification: \(message)".
  3. Implement three structs: EmailService, SmsService, and PushNotificationService.
    • EmailService should use the default send behavior.
    • SmsService should override send to print: "SMS: Sending short message: \(message)".
    • PushNotificationService should use the default send behavior.
  4. Create a NotificationDispatcher struct that contains an array of any NotificationService. It should have a method dispatchAll(message: String) that iterates through all services and calls send(message:) on each.
  5. Instantiate the dispatcher with all three services and trigger a single message to be sent across all channels.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.