Unit 4 · Lesson 4

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, and public — to fields and methods
  • Explain why hardware fields should always be private in 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.

Access Modifier Matrix — click any row
Modifier Same class Same package Subclass Anywhere
private
(default)
protected
public
← click a row to see how it applies in FRC subsystem design

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.

// ❌ Poorly encapsulated — everything is public public class IntakeSubsystem { public TalonFX motor; // anyone can call motor.set(9999) public DigitalInput beamBreak; // anyone can read OR write the sensor public double speed = 0.0; // anyone can set speed to anything public boolean running; // state can be set without moving the motor public IntakeSubsystem() { motor = new TalonFX(8); beamBreak = new DigitalInput(0); } } // In Robot.java — external code bypasses all intent: intake.speed = 2.5; // out-of-range — motor clips at 1.0 but logs an error intake.running = true; // state says running, motor is actually stopped intake.motor.set(-1.0); // reversing at full speed, bypassing the speed variable

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.

// ✅ Properly encapsulated public class IntakeSubsystem { private final TalonFX motor; private final DigitalInput beamBreak; private double currentSpeed = 0.0; private boolean running = false; public IntakeSubsystem() { motor = new TalonFX(Constants.INTAKE_MOTOR_ID); beamBreak = new DigitalInput(Constants.BEAM_BREAK_PORT); } // ── Public interface — the ONLY way to interact with this subsystem ── public void run() { motor.set(Constants.INTAKE_SPEED_FWD); currentSpeed = Constants.INTAKE_SPEED_FWD; running = true; } public void stop() { motor.set(0.0); currentSpeed = 0.0; running = false; } // Getter — read state without exposing the field public boolean isRunning() { return running; } public boolean hasGamePiece() { return beamBreak.get(); } public double getSpeed() { return currentSpeed; } } // In Robot.java — callers use the public interface, nothing else: intake.run(); // runs at the configured safe speed if (intake.hasGamePiece()) { intake.stop(); } // reads sensor through method

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.

// Getter — read-only access to private state public double getCurrentAngle() { return currentAngleDeg; } public boolean isCalibrated() { return calibrated; } // Setter with validation — rejects dangerous values public void setTargetAngle(double angleDeg) { if (angleDeg < MIN_ANGLE_DEG || angleDeg > MAX_ANGLE_DEG) { logFault("Arm angle out of range: " + angleDeg); return; // silently reject — don't move the arm } targetAngleDeg = angleDeg; } // Setter with transformation — enforces the output range public void setMotorOutput(double output) { // Clamp to [-1.0, 1.0] regardless of what was passed motor.set(MathUtil.clamp(output, -1.0, 1.0)); }

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.

💡 The tell-don't-ask principle

"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.

🔍 LRI Observation

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

⚙️ Encapsulation Standards for Competition Code
  • 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

A subsystem class has a public TalonFX driveMotor field. A programmer in Robot.java writes subsystem.driveMotor.set(1.5). What is the problem?
  • 11.5 is a valid motor output — TalonFX accepts any double
  • 2A compile error — fields of subsystem classes cannot be accessed from Robot.java
  • 3External code bypasses any validation or safety logic the subsystem provides, sets a value outside the valid range (-1.0 to 1.0), and the subsystem's state variables (like a speed tracker or running flag) are now inconsistent with what the motor is actually doing
  • 4No problem — public fields are designed for this pattern
You write a setTargetAngle(double degrees) setter. The arm's valid range is 0–120 degrees. What should the setter do if called with 150 degrees?
  • 1Set the arm to 120 degrees automatically — clamp to the nearest valid value
  • 2Throw an exception — invalid inputs must not be silently handled
  • 3It depends on the context — clamping is appropriate if the caller might naturally overshoot (joystick-driven), while silent rejection with logging is appropriate if 150 degrees represents a programming error that should be caught; the important thing is that the arm never attempts to reach 150 degrees regardless of which approach is chosen
  • 4Pass 150 through unchanged — let the hardware's built-in limits handle it
An 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?
  • 1Java doesn't allow boolean setters
  • 2Only the subsystem class owner can write setters
  • 3A setRunning(true) call would update the boolean flag without moving the motor — creating an inconsistency between the state variable and hardware. The existing run() and stop() methods do both together atomically. The setter recreates the exact problem encapsulation was meant to prevent.
  • 4The teammate should use reflection to access the private field instead
💪 Practice Prompt

Encapsulate a Real Subsystem

  1. Take your ClimberSubsystem from the previous lessons. Audit every field: are any public? Make them all private final. Verify the class still compiles — if anything in Robot.java was accessing fields directly, those lines will now error. Fix each error by adding or using a public method instead.
  2. 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.
  3. Add a getter getExtendSpeedPercent() that returns the current commanded speed. Post it to SmartDashboard as "Climber Speed" in teleopPeriodic().
  4. 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.
  5. Bonus: Review Team 2910's most recent public subsystem class. List every public method and every private field. Write a paragraph explaining what the public interface allows callers to do and what it prevents them from doing. Is there anything public that you think should be private, or vice versa?