From 419948f0e52ab6ee5631f6fca47c3ff2545eb43c Mon Sep 17 00:00:00 2001 From: Alex Schokking Date: Sat, 3 Oct 2026 14:35:06 -0700 Subject: [PATCH] Fold wiki DI/factory lesson content into core-programming patterns The curriculum sequence now routes students from Command Groups into these two pages instead of duplicating the wiki lessons, so they need to cover what those lessons taught. dependency-injection.md: - new "Real hardware vs. mocks" section: RobotComponent vs SimulationComponent, Robot.createDaggerComponent() choosing between them, and the MockDevicesModule @Binds that swaps the motor controller factory. The page had no mention of simulation or mocks at all, which is the whole reason DI matters to a student running unit tests on a laptop. - explains how tests pull objects out of Dagger via getInjectorComponent(), and why BaseRobotComponent needs a line per directly-requestable type - names constructor injection - fixes "public class ShooterSubsystem() {" - a class declaration cannot take parentheses, so the sample would not compile providers-factories.md: - new "What's a Factory?" opener with the Car/CarFactory analogy and the DriveSubsystem motorControllerFactory call students already wrote in Tank Drive. The page previously opened at Dagger level, which is a steep start for someone arriving from the challenges. - adds XGyroFactory and XSolenoidFactory to the factory table All class and factory names verified against the current SeriouslyCommonLib and XbotEdu sources. Co-Authored-By: Claude Opus 5 (1M context) --- .../patterns/dependency-injection.md | 84 ++++++++++++++++++- .../patterns/providers-factories.md | 48 +++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/docs/core-programming/patterns/dependency-injection.md b/docs/core-programming/patterns/dependency-injection.md index 219b5c3..4b10740 100644 --- a/docs/core-programming/patterns/dependency-injection.md +++ b/docs/core-programming/patterns/dependency-injection.md @@ -51,7 +51,7 @@ Without DI, you would create dependencies manually: ```java // BAD: Hard to test, tightly coupled // Every dependency is hardcoded inside the class -public class ShooterSubsystem() { +public class ShooterSubsystem { private ElectricalContract contract = new CompetitionContract(); // Always this contract private PIDManager pid = new PIDManager(...); // Always these values private MotorController motor = new TalonFX(...); // Always this motor type @@ -269,6 +269,88 @@ If Dagger cannot find a way to provide a dependency, you get a **compile error** +Handing a class what it needs through its constructor, rather than letting it build its own dependencies, is called **constructor injection**. Every `@Inject` constructor you have written is an example of it. + +### 4. Real hardware vs. mocks + +This is where dependency injection earns its keep, and it is the reason your unit tests can run on a laptop with no robot attached. + +There are two components, built from different modules: + +[Source: XbotEdu injection components](https://github.com/Team488/XbotEdu/tree/main/src/main/java/competition/injection/components) + +```java +// On a real robot: real motors, real gamepads +@Singleton +@Component(modules = { RobotModule.class, RealDevicesModule.class, + RealControlsModule.class, CompetitionModule.class }) +public abstract class RobotComponent extends BaseRobotComponent { } + +// In simulation and unit tests: fake devices +@Singleton +@Component(modules = { SimulationModule.class, MockDevicesModule.class, + RealControlsModule.class, SimulatedRobotModule.class }) +public abstract class SimulationComponent extends BaseRobotComponent { } +``` + +Both extend the same `BaseRobotComponent`, so both can produce the same list of objects -- they just build them from different parts. `Robot.java` picks one at startup: + +```java +protected BaseRobotComponent createDaggerComponent() { + if (BaseRobot.isReal()) { + return DaggerRobotComponent.create(); + } else { + return DaggerSimulationComponent.create(); + } +} +``` + +The swap itself lives in the modules. `MockDevicesModule` binds the same factory interface to a mock implementation: + +[Source: SeriouslyCommonLib MockDevicesModule](https://github.com/Team488/SeriouslyCommonLib/blob/main/src/main/java/xbot/common/injection/modules/MockDevicesModule.java) + +```java +@Module +public abstract class MockDevicesModule { + // "When anything asks for a motor controller factory, hand it the mock one" + @Binds + @Singleton + public abstract XCANMotorController.XCANMotorControllerFactory getMotorControllerFactory( + MockCANMotorController.MockCANMotorControllerFactory impl); +} +``` + +**The payoff:** `DriveSubsystem` asks for an `XCANMotorControllerFactory` and never learns which kind it got. On the robot it receives real motors; in your tests it receives fake ones. The subsystem code does not change, and there is no `if (testMode)` branch anywhere in it. + +
+How do tests get objects out of Dagger? + +A test asks the component for what it needs, instead of calling `new`: + +```java +public class TankDriveTest extends BaseDriveTest { + @Test + public void test() { + BaseCommand command = this.getInjectorComponent().tankDriveWithJoysticksCommand(); + // ... the command arrives fully wired, with mock hardware underneath + } +} +``` + +For that to compile, the component has to expose the type. `BaseRobotComponent` declares one abstract method per object a test can request: + +```java +public abstract class BaseRobotComponent extends BaseComponent { + public abstract TankDriveWithJoysticksCommand tankDriveWithJoysticksCommand(); + public abstract DriveToPositionCommand driveToPositionCommand(); + // ... one line per directly-requestable object +} +``` + +Most classes never need an entry here -- an `@Inject` constructor is enough for Dagger to build them as a dependency of something else. You only add a line when a test (or `Robot.java`) needs to ask for that object **directly**. + +
+ ## Scopes [Source: SeriouslyCommonLib SwerveSingleton](https://github.com/Team488/SeriouslyCommonLib/blob/main/src/main/java/xbot/common/injection/swerve/SwerveSingleton.java) diff --git a/docs/core-programming/patterns/providers-factories.md b/docs/core-programming/patterns/providers-factories.md index 638e2d5..e754bc1 100644 --- a/docs/core-programming/patterns/providers-factories.md +++ b/docs/core-programming/patterns/providers-factories.md @@ -2,6 +2,52 @@ Creating objects that need runtime parameters. +## What's a Factory? + +A **Factory** is a class whose only job is to create instances of other objects. + +Some objects are a chore to build. Take a `Car`: to create one you need its Wheels, Doors, Windshield and so on. You would not want to assemble all of those by hand every time you needed a car. + +```java +public class Car { + public Car(Wheel frontLeft, Wheel frontRight, Wheel rearLeft, Wheel rearRight, ...) { + // ... + } +} +``` + +A factory hides that work behind one call: + +```java +public class CarFactory { + public static Car create() { + Wheel frontLeftWheel = new Wheel(); + Wheel frontRightWheel = new Wheel(); + // .. create all other car parts .. + return new Car(frontLeftWheel, frontRightWheel, ...); + } + + // A factory can also make variations + public static Car createSillyCar() { + // .. same idea, but with SquareWheel instead .. + } +} +``` + +```java +Car myCar = CarFactory.create(); +Car mySillyCar = CarFactory.createSillyCar(); +``` + +**You have already used one of these.** Back in the Tank Drive challenge, `DriveSubsystem` got its motors from a factory rather than constructing them itself: + +```java +this.frontLeft = motorControllerFactory + .create(new CANMotorControllerInfo("FrontLeft", 1), this.getPrefix(), "FrontLeft"); +``` + +The rest of this page is about *why* XBot builds objects this way, and how the factories are wired up. + ## The Problem [Source: SeriouslyCommonLib PIDManager](https://github.com/Team488/SeriouslyCommonLib/blob/main/src/main/java/xbot/common/math/PIDManager.java) @@ -128,6 +174,8 @@ The key insight: **Dagger provides the factory, you provide the runtime values.* | `HeadingModule.HeadingModuleFactory` | Heading PID modules | | `XDigitalInput.XDigitalInputFactory` | Digital sensors | | `XLaserCAN.XLaserCANFactory` | Laser distance sensors | +| `XGyro.XGyroFactory` | Gyros | +| `XSolenoid.XSolenoidFactory` | Solenoids | ## Usage Pattern