# Swift Protocol-Oriented Programming Over Inheritance

Source: https://devdocs10.com/articles/swift/swift-pop-over-inheritance  
By Murat Bo · Updated 2026-10-17 · DevDocs10 (https://devdocs10.com)

Swift encourages you to model behavior with protocols and composition instead of building deep class hierarchies. This article explains why that approach is preferred, how protocol-oriented design works in practice, and when inheritance is still the right tool.

> **Quick answer:** In Swift, use protocols to describe shared behavior and use protocol extensions to provide default implementations. Prefer classes and inheritance only when you need shared reference identity, superclass behavior, or framework requirements that depend on a class hierarchy.

> **Difficulty:** Intermediate
>
> **You'll understand this better if you know:** basic Swift types, how functions and methods work, and the difference between classes and structs.

## 1. What Is Protocol-Oriented Programming?

Protocol-oriented programming, often shortened to `POP`, is a style of design where you build types around capabilities instead of around a shared base class. A protocol defines what a type can do, and any struct, enum, or class can adopt that protocol.

- A `protocol` describes requirements such as properties and methods.
- A `protocol extension` can add default behavior.
- Different types can share the same behavior without sharing the same ancestor.
- Value types like `struct` and `enum` work naturally with this model.

Inheritance, by contrast, organizes code by parent-child relationships in classes. That can be useful, but it often becomes rigid when many types need overlapping features that do not fit into a single hierarchy.

## 2. Why Protocol-Oriented Programming Matters

Swift was designed to make composition easier and safer than deep inheritance trees. Protocol-oriented code usually becomes more flexible, easier to test, and easier to reuse across unrelated types.

This matters because real apps often have multiple kinds of objects or values that share behavior but not identity. For example, a `Downloadable` protocol might apply to files, images, and documents even though those types should not inherit from one parent just to share a `download()` method.

Protocol-oriented design also helps you avoid common inheritance problems:

- Fragile superclass dependencies
- Hard-to-predict method overrides
- Forced behavior that does not apply to every subclass
- Class hierarchies that are difficult to extend safely

## 3. Basic Syntax

The core idea is simple: define shared behavior in a protocol, then make concrete types conform to it. You can also give that protocol a default implementation so conforming types get behavior automatically.

### Protocol definition

This protocol requires a `name` property and a `describe()` method:

```swift
protocol Named {
    var name: String { get }
    func describe() -> String
}
```

Any type that adopts `Named` must provide those requirements.

### Default implementation

A protocol extension can supply a default version of `describe()`:

```swift
extension Named {
    func describe() -> String {
        return "My name is \(name)."
    }
}
```

If a conforming type does not provide its own `describe()`, Swift uses the default version.

### Conforming type

Here is a struct that adopts the protocol without needing inheritance:

```swift
struct User: Named {
    let name: String
}
```

The `User` type now gets protocol behavior while staying a value type.

## 4. Step-by-Step Examples

### Example 1: Sharing behavior across unrelated types

Suppose different app models need to be searchable. A protocol lets you express that capability without forcing a base class.

```swift
import Foundation

protocol Searchable {
    var searchText: String { get }
}

extension Searchable {
    func matches(_ query: String) -> Bool {
        searchText.localizedCaseInsensitiveContains(query)
    }
}

struct Book: Searchable {
    let searchText: String
}

struct Song: Searchable {
    let searchText: String
}
```

Both types gain the same behavior, but neither one needs to inherit from a shared parent type.

### Example 2: Replacing a superclass with a protocol

Imagine a payment system. Instead of a superclass with forced methods, you can define a capability-based protocol.

```swift
protocol Payable {
    var amount: Double { get }
    func pay()
}

extension Payable {
    func pay() {
        print("Paying \(amount)")
    }
}

struct Invoice: Payable {
    let amount: Double
}

let invoice = Invoice(amount: 42.5)
invoice.pay()
```

Output (tested with Swift 6.4):

```text
Paying 42.5
```

The default implementation gives usable behavior immediately, and you can still override it in a specific type when needed.

### Example 3: Using protocol composition

One of Swift’s strengths is combining capabilities. A type can conform to multiple protocols without needing multiple inheritance.

```swift
protocol Printable {
    func printDescription()
}

protocol Archivable {
    func archive()
}

struct Report: Printable, Archivable {
    func printDescription() {
        print("Monthly report")
    }

    func archive() {
        print("Report archived")
    }
}
```

This is a cleaner fit than building a class tree just to combine two unrelated behaviors.

### Example 4: Choosing a class only when you need reference semantics

Protocols do not eliminate classes. If you need shared mutable identity, a class can still conform to a protocol and participate in a protocol-oriented design.

```swift
protocol Counter {
    var value: Int { get }
    mutating func increment()
}

final class Meter: Counter {
    var value: Int = 0

    func increment() {
        value += 1
    }
}
```

This shows the pattern clearly: the protocol defines the behavior, while the class is used only because identity and shared state matter.

## 5. Practical Use Cases

Protocol-oriented programming is a strong fit when you need one capability in many places. Typical uses include:

- Models that need common behavior such as formatting, validation, or serialization
- Services that can be swapped in tests, such as networking or storage abstractions
- Collections of unrelated types that should be processed through the same API
- Feature flags or roles, such as `Cacheable`, `Loggable`, or `Retryable`
- Code that benefits from default behavior but still needs opt-in overrides

Inheritance is less suitable when the shared behavior is not really a true “is-a” relationship. A `FileExporter`, `CSVExporter`, and `PDFExporter` may share logic, but forcing them into one superclass can make the design awkward.

## 6. Common Mistakes

### Mistake 1: Using inheritance just to reuse one method

Many beginners create a superclass when they only need a shared function or two. That creates coupling that grows worse as the project expands.

**Problem:** This superclass exists only so subclasses can reuse `log()`, but it also forces every child into a rigid class hierarchy.

```swift
class BaseLogger {
    func log(_ message: String) {
        print(message)
    }
}

class FileService: BaseLogger {
    func save() {
        log("Saved")
    }
}
```

**Fix:** Move the behavior into a protocol extension so any type can adopt it without inheriting unrelated state.

```swift
protocol Loggable {}

extension Loggable {
    func log(_ message: String) {
        print(message)
    }
}

struct FileService: Loggable {
    func save() {
        log("Saved")
    }
}
```

The protocol version reuses code without forcing a class-based design.

### Mistake 2: Assuming protocol extensions always behave like overrides

Protocol extensions provide default behavior, but that is not the same as virtual dispatch in a superclass. This surprises developers when the concrete type also defines a method with the same name.

**Problem:** When you call a protocol extension method on a value typed as the protocol, Swift may use the extension implementation instead of a more specific method you expected.

```swift
protocol Greeter {}

extension Greeter {
    func greet() {
        print("Hello from protocol extension")
    }
}

struct Person: Greeter {
    func greet() {
        print("Hello from Person")
    }
}

let person = Person()
person.greet()
```

Output (tested with Swift 6.4):

```text
Hello from Person
```

**Fix:** Put the requirement in the protocol itself when you need dynamic dispatch through protocol values.

```swift
protocol Greeter {
    func greet()
}

extension Greeter {
    func greet() {
        print("Hello from protocol extension")
    }
}

struct Person: Greeter {
    func greet() {
        print("Hello from Person")
    }
}
```

The corrected design makes the protocol requirement explicit, so Swift dispatches as you expect.

### Mistake 3: Reaching for classes when a value type would be safer

Sometimes developers choose a base class only because they think shared behavior requires inheritance. In Swift, structs often work better because they avoid accidental shared mutation.

**Problem:** Class inheritance can create surprising state changes when multiple parts of your program hold references to the same object.

```swift
class Settings {
    var theme = "Light"
}

let a = Settings()
let b = a
b.theme = "Dark"
```

**Fix:** Use a value type with protocol conformance when you want a copy-by-value model.

```swift
protocol Themed {
    var theme: String { get set }
}

struct Settings: Themed {
    var theme = "Light"
}

var a = Settings()
var b = a
b.theme = "Dark"
```

The struct version keeps each copy independent, which is often a better default in Swift.

## 7. Best Practices

### Practice 1: Model capabilities, not inheritance trees

Design protocols around what something can do, not what it is from a class-hierarchy point of view. That keeps APIs smaller and more reusable.

```swift
protocol Readable {
    func read() -> String
}
```

This is better than creating a base class for every object that “might read” something.

### Practice 2: Prefer small, focused protocols

Protocols that do too much become just as hard to manage as large base classes. Split responsibilities so conforming types only adopt what they truly need.

```swift
protocol Savable {
    func save()
}

protocol Loadable {
    func load()
}
```

Smaller protocols are easier to reuse, test, and combine.

### Practice 3: Use protocol extensions for shared default behavior

Default implementations reduce repetition, but they should represent sensible shared behavior rather than hidden business rules.

```swift
protocol Timestamped {
    var createdAt: Date { get }
}

extension Timestamped {
    var ageInDays: Int {
        let seconds = Date().timeIntervalSince(createdAt)
        return Int(seconds / 86_400)
    }
}
```

The extension keeps reusable logic in one place while leaving the core requirement explicit.

## 8. Limitations and Edge Cases

- Protocols do not store state. If you need shared stored properties, you still need a concrete type, often a struct or class.
- Protocol extensions cannot fully replace superclass behavior when you need inherited stored properties or shared initialization logic.
- Some Cocoa or framework APIs still expect classes, delegates, or subclassing patterns, so protocol-oriented design must fit the API surface you are using.
- Protocol methods with default implementations can behave differently depending on whether they are required requirements or extension-only helpers.
- Class inheritance may still be the most practical choice for objects that must share identity, mutable lifecycle, or framework subclass contracts.

> A useful rule: if the shared behavior is about capability, use a protocol; if the shared behavior is about a common object family with shared state and lifecycle, a class may still be appropriate.

## 9. Practical Mini Project

Let’s build a tiny notification system that uses protocols instead of a base notification class. The goal is to send different kinds of messages through one interface.

```swift
protocol Notifiable {
    var recipient: String { get }
    func message() -> String
}

extension Notifiable {
    func deliver() {
        print("Sending to \(recipient): \(message())")
    }
}

struct EmailAlert: Notifiable {
    let recipient: String
    let subject: String

    func message() -> String {
        "Email subject: \(subject)"
    }
}

struct SMSAlert: Notifiable {
    let recipient: String
    let text: String

    func message() -> String {
        "SMS text: \(text)"
    }
}

let items: [Notifiable] = [
    EmailAlert(recipient: "alice@example.com", subject: "Welcome"),
    SMSAlert(recipient: "+1-555-0100", text: "Your code is 1234")
]

for item in items {
    item.deliver()
}
```

Output (tested with Swift 6.4):

```text
Sending to alice@example.com: Email subject: Welcome
Sending to +1-555-0100: SMS text: Your code is 1234
```

This example shows the core advantage of protocol-oriented design: one processing path can handle multiple types that share behavior but not ancestry.

## 10. Key Points

- Protocols describe behavior; inheritance describes a class family.
- Swift makes protocols work well with structs, enums, and classes.
- Protocol extensions can reduce repetition with default implementations.
- Deep inheritance is often less flexible than composing small protocols.
- Use classes only when identity, lifecycle, or framework requirements make them necessary.

## 11. Practice Exercise

Create a small reporting system using protocol-oriented design.

- Define a protocol named `Summarizable` with a `summary()` method.
- Create two types, such as `Task` and `Invoice`, that conform to the protocol.
- Add a default implementation in a protocol extension for a helper method named `printSummary()`.
- Store both values in an array of `Summarizable` and print their summaries.

**Expected output:** Each item should print a readable summary line for its own type.

**Hint:** Make the protocol require only the behavior that every conforming type can supply, then move reusable formatting into the extension.

Solution:

```swift
protocol Summarizable {
    func summary() -> String
}

extension Summarizable {
    func printSummary() {
        print(summary())
    }
}

struct Task: Summarizable {
    let title: String
    let done: Bool

    func summary() -> String {
        "Task: \(title) - \(done ? "done" : "pending")"
    }
}

struct Invoice: Summarizable {
    let number: String
    let total: Double

    func summary() -> String {
        "Invoice #\(number): $\(total)"
    }
}

let items: [any Summarizable] = [
    Task(title: "Write docs", done: true),
    Invoice(number: "A-100", total: 79.5)
]

for item in items {
    item.printSummary()
}
```

Output (tested with Swift 6.4):

```text
Task: Write docs - done
Invoice #A-100: $79.5
```

This solution works because the protocol defines the common contract, while the extension supplies reusable convenience behavior for every conforming type.

## 12. Final Summary

Protocol-oriented programming is a natural fit for Swift because it lets you express behavior as capabilities instead of forcing types into a single inheritance tree. That usually leads to code that is more flexible, more testable, and easier to extend over time.

Inheritance still has a place, but it should be chosen for the right reasons: shared identity, shared implementation details, or framework-driven class hierarchies. When you only need shared behavior, Swift protocols and protocol extensions are usually the cleaner and more scalable choice.

If you want to go further, the next useful topic is protocol composition and existential types, since those features show how Swift builds larger APIs from small protocol-based pieces.
