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
    }
}

Additional Resources

Open full interactive app