Why Use Dependency Injection?

Dependency Injection (DI) is a design principle where objects receive their dependencies from an external source rather than creating them internally. In the context of FRC and robot subsystems, this pattern helps us write cleaner, more modular, and more testable code.


The Problem with Direct References

When a subsystem directly creates or accesses another subsystem, it introduces tight coupling. For example:


public class Shooter {
    private final Intake intake = new Intake(); // 🚫 Bad
}

Why this is bad:

  • Tightly coupled: Changes to Intake may force changes in Shooter.

  • Hard to test: You can’t replace Intake with a fake or mock.

  • Hidden dependencies: It’s unclear what external systems the class depends on.

  • Lifecycle issues: Creating a new instance may interfere with the actual robot code using a different instance.


The Dependency Injection Solution

Instead of constructing dependencies internally, we inject them through the constructor (or a factory):

public class Shooter {
    private final Intake intake;

    public Shooter(Intake intake) {
        this.intake = intake; // ✅ Good
    }
}

Benefits:

  • Loose coupling: Shooter can work with any class that behaves like an Intake.

  • Testability: Easily pass in mock or fake subsystems.

  • Explicit dependencies: It's clear what the class needs to function.

  • Flexible structure: Easier to refactor and swap implementations.


Example in Robot Code

Here’s how this pattern might look in a RobotContainer setup:

public class RobotContainer {
    private final Intake intake = new Intake();
    private final Shooter shooter = new Shooter(intake);
}

By constructing and wiring everything in one place (RobotContainer), we:

  • Control the system architecture from a central location.

  • Avoid surprises or duplicated instances.

  • Make it easier to debug and understand the flow of data.


Why It’s Especially Useful in FRC

  • Your subsystems often rely on shared sensors or motors.

  • Logging, replay, and simulation are easier when subsystems are isolated and injectable.

  • You can swap in simulations or mocks without rewriting the entire system.

  • More scalable as your robot grows in complexity.


Anti-Patterns to Avoid

Avoid things like:

  • SubsystemA.getInstance() from inside SubsystemB (singleton misuse).

  • Calling new Subsystem() inside other subsystems.

  • Statically referencing global hardware components.

These lead to spaghetti code, where everything is tangled and hard to debug or refactor.

Last updated