0Pricing
Swift Academy · Lesson

Protocol-Based Abstractions

Depend on protocols, not concrete types.

Protocol-Based Abstractions is a free Swift Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Protocol-Based Abstractions

Injecting a protocol instead of a concrete type lets you swap implementations freely. The consumer depends only on the behavior, not the class.

Define a Protocol

Describe the capability as a protocol. Any type that conforms can be supplied as the dependency.

protocol Logging {
    func log(_ message: String)
}
print("Protocol describes behavior, not a concrete class.")

Conform a Concrete Type

A real implementation conforms to the protocol. It is the production dependency.

protocol Logging { func log(_ m: String) }
struct ConsoleLogger: Logging {
    func log(_ m: String) { print("LOG: \(m)") }
}
ConsoleLogger().log("ready")

Depend on the Protocol

The consumer's property type is the protocol, so it accepts any conforming value.

protocol Logging { func log(_ m: String) }
struct ConsoleLogger: Logging { func log(_ m: String) { print(m) } }
struct Service {
    let logger: Logging // protocol, not ConsoleLogger
    func run() { logger.log("running") }
}
Service(logger: ConsoleLogger()).run()

Swapping Implementations

Because the type is a protocol, you can pass a different conforming implementation without touching the consumer.

protocol Logging { func log(_ m: String) }
struct Loud: Logging { func log(_ m: String) { print(m.uppercased()) } }
struct Quiet: Logging { func log(_ m: String) {} }
struct Service { let logger: Logging; func run() { logger.log("hi") } }
Service(logger: Loud()).run()
Service(logger: Quiet()).run()

Programming to Interfaces

This is the "depend on abstractions, not concretions" principle. High-level code is shielded from low-level details.

protocol Storage { func save(_ v: Int) }
struct MemoryStore: Storage { func save(_ v: Int) { print("kept \(v)") } }
struct Manager { let store: Storage; func add() { store.save(1) } }
Manager(store: MemoryStore()).add()

Multiple Conformers

Several types can conform to the same protocol, each suited to a context such as network, disk, or memory.

protocol Source { func value() -> Int }
struct Fixed: Source { func value() -> Int { 10 } }
struct Doubler: Source { func value() -> Int { 20 } }
func use(_ s: Source) { print(s.value()) }
use(Fixed())
use(Doubler())

Protocols with Associated Behavior

Protocols can declare several methods, forming a clear contract for the dependency.

protocol Cache {
    func get(_ key: String) -> Int?
    func set(_ key: String, _ v: Int)
}
struct Dict: Cache {
    var store: [String: Int] = [:]
    func get(_ k: String) -> Int? { store[k] }
    mutating func set(_ k: String, _ v: Int) { store[k] = v }
}
var c = Dict(); c.set("a", 1)
print(c.get("a") ?? -1)

Generic Constraints

You can also inject via a generic parameter constrained to the protocol, avoiding existential overhead.

protocol Greeter { func greet() -> String }
struct English: Greeter { func greet() -> String { "Hello" } }
struct Welcome<G: Greeter> {
    let greeter: G
    func show() { print(greeter.greet()) }
}
Welcome(greeter: English()).show()

Stable Contracts

As long as the protocol stays stable, implementations can change internally without breaking consumers.

protocol Formatter { func format(_ n: Int) -> String }
struct Plain: Formatter { func format(_ n: Int) -> String { "\(n)" } }
struct Padded: Formatter { func format(_ n: Int) -> String { String(format: "%03d", n) } }
func render(_ f: Formatter) { print(f.format(7)) }
render(Plain())
render(Padded())

Sets Up Mocking

Crucially, protocols let tests provide a fake conformer in place of the real one. That is the focus of the next lesson.

protocol Network { func fetch() -> String }
struct Real: Network { func fetch() -> String { "server data" } }
struct VM { let net: Network; func load() { print(net.fetch()) } }
VM(net: Real()).load()

Quick Check

Why inject a protocol type instead of a concrete class?

Recap

Injecting a protocol abstracts away the concrete type, so any conformer can be substituted. This follows "depend on abstractions" and enables alternate implementations and, importantly, test doubles. Next: mocking dependencies in tests.

Frequently asked questions

Is the “Protocol-Based Abstractions” lesson free?

Yes — the full text of “Protocol-Based Abstractions” is free to read here on the web, and the Swift Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Swift Academy course, upgrade to CoddyKit PRO.

What will I learn in “Protocol-Based Abstractions”?

Depend on protocols, not concrete types. You practise Swift Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Swift Academy?

No prior experience is required. Swift Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Protocol-Based Abstractions” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Swift Academy lesson?

Yes. Every Swift Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Why Dependency Injection
  2. Constructor Injection
  3. Protocol-Based Abstractions
  4. Mocking Dependencies in Tests
← Back to Swift Academy