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
- Why Dependency Injection
- Constructor Injection
- Protocol-Based Abstractions
- Mocking Dependencies in Tests