Encapsulation
Encapsulation is the practice of hiding an object's internal state and exposing only a controlled, intentional interface. In robot code, it's the difference between a subsystem that can only be broken by using it correctly and one that can be broken from anywhere in the codebase by anyone who knows the field name.
By the end of this lesson, you will:
- Apply all four Java access modifiers —
private, default (package-private),protected, andpublic— to fields and methods - Explain why hardware fields should always be
privatein a subsystem class - Write getters and setters with validation logic that protects hardware state
- Refactor a poorly encapsulated class into one with a clean public interface
- Recognize when a getter or setter is unnecessary and when it adds real value
What Encapsulation Means in Practice
Encapsulation has two parts: hiding internal state (private fields) and exposing a controlled interface (public methods). Together they enforce that the only way to interact with a subsystem is through the methods it explicitly provides.
On a robot, this matters because hardware state is consequential. A motor set to 1.5 (out of range), a current limit set to 200 amps, or a boolean flag set to the wrong value at the wrong moment can damage hardware or lose a match. Encapsulation puts validation logic between external code and hardware state, making those mistakes impossible rather than merely unintentional.
Access Modifiers
Java has four access levels, from most restrictive to most permissive. Click any row in the matrix to see which contexts can access that level and how it applies in FRC code.
| Modifier | Same class | Same package | Subclass | Anywhere |
|---|---|---|---|---|
private |
✓ | ✗ | ✗ | ✗ |
| (default) | ✓ | ✓ | ✗ | ✗ |
protected |
✓ | ✓ | ✓ | ✗ |
public |
✓ | ✓ | ✓ | ✓ |
The Encapsulation Refactor
Compare these two implementations of the same intake subsystem. Both compile. Both "work." Only one makes the class safe to use from the rest of the robot code.
All fields are public. Nothing stops external code from reaching into the subsystem and directly commanding hardware — bypassing safety checks, validation, and state tracking entirely.
Fields are private. Public methods form the only API. Each method can validate its inputs, maintain consistent state, and enforce hardware safety — callers can't reach around the interface.
Getters and Setters with Validation
A getter returns the value of a private field. A setter sets it — but can also validate or transform the value before assigning it. This is where encapsulation provides real protection against hardware damage.
When a getter or setter is unnecessary
Not every private field needs a getter and setter. The question is: does external code need this value? Does external code need to set this value? If not, leave the field private with no accessor. A public int getCanId() on a subsystem is almost certainly useless — nothing outside the class needs to read the CAN ID at runtime.
"Tell, don't ask" is a guideline that applies directly to robot subsystem design. Instead of asking a subsystem for its state and then deciding what to do with it, tell the subsystem what behavior you want. intake.run() is better than intake.getMotor().set(intake.getConfiguredSpeed()) — the first form encapsulates the intent, the second reaches through the interface and recreates the logic the class already knows.
The pattern I see most often in public fields is this: a motor field is public because "it was easier to just access it directly." Then, over a build season, direct motor commands appear in five different files. When the team wants to add current monitoring to the intake, they have to find and update all five sites. When they want to add safety checks, same problem. A public TalonFX motor feels like a shortcut but adds maintenance cost every time someone uses it. Five minutes making it private and writing a setOutput() method saves that cost for the life of the project.
🔌 System Check
- Every hardware object field is
private final.TalonFX,CANcoder,DigitalInput,XboxController— all private. If external code needs to interact with hardware, it calls a method that does it safely. There is no exception to this rule. - State variables (boolean flags, running speeds, target positions) are private. A public state variable can be set to any value from anywhere in the codebase, creating an inconsistency between the variable and the hardware it represents.
- Setters with validation are not optional for safety-critical values. Any setter that sets a motor output, target angle, or current limit should clamp or validate its input. Silent rejection (log and return) is better than silently passing an invalid value to hardware.
- The public interface should express intent, not mechanism.
intake.run()expresses intent.intake.motor.set(0.6)expresses mechanism. External code should never need to know how the intake works internally — only what it can do. - Before adding a getter, ask: does the caller really need this value? A getter for every private field is not encapsulation — it's encapsulation bypassed through indirection. Only expose what callers genuinely need to read.
Knowledge Check
public TalonFX driveMotor field. A programmer in Robot.java writes subsystem.driveMotor.set(1.5). What is the problem?setTargetAngle(double degrees) setter. The arm's valid range is 0–120 degrees. What should the setter do if called with 150 degrees?IntakeSubsystem has private boolean running and methods run() and stop(). A teammate asks for a setRunning(boolean) setter so they can toggle the intake from outside. Why should you decline?Encapsulate a Real Subsystem
- Take your
ClimberSubsystemfrom the previous lessons. Audit every field: are anypublic? Make them allprivate final. Verify the class still compiles — if anything inRobot.javawas accessing fields directly, those lines will now error. Fix each error by adding or using a public method instead. - Add a
setExtendSpeed(double speed)setter that clamps the input to [0.0, 1.0] (no reverse) and logs a fault message if the input was out of range. Test by calling it with 1.5 — the motor should receive 1.0, not 1.5. - Add a getter
getExtendSpeedPercent()that returns the current commanded speed. Post it to SmartDashboard as "Climber Speed" inteleopPeriodic(). - Identify which getters you added are genuinely useful (something external code needs) and which are only there "in case." Remove any getter that has no current caller — you can always add it back when something actually needs it.
- Bonus: Review Team 2910's most recent public subsystem class. List every
publicmethod and everyprivatefield. Write a paragraph explaining what the public interface allows callers to do and what it prevents them from doing. Is there anythingpublicthat you think should beprivate, or vice versa?