Software Development Best Practices
Introduction to Software Development Best Practices
Good software development practices are essential for FTC teams. Well-organized, maintainable code is easier to debug, extend, and collaborate on. This lesson covers professional development practices that will improve your team's productivity and code quality.
Why Best Practices Matter
- Well-organized code is easier to debug and maintain
- Consistent naming and structure improve team collaboration
- Version control enables safe experimentation and rollbacks
- Modular design makes code reusable and testable
Code Organization and Structure
Organizing your code into logical, focused methods and classes makes it easier to understand, debug, and maintain. Break large OpModes into smaller, well-defined components.
Well-Organized OpMode - Basic Structure
public class WellOrganizedOpMode extends OpMode {
// Hardware components - grouped by function
private DcMotor leftDrive, rightDrive;
private Servo armServo, clawServo;
private ColorSensor colorSensor;
private DistanceSensor distanceSensor;
// State variables - clearly named and grouped
private RobotState currentState = RobotState.IDLE;
private double targetArmPosition = 0.5;
private boolean isClawOpen = false;
// Configuration constants
private static final double ARM_SPEED = 0.02;
private static final double CLAW_OPEN_POSITION = 1.0;
private static final double CLAW_CLOSED_POSITION = 0.0;
@Override
public void init() {
initializeHardware();
configureHardware();
resetRobotState();
displayInitializationStatus();
}
@Override
public void loop() {
processDriverInput();
updateRobotState();
controlHardware();
updateTelemetry();
}Understanding Code Organization
Good code organization makes your OpMode easier to understand, debug, and maintain. By grouping related variables together and breaking functionality into focused methods, you create a clear structure that other team members can easily follow.
Well-Organized OpMode - Hardware Methods
// Hardware initialization - focused and clear
private void initializeHardware() {
try {
// Initialize drive motors
leftDrive = hardwareMap.get(DcMotor.class, "left_drive");
rightDrive = hardwareMap.get(DcMotor.class, "right_drive");
// Initialize servos
armServo = hardwareMap.get(Servo.class, "arm_servo");
clawServo = hardwareMap.get(Servo.class, "claw_servo");
// Initialize sensors
colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
distanceSensor = hardwareMap.get(DistanceSensor.class, "distance_sensor");
} catch (Exception e) {
telemetry.addData("ERROR", "Hardware initialization failed: " + e.getMessage());
}
}
// Hardware configuration - separate from initialization
private void configureHardware() {
if (leftDrive != null && rightDrive != null) {
leftDrive.setDirection(DcMotor.Direction.FORWARD);
rightDrive.setDirection(DcMotor.Direction.REVERSE);
leftDrive.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
rightDrive.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
}
}
// State management - clear and focused
private void resetRobotState() {
currentState = RobotState.IDLE;
targetArmPosition = 0.5;
isClawOpen = false;
}Separation of Concerns in Hardware Methods
Separating hardware initialization from configuration makes the code more maintainable. Initialization handles getting references to hardware components, while configuration sets up their behavior. State management ensures the robot starts in a known, safe state.
Well-Organized OpMode - Input Processing Methods
// Input processing - handle one concern at a time
private void processDriverInput() {
processDriveInput();
processArmInput();
processClawInput();
processAutonomousTriggers();
}
private void processDriveInput() {
double forward = -gamepad1.left_stick_y;
double turn = gamepad1.right_stick_x;
double leftPower = forward + turn;
double rightPower = forward - turn;
// Apply deadband
if (Math.abs(leftPower) < 0.1) leftPower = 0.0;
if (Math.abs(rightPower) < 0.1) rightPower = 0.0;
setDrivePowers(leftPower, rightPower);
}
private void processArmInput() {
if (gamepad1.dpad_up) {
targetArmPosition = Math.min(1.0, targetArmPosition + ARM_SPEED);
} else if (gamepad1.dpad_down) {
targetArmPosition = Math.max(0.0, targetArmPosition - ARM_SPEED);
}
}
private void processClawInput() {
if (gamepad1.a && !isClawOpen) {
openClaw();
} else if (gamepad1.b && isClawOpen) {
closeClaw();
}
}
private void processAutonomousTriggers() {
if (gamepad1.x) {
startAutonomousSequence();
} else if (gamepad1.y) {
stopAutonomousSequence();
}
}Naming Conventions and Documentation
Consistent naming conventions and clear documentation help team members understand and maintain code. Follow established patterns and document complex logic thoroughly.
Naming Convention Guidelines
- Use descriptive names that explain what variables and methods do
- Follow camelCase for variables and methods (e.g., leftDriveMotor)
- Use UPPER_CASE for constants (e.g., MAX_MOTOR_POWER)
- Use PascalCase for class names (e.g., DriveController)
- Avoid abbreviations unless they are widely understood
Good Documentation Practices - Class Header
/**
* Autonomous OpMode for collecting game elements.
* This OpMode implements a state machine to drive to game elements,
* collect them using the intake mechanism, and return to the depot.
*
* @author Your Team Name
* @version 1.0
*/
public class AutonomousCollectorOpMode extends OpMode {
// Hardware components
private DcMotor leftDriveMotor, rightDriveMotor;
private DcMotor intakeMotor;
private Servo armServo;
private ColorSensor gameElementDetector;
// State management
private AutonomousState currentState = AutonomousState.SEARCHING;
private double startTime;
private int collectedElements = 0;
// Configuration constants
private static final double DRIVE_SPEED = 0.6;
private static final double TURN_SPEED = 0.4;
private static final double INTAKE_SPEED = 0.8;
private static final double ARM_UP_POSITION = 0.8;
private static final double ARM_DOWN_POSITION = 0.2;
private static final int MAX_COLLECTION_TIME_MS = 3000;Good Documentation Practices - Method Documentation
/**
* Initializes all hardware components.
* Maps hardware devices to variables and configures basic settings.
*/
private void initializeHardware() {
try {
// Initialize drive motors
leftDriveMotor = hardwareMap.get(DcMotor.class, "left_drive");
rightDriveMotor = hardwareMap.get(DcMotor.class, "right_drive");
// Initialize collection mechanism
intakeMotor = hardwareMap.get(DcMotor.class, "intake_motor");
armServo = hardwareMap.get(Servo.class, "arm_servo");
// Initialize sensors
gameElementDetector = hardwareMap.get(ColorSensor.class, "color_sensor");
} catch (Exception e) {
telemetry.addData("ERROR", "Hardware initialization failed: " + e.getMessage());
}
}
/**
* Configures robot settings for autonomous operation.
* Sets motor directions, zero power behaviors, and initial positions.
*/
private void configureRobot() {
// Configure drive motors
if (leftDriveMotor != null && rightDriveMotor != null) {
leftDriveMotor.setDirection(DcMotor.Direction.FORWARD);
rightDriveMotor.setDirection(DcMotor.Direction.REVERSE);
leftDriveMotor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
rightDriveMotor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
}
// Configure collection mechanism
if (armServo != null) {
armServo.setPosition(ARM_DOWN_POSITION); // Start with arm down
}
}Version Control with Git for FTC Projects
Version control is essential for team collaboration and safe experimentation. Git allows you to track changes, collaborate with teammates, and roll back to previous versions when needed.
Git Workflow for FTC Teams
// Git setup and workflow for FTC projects
// This is a guide for setting up version control
/*
* GIT SETUP FOR FTC PROJECTS
*
* 1. Initialize Git repository:
* git init
*
* 2. Create .gitignore file:
* # Android Studio files
* .idea/
* *.iml
* build/
*
* # FTC SDK files
* FtcRobotController/build/
* TeamCode/build/
*
* # Logs
* *.log
*
* # Temporary files
* *.tmp
* *.temp
*
* 3. Initial commit:
* git add .
* git commit -m "Initial FTC project setup"
*
* 4. Create development branch:
* git checkout -b development
*
* 5. Create feature branches for new features:
* git checkout -b feature/autonomous-collector
* git checkout -b feature/teleop-improvements
*
* 6. Regular workflow:
* # Make changes to code
* # Test changes
* git add .
* git commit -m "Add autonomous collector functionality"
* git push origin feature/autonomous-collector
*
* 7. Merge to development:
* git checkout development
* git merge feature/autonomous-collector
* git push origin development
*
* 8. Merge to main for competition:
* git checkout main
* git merge development
* git tag -a v1.0 -m "Competition version 1.0"
* git push origin main --tags
*/Version Controlled OpMode Example
// Example of how to structure your project with version control in mind:
public class VersionControlledOpMode extends OpMode {
// Version information - useful for tracking which version is running
private static final String VERSION = "1.0.0";
private static final String BUILD_DATE = "2024-01-15";
private static final String TEAM_NAME = "Team 1234";
// Configuration that might change between competitions
private static final class CompetitionConfig {
// These values might be different for each competition
public static final double DRIVE_SPEED_MULTIPLIER = 0.8;
public static final double TURN_SPEED_MULTIPLIER = 0.6;
public static final boolean ENABLE_SAFETY_FEATURES = true;
public static final double SAFETY_DISTANCE_CM = 10.0;
}
// Hardware components
private DcMotor leftDrive, rightDrive;
private Servo armServo;
private ColorSensor colorSensor;
@Override
public void init() {
// Display version information
telemetry.addData("Version", VERSION);
telemetry.addData("Build Date", BUILD_DATE);
telemetry.addData("Team", TEAM_NAME);
telemetry.addData("Config", "Drive: " + CompetitionConfig.DRIVE_SPEED_MULTIPLIER +
", Turn: " + CompetitionConfig.TURN_SPEED_MULTIPLIER);
initializeHardware();
}Modular Design Principles
Modular design makes your code more maintainable, testable, and reusable. Break your robot functionality into focused classes that handle specific responsibilities.
Modular Robot Design - Drive Controller
// Modular robot design with separate classes for different functions
/**
* Drive controller class - handles all drive train operations
*/
public class DriveController {
private DcMotor leftMotor, rightMotor;
private double maxSpeed = 1.0;
private boolean isEnabled = true;
public DriveController(HardwareMap hardwareMap) {
leftMotor = hardwareMap.get(DcMotor.class, "left_drive");
rightMotor = hardwareMap.get(DcMotor.class, "right_drive");
if (leftMotor != null && rightMotor != null) {
leftMotor.setDirection(DcMotor.Direction.FORWARD);
rightMotor.setDirection(DcMotor.Direction.REVERSE);
leftMotor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
rightMotor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
}
}
public void setMaxSpeed(double maxSpeed) {
this.maxSpeed = Math.max(0.0, Math.min(1.0, maxSpeed));
}
public void drive(double forward, double turn) {
if (!isEnabled) return;
double leftPower = (forward + turn) * maxSpeed;
double rightPower = (forward - turn) * maxSpeed;
// Apply deadband
if (Math.abs(leftPower) < 0.1) leftPower = 0.0;
if (Math.abs(rightPower) < 0.1) rightPower = 0.0;
setPowers(leftPower, rightPower);
}
public void setPowers(double leftPower, double rightPower) {
if (leftMotor != null) leftMotor.setPower(leftPower);
if (rightMotor != null) rightMotor.setPower(rightPower);
}
public void stop() {
setPowers(0.0, 0.0);
}
public void enable() { isEnabled = true; }
public void disable() { isEnabled = false; }
public double getLeftPower() {
return leftMotor != null ? leftMotor.getPower() : 0.0;
}
public double getRightPower() {
return rightMotor != null ? rightMotor.getPower() : 0.0;
}
}Modular Robot Design - Arm Controller
/**
* Arm controller class - handles arm and claw operations
*/
public class ArmController {
private Servo armServo, clawServo;
private double currentArmPosition = 0.5;
private boolean isClawOpen = false;
private double armSpeed = 0.02;
public ArmController(HardwareMap hardwareMap) {
armServo = hardwareMap.get(Servo.class, "arm_servo");
clawServo = hardwareMap.get(Servo.class, "claw_servo");
}
public void setArmSpeed(double speed) {
this.armSpeed = Math.max(0.01, Math.min(0.1, speed));
}
public void moveArmUp() {
currentArmPosition = Math.min(1.0, currentArmPosition + armSpeed);
updateArmPosition();
}
public void moveArmDown() {
currentArmPosition = Math.max(0.0, currentArmPosition - armSpeed);
updateArmPosition();
}
public void setArmPosition(double position) {
currentArmPosition = Math.max(0.0, Math.min(1.0, position));
updateArmPosition();
}
public void openClaw() {
isClawOpen = true;
updateClawPosition();
}
public void closeClaw() {
isClawOpen = false;
updateClawPosition();
}
public void toggleClaw() {
isClawOpen = !isClawOpen;
updateClawPosition();
}
private void updateArmPosition() {
if (armServo != null) {
armServo.setPosition(currentArmPosition);
}
}
private void updateClawPosition() {
if (clawServo != null) {
clawServo.setPosition(isClawOpen ? 1.0 : 0.0);
}
}
public double getArmPosition() { return currentArmPosition; }
public boolean isClawOpen() { return isClawOpen; }
}Modular Robot Design - Sensor Manager
/**
* Sensor manager class - handles all sensor operations
*/
public class SensorManager {
private ColorSensor colorSensor;
private DistanceSensor distanceSensor;
private long lastReadTime = 0;
private static final long READ_INTERVAL = 50; // 50ms between reads
private int[] colorReadings = new int[3];
private double distanceReading = 0.0;
public SensorManager(HardwareMap hardwareMap) {
colorSensor = hardwareMap.get(ColorSensor.class, "color_sensor");
distanceSensor = hardwareMap.get(DistanceSensor.class, "distance_sensor");
}
public void updateReadings() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastReadTime > READ_INTERVAL) {
if (colorSensor != null) {
colorReadings[0] = colorSensor.red();
colorReadings[1] = colorSensor.green();
colorReadings[2] = colorSensor.blue();
}
if (distanceSensor != null) {
distanceReading = distanceSensor.getDistance(DistanceUnit.CM);
}
lastReadTime = currentTime;
}
}
public int getRed() { return colorReadings[0]; }
public int getGreen() { return colorReadings[1]; }
public int getBlue() { return colorReadings[2]; }
public double getDistance() { return distanceReading; }
public boolean isGameElementDetected() {
// Example detection logic
return getRed() > 100 && getGreen() > 100 && getBlue() < 50;
}
public boolean isTooClose() {
return getDistance() < 5.0; // Less than 5cm
}
}Modular Robot Design - Main OpMode
/**
* Main OpMode that uses the modular components
*/
public class ModularOpMode extends OpMode {
private DriveController driveController;
private ArmController armController;
private SensorManager sensorManager;
@Override
public void init() {
// Initialize all controllers
driveController = new DriveController(hardwareMap);
armController = new ArmController(hardwareMap);
sensorManager = new SensorManager(hardwareMap);
// Configure controllers
driveController.setMaxSpeed(0.8);
armController.setArmSpeed(0.02);
telemetry.addData("Status", "Modular OpMode initialized");
}
@Override
public void loop() {
// Update sensor readings
sensorManager.updateReadings();
// Process input
processDriveInput();
processArmInput();
// Apply safety checks
applySafetyChecks();
// Update telemetry
updateTelemetry();
}
private void processDriveInput() {
double forward = -gamepad1.left_stick_y;
double turn = gamepad1.right_stick_x;
driveController.drive(forward, turn);
}
private void processArmInput() {
if (gamepad1.dpad_up) {
armController.moveArmUp();
} else if (gamepad1.dpad_down) {
armController.moveArmDown();
}
if (gamepad1.a) {
armController.toggleClaw();
}
}
private void applySafetyChecks() {
// Stop if too close to something
if (sensorManager.isTooClose()) {
driveController.stop();
telemetry.addData("Safety", "Stopped - too close to obstacle");
}
}
private void updateTelemetry() {
telemetry.addData("=== Drive Status ===", "");
telemetry.addData("Left Power", driveController.getLeftPower());
telemetry.addData("Right Power", driveController.getRightPower());
telemetry.addData("=== Arm Status ===", "");
telemetry.addData("Arm Position", String.format("%.2f", armController.getArmPosition()));
telemetry.addData("Claw Open", armController.isClawOpen());
telemetry.addData("=== Sensor Status ===", "");
telemetry.addData("Color R", sensorManager.getRed());
telemetry.addData("Color G", sensorManager.getGreen());
telemetry.addData("Color B", sensorManager.getBlue());
telemetry.addData("Distance (cm)", String.format("%.1f", sensorManager.getDistance()));
telemetry.addData("Game Element Detected", sensorManager.isGameElementDetected());
}
}Software Development Best Practices Exercise
Refactor an existing OpMode to follow software development best practices. Organize the code, add proper documentation, and implement modular design.
- Take a complex OpMode and break it into focused methods
- Add comprehensive JavaDoc documentation
- Implement consistent naming conventions
- Create modular classes for different robot functions
- Set up Git version control for the project
- Create a development workflow with branching
// Starter code for the refactoring exercise:
public class RefactoringExerciseOpMode extends OpMode {
// TODO: Break this into modular components
// TODO: Add proper documentation
// TODO: Implement consistent naming
// TODO: Create separate classes for different functions
@Override
public void init() {
// TODO: Initialize modular components
// TODO: Add proper error handling
// TODO: Add initialization documentation
}
@Override
public void loop() {
// TODO: Organize into focused methods
// TODO: Add proper state management
// TODO: Implement safety checks
// TODO: Add comprehensive telemetry
}
}