Dependency Injection
The problem with building your own
Here is a class that creates what it depends on:
class Shooter {
private final Motor motor;
Shooter() {
this.motor = new Motor(5); // decides for itself
}
void spinUp() {
motor.set(0.9);
}
}
This works, and it is what most people write first. But three things are now impossible.
You cannot test it without real hardware — constructing a Shooter constructs a Motor on port 5. You cannot reuse it on a robot where the shooter is on a different port. And you cannot substitute a different kind of motor without editing the class.
The class made a decision that was not really its business.
Passing it in instead
class Shooter {
private final Motor motor;
Shooter(Motor motor) { // told, not decided
this.motor = motor;
}
void spinUp() {
motor.set(0.9);
}
}
The change is small. The consequences are not.
Motor shooterMotor = new Motor(5);
Shooter shooter = new Shooter(shooterMotor);
The decision about which motor moved outward, to the code assembling the robot. Shooter now describes behaviour, and something else decides configuration.
That is dependency injection: a class receives what it needs rather than constructing it. The name is grander than the idea.
Prefer constructor injection
Passing dependencies through the constructor is the best of the three options:
// constructor injection — preferred
Shooter(Motor motor) { this.motor = motor; }
// setter injection — object exists in an incomplete state first
void setMotor(Motor motor) { this.motor = motor; }
// field injection via a framework — hidden, hard to followConstructor injection lets you mark the field final, and it makes the object valid the moment it exists. With a setter, there is a window where motor is null and any method call fails.
Use a setter only for genuinely optional dependencies.
Injecting an interface
The full benefit arrives when the parameter is an interface rather than a concrete class.
interface MotorController {
void set(double output);
void stop();
}
class Shooter {
private final MotorController motor;
Shooter(MotorController motor) {
this.motor = motor;
}
void spinUp() { motor.set(0.9); }
void stop() { motor.stop(); }
}
Shooter now works with anything satisfying that interface — a vendor’s motor class, a simulated one, or a recording one used for diagnostics. It does not know or care, thanks to runtime polymorphism.
Testing becomes possible
This is the payoff that convinces most people.
class FakeMotor implements MotorController {
double lastOutput = 0;
boolean stopped = false;
@Override public void set(double output) { lastOutput = output; }
@Override public void stop() { stopped = true; }
}
FakeMotor fake = new FakeMotor();
Shooter shooter = new Shooter(fake);
shooter.spinUp();
System.out.println(fake.lastOutput); // 0.9 — verified with no hardware
shooter.stop();
System.out.println(fake.stopped); // true
You just tested shooter logic on a laptop. With the original version this test could not be written at all, because the constructor would try to talk to a motor controller that is not there.
Injecting values, not just objects
The same reasoning applies to plain values and to suppliers.
class DriveCommand {
private final Drivetrain drive;
private final DoubleSupplier forward;
private final DoubleSupplier rotation;
DriveCommand(Drivetrain drive, DoubleSupplier forward, DoubleSupplier rotation) {
this.drive = drive;
this.forward = forward;
this.rotation = rotation;
}
void execute() {
drive.arcadeDrive(forward.getAsDouble(), rotation.getAsDouble());
}
}
new DriveCommand(drivetrain,
() -> -joystick.getLeftY(),
() -> joystick.getRightX());
Injecting suppliers rather than numbers is what keeps the command reading live inputs. Passing joystick.getLeftY() directly would freeze one reading forever — the mistake described in the Supplier lesson.
For a test, inject constants:
new DriveCommand(fakeDrive, () -> 0.5, () -> 0.0);
Where the wiring goes
If every class receives its dependencies, something must eventually create the real objects. That job collects in one place — often called the composition root.
class RobotContainer {
private final MotorController shooterMotor = new Motor(5);
private final MotorController intakeMotor = new Motor(6);
private final Shooter shooter = new Shooter(shooterMotor);
private final Intake intake = new Intake(intakeMotor);
// ... bind commands here
}
All hardware decisions live in this file. Every other class is portable, testable, and free of port numbers. If you have written FRC command-based code, this is exactly what RobotContainer is for, and now the reason is visible.
You do not need a framework
Dependency injection is often confused with the frameworks that automate it. Those solve a real problem in very large applications, and they are unnecessary here.
Passing arguments to constructors is dependency injection. Nothing more is required.
What changes
| Building its own | Injected | |
|---|---|---|
| Testable without hardware | No | Yes |
| Reusable on other robots | No | Yes |
| Swap implementations | Edit the class | Change one wiring line |
| Where configuration lives | Scattered | One place |
| Constructor complexity | Simpler | More parameters |
The last row is the honest cost. Constructors get longer, and wiring code appears. For a small program that trade is not always worth it — but for anything you intend to test or reuse, it is.
Common mistakes
- Injecting a concrete class instead of an interface, keeping you locked in.
- Setter injection by default, leaving a window where fields are
null. - A constructor with eight parameters. That is a sign the class does too much — split it.
- Injecting a value where a
Supplieris needed, freezing a changing reading. - Reaching for a framework to solve a problem that constructor parameters already solve.
- Keeping a hidden dependency on a static or global, which defeats the whole exercise.
Practice
- Rewrite a class that constructs its own dependency so the dependency is passed in.
- Define a
MotorControllerinterface, a real implementation, and a fake one that records calls. - Write a
Shootertaking aMotorControllerand test it with the fake — no hardware. - Write a class taking a
DoubleSupplierand show that it sees updated values when the source changes. - Write a small composition root creating two subsystems and their dependencies in one place.
Hints
- Move the
newout of the constructor and into a parameter. - The fake stores whatever it is told so the test can inspect it afterwards.
- Call a method, then assert against the fake’s recorded field.
- Back the supplier with a mutable field; change it between calls.
- A class whose fields are the real objects, constructed in declaration order. Note that order matters — a dependency must exist before whatever needs it.