0Pricing
Swift Academy · Lesson

Why Dependency Injection

Separate construction from use for testability.

Why Dependency Injection is a free Swift Academy lesson on CoddyKit — lesson 1 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.

Why Dependency Injection

Dependency injection (DI) means giving an object the things it needs from outside, instead of letting it create them itself. This decouples components and makes them easier to test.

The Problem: Hard Coupling

When a type constructs its own collaborators, it is locked to those concrete classes and cannot be reconfigured or tested in isolation.

class Logger {
    func log(_ m: String) { print("LOG: \(m)") }
}
class Service {
    let logger = Logger() // hard-coded dependency
    func run() { logger.log("running") }
}
Service().run()

Coupling Hurts Testing

Because Service always makes a real Logger, a test cannot capture or fake the log output. The dependency is invisible from outside.

class Logger { func log(_ m: String) { print(m) } }
class Service {
    let logger = Logger()
    func run() { logger.log("hello") }
}
// No way to swap Logger here.
Service().run()

The Idea: Inject It

Instead, pass the dependency in. The caller decides which implementation to provide, so Service no longer cares.

class Logger { func log(_ m: String) { print("LOG: \(m)") } }
class Service {
    let logger: Logger
    init(logger: Logger) { self.logger = logger }
    func run() { logger.log("running") }
}
Service(logger: Logger()).run()

Decoupling Benefits

With injection, the same Service works with different loggers, in production or in tests, without changing its code.

class Logger { var lines: [String] = []
    func log(_ m: String) { lines.append(m) } }
class Service {
    let logger: Logger
    init(logger: Logger) { self.logger = logger }
    func run() { logger.log("event") }
}
let l = Logger()
Service(logger: l).run()
print(l.lines)

Single Responsibility

DI supports the single-responsibility principle: a type focuses on its job and delegates other concerns to injected collaborators.

struct Mailer { func send(_ to: String) { print("Mail to \(to)") } }
struct SignupFlow {
    let mailer: Mailer
    func register(_ email: String) { mailer.send(email) }
}
SignupFlow(mailer: Mailer()).register("a@b.com")

Flexibility

You can recombine objects in new ways. The wiring happens at the edges of your app, keeping the core flexible.

struct Engine { let power: Int }
struct Car {
    let engine: Engine
    func describe() { print("Power: \(engine.power)") }
}
Car(engine: Engine(power: 200)).describe()
Car(engine: Engine(power: 90)).describe()

Explicit Dependencies

An initializer that lists its dependencies documents exactly what the type needs. Hidden, self-created dependencies hide that contract.

struct Repo { func load() -> [Int] { [1, 2, 3] } }
struct Report {
    let repo: Repo // visible dependency
    func total() -> Int { repo.load().reduce(0, +) }
}
print(Report(repo: Repo()).total())

Composition Over Construction

DI favors composing objects from outside. A small composition root assembles the graph; the rest of the code just receives what it needs.

struct DB { func count() -> Int { 42 } }
struct Stats { let db: DB; func show() { print(db.count()) } }
// Composition root:
let app = Stats(db: DB())
app.show()

Not Just for Big Apps

Even tiny programs benefit: injecting a dependency makes behavior swappable and intentions clear without extra frameworks.

struct Clock { func now() -> Int { 12 } }
struct Greeter {
    let clock: Clock
    func greet() { print(clock.now() < 12 ? "Morning" : "Afternoon") }
}
Greeter(clock: Clock()).greet()

No Framework Needed

Swift DI needs no library. Plain initializers and protocols are enough. The upcoming lessons show constructor injection, protocols, and mocking.

struct Adder { func add(_ a: Int, _ b: Int) -> Int { a + b } }
struct Calc {
    let adder: Adder
    func run() { print(adder.add(2, 3)) }
}
Calc(adder: Adder()).run()

Quick Check

What is the core idea of dependency injection?

Recap

Dependency injection supplies collaborators from outside rather than constructing them internally, reducing coupling, clarifying dependencies, and enabling testing. It needs no framework in Swift. Next you will see the most common form: constructor injection.

Frequently asked questions

Is the “Why Dependency Injection” lesson free?

Yes — the full text of “Why Dependency Injection” 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 “Why Dependency Injection”?

Separate construction from use for testability. 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 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Why Dependency Injection” 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