Dependency Injection

intermediate30 min

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 follow

Constructor 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 ownInjected
Testable without hardwareNoYes
Reusable on other robotsNoYes
Swap implementationsEdit the classChange one wiring line
Where configuration livesScatteredOne place
Constructor complexitySimplerMore 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 Supplier is 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

  1. Rewrite a class that constructs its own dependency so the dependency is passed in.
  2. Define a MotorController interface, a real implementation, and a fake one that records calls.
  3. Write a Shooter taking a MotorController and test it with the fake — no hardware.
  4. Write a class taking a DoubleSupplier and show that it sees updated values when the source changes.
  5. Write a small composition root creating two subsystems and their dependencies in one place.
Hints
  1. Move the new out of the constructor and into a parameter.
  2. The fake stores whatever it is told so the test can inspect it afterwards.
  3. Call a method, then assert against the fake’s recorded field.
  4. Back the supplier with a mutable field; change it between calls.
  5. A class whose fields are the real objects, constructed in declaration order. Note that order matters — a dependency must exist before whatever needs it.

Next

Related