← Knowledge hub
Architecture · ORIGINAL STUDY NOTE

OOP in Swift: abstraction, encapsulation, inheritance & polymorphism

A clear mental model of the four pillars, composition over inheritance, and how Swift protocols change the design trade-offs.

Four ideas, different questions

Abstraction asks which capabilities the caller needs; encapsulation protects internal state and its invariants; inheritance reuses or specializes class behavior; polymorphism lets different implementations satisfy one interface. Protocol-oriented design is especially useful in Swift because structs do not support class inheritance.

1. Abstraction

Use a protocol to describe what a service does, not how it performs the work. This makes a use case independent from HTTP, persistence, or payment-provider details.

Protocol as an abstraction
import Foundation
protocol PaymentProcessing {
    func pay(amount: Decimal) async throws
}
struct Checkout {
    let payment: any PaymentProcessing
    func confirm(total: Decimal) async throws {
        try await payment.pay(amount: total)
    }
}

2. Encapsulation

Protect invariants with access control. private(set) lets callers observe a value while requiring mutations to go through validated methods. A computed balance also avoids duplicated derived state.

Protect invariants
struct Cart {
    private(set) var items: [String] = []
    var itemCount: Int { items.count }
    mutating func add(_ item: String) {
        guard !item.isEmpty else { return }
        items.append(item)
    }
}

3. Inheritance

Only classes can inherit from other classes in Swift. Override behavior when there is a genuine is-a relationship; avoid deep hierarchies that make behavior difficult to predict. Mark a class final when subclassing is unnecessary.

A small class hierarchy
class Screen {
    func title() -> String { "Screen" }
}
final class HomeScreen: Screen {
    override func title() -> String { "Home" }
}

4. Polymorphism

Callers interact with a protocol while runtime implementations vary. Swift supports this through protocol existentials and generics; inheritance is not required.

Protocol polymorphism
import Foundation
struct CardPayment: PaymentProcessing {
    func pay(amount: Decimal) async throws { /* Call card gateway */ }
}
struct BankTransfer: PaymentProcessing {
    func pay(amount: Decimal) async throws { /* Start transfer */ }
}
func complete(_ payment: any PaymentProcessing) async throws {
    try await payment.pay(amount: 10)
}

Composition versus inheritance

Inheritance means HomeScreen IS A Screen: behavior is coupled to a base class and changes to that parent can affect descendants. Composition means an AccountsViewModel HAS AN AccountsRepository and a formatter; it delegates work to collaborators. When the repository changes from network to mock, the ViewModel keeps the same type. Composition is therefore often easier to test and evolve. Prefer it for services and cross-cutting behaviors, but inheritance is still valid for stable class specializations.

Composition: swap parts without subclassing the ViewModel
struct Account { let name: String }
protocol AccountsRepository {
    func load() async throws -> [Account]
}
struct AccountsViewModel<Repository: AccountsRepository> {
    let repository: Repository
    func names() async throws -> [String] {
        try await repository.load().map(\.name)
    }
}
struct PreviewRepository: AccountsRepository {
    func load() async throws -> [Account] {
        [Account(name: "Checking")]
    }
}
Decision guide
ApproachGood fitMain trade-off
Protocol + compositionReplaceable services, independent collaboratorsMore types and wiring
Class inheritanceGenuine stable is-a specializationTight parent–child coupling
Generic compositionFixed collaborator type, compile-time specializationMore type parameters
any ProtocolRuntime choice between implementationsExistential indirection and lost concrete identity

Interview traps

A protocol is not automatically an abstraction of good quality; avoid pointless one-method protocols. Encapsulation is not just marking fields private: it also means protecting invariants. Composition does not eliminate all coupling—it moves it into explicit dependencies. Do not confuse protocol conformance with class inheritance.

SOURCES & DOCUMENTATION

Personal study notes. Check details against your target Swift/iOS version and official documentation.