Skip to content

afehid/java-mars-rover

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mars Rover Simulation

A Java Mars Rover kata that started as a single procedural main method and was refactored into a clean, object-oriented design using several design patterns and SOLID principles — with clean architecture–inspired layering (domain model + commands, thin UI/demo).

Commands: F (forward), B (backward), L (turn left), R (turn right).
The planet is a wrap-around grid (torus). Hitting an obstacle aborts the rest of the command sequence.

Before vs After


Table of Contents

  1. Quick Start
  2. Project Structure
  3. Old Code vs New Architecture
  4. Refactoring Journey (Step by Step)
  5. Design Patterns Explained
  6. How to Run
  7. SOLID in this project

Quick Start

# Compile
compile.bat
# or:
javac -encoding UTF-8 -d out $(find . -name "*.java")   # Git Bash / Linux / macOS

# Run tests (no libraries — manual assertions)
java -cp out com.itbulls.learnit.javacore.Task.test.MarsRoverTest

# Run the refactored interactive demo
java -cp out com.itbulls.learnit.javacore.Task.LegacyMarsRoverDemo

# Run the original monolithic version (for comparison)
java -cp out com.itbulls.learnit.javacore.Task.LegacyMarsRoverOld

Requires JDK 17+ (tested with JDK 26). No Maven/Gradle or third-party libraries.


Project Structure

com/itbulls/learnit/javacore/Task/
├── LegacyMarsRoverOld.java          # BEFORE: unre factored, everything in main
├── LegacyMarsRoverDemo.java         # AFTER: thin UI that uses the new domain
├── domain/
│   ├── model/
│   │   ├── MarsRover.java          # Rover orchestrator
│   │   ├── Position.java            # (x, y, direction)
│   │   ├── Direction.java           # Enum + turn deltas (State-like)
│   │   └── Grid.java                # Wrapping + obstacles
│   └── commands/
│       ├── MarsRoverCommand.java    # Command interface
│       ├── BaseMoveCommand.java     # Template Method for F/B
│       ├── MoveForwardCommand.java
│       ├── MoveBackwardCommand.java
│       ├── TurnLeftCommand.java
│       ├── TurnRightCommand.java
│       └── CommandFactory.java      # Factory + Flyweight
└── test/
    └── MarsRoverTest.java           # Manual unit tests (Arrange-Act-Assert)

Old Code vs New Architecture

The old way (LegacyMarsRoverOld)

Everything lived in one main method:

Concern How it was done
Position Local int x, int y, String direction
Turning Nested if / else if chains for N→E→S→W
Moving Duplicated forward/backward blocks (~30 lines each)
Grid wrap Copied if checks in both F and B
Obstacles Same loop copied in both F and B
Commands Giant if (cmd == 'R') ... else if (cmd == 'L') ...
Testing Almost impossible without running the whole program

Problems: duplicated logic, hard to extend (new command = more ifs), fragile strings for direction, no separation of responsibilities.

The new way (domain model + commands)

Concern How it is done now
Position Position object with Direction enum
Turning direction.turnLeft() / turnRight() — no if sprawl
Moving Shared algorithm in BaseMoveCommand; F/B only pass +1 / -1
Grid wrap Grid.wrapX() / wrapY()
Obstacles Grid.hasObstacle(x, y)
Commands Objects from CommandFactory; rover just calls command.execute(this)
Testing Focused tests in MarsRoverTest without user input

Benefits: single responsibility per class, easy to add a new command, reusable movement rules, testable without Scanner.

Side-by-side mental model

OLD                                      NEW
─────────────────────────────────        ─────────────────────────────────
main() does everything                   Demo: read input → list of commands
  ├─ turn with if-chains                   └─ MarsRover.execute(list)
  ├─ move F (copy-pasted)                      ├─ Command objects
  ├─ move B (copy-pasted)                      ├─ Direction knows how to turn
  └─ wrap + obstacles inline                   └─ Grid knows wrap + obstacles

Refactoring Journey (Step by Step)

Each step keeps behavior the same while improving structure. You can still run LegacyMarsRoverOld to compare outcomes.

Step 1 — Extract domain primitives: Position, Direction, Grid

Why: Primitive values and strings were scattered in main.

  • Position groups x, y, and facing direction.
  • Direction becomes an enum with deltaX / deltaY for movement and turnLeft() / turnRight() using modulo arithmetic.
  • Grid owns width/height, wrapping (torus), and obstacle detection.

Before:

String direction = "N";
if (direction.equals("N")) direction = "E";
else if (direction.equals("E")) direction = "S";
// ...

After:

position.direction = position.direction.turnRight();

Step 2 — Introduce MarsRover as the orchestrator

Why: main should not own business logic.

MarsRover holds a Position and a Grid, and exposes execute(...). It stops the sequence when a command returns false (obstacle hit) — same abort behavior as the old break.

Step 3 — Apply the Command pattern

Why: The long if / else if on each character mixed parsing with execution.

  • MarsRoverCommand interface: boolean execute(MarsRover rover).
  • Concrete commands: MoveForwardCommand, MoveBackwardCommand, TurnLeftCommand, TurnRightCommand.
  • Each command encapsulates one action; the rover iterates and executes them.

Step 4 — Apply the Template Method pattern (BaseMoveCommand)

Why: Forward and backward shared the same pipeline (compute next cell → wrap → obstacle check → update), only the step direction differed.

BaseMoveCommand.execute():
  next = current + (direction.delta * stepSign)
  wrap on grid
  if obstacle → fail
  else update position → success
  • MoveForwardCommandsuper(1, "Moved Forward")
  • MoveBackwardCommandsuper(-1, "Moved Backward")

One place to fix bugs or change wrapping/obstacle rules.

Step 5 — Treat Direction as a lightweight State model

Why: Moving depended on facing direction with many branches.

Each direction stores its movement vector. Commands use pos.direction.deltaX / deltaY instead of branching on "N", "E", etc. Turning returns the next enum value — behavior changes with state without giant conditionals.

Step 6 — Apply Factory (+ Flyweight) in CommandFactory

Why: Mapping chars like 'F' to command objects should live in one place.

CommandFactory.createCommand('F') → MoveForwardCommand
CommandFactory.createCommand('L') → TurnLeftCommand
// unknown → null (demo skips; invalid input is visible)

Reusable singleton instances of each command act as Flyweight: commands are stateless, so one shared instance per type is enough.

Step 7 — Thin UI / demo and dedicated tests

  • LegacyMarsRoverDemo only builds the model, reads commands, builds a List<MarsRoverCommand>, and prints the final state.
  • MarsRoverTest uses Arrange–Act–Assert for move, turn, wrap, and obstacle abort — without JUnit, to keep the project dependency-free.

Design Patterns Explained

1. Command Pattern

Intent: Represent an action as an object so you can queue, log, or undo it (here: execute a sequence and abort on failure).

Role In this project
Command MarsRoverCommand
Concrete commands MoveForwardCommand, MoveBackwardCommand, TurnLeftCommand, TurnRightCommand
Invoker / receiver MarsRover.execute(List<...>)
for (MarsRoverCommand command : parsedCommands) {
    if (!command.execute(this)) break;  // obstacle → stop remaining commands
}

2. Template Method Pattern

Intent: Define the skeleton of an algorithm in a base class; subclasses supply only the varying parts.

BaseMoveCommand owns the full move algorithm. Subclasses only choose stepSign (+1 / -1) and the log label. That removed duplicated F/B code from the legacy file.

3. State Pattern (via Direction enum)

Intent: Allow an object to change behavior when its internal state changes.

Facing direction changes how F/B compute the next cell. Instead of if (direction.equals("N"))..., state carries its own deltas and turn transitions — same idea as State, implemented with a small enum.

4. Factory Pattern

Intent: Centralize creation so callers do not know concrete classes.

CommandFactory.createCommand(char) maps input characters to command objects. Adding a new command means extending the factory switch and adding one class — not growing a mega-if in main.

5. Flyweight Pattern

Intent: Share fine-grained objects when they are interchangeable and have no mutable per-use state.

Command instances are immutable (no rover-specific fields). CommandFactory reuses four static instances instead of allocating a new object for every character in a long command string.


How to Run

Windows

compile.bat
java -cp out com.itbulls.learnit.javacore.Task.test.MarsRoverTest
java -cp out com.itbulls.learnit.javacore.Task.LegacyMarsRoverDemo

Manual compile (any OS)

Compile all sources under the project root into out/, then run the fully qualified class names above.

Example session (demo)

Enter commands (f = forward, b = backward, l = left, r = right): FFRFF

You should see each move/turn logged, then a final position like x:y:DIRECTION.

What the tests cover

Test Expectation
should_move_forward_north (5,5,N) + F5:6:N
should_turn_left_north (0,0,N) + L0:0:W
should_turn_right_north (0,0,N) + R0:0:E
should_wrap_from_top_to_bottom (0,9,N) + F0:0:N
should_stop_and_abort_sequence_when_encounter_obstacle Obstacle at (0,1)FRF aborts after failed F, stays 0:0:N

SOLID in this project

Principle How it shows up
Single Responsibility Grid wraps/obstacles; Direction turns; commands perform actions; demo only reads input
Open/Closed Add a new command class + one factory case; rover loop stays unchanged
Liskov Substitution Any MarsRoverCommand can replace another in the execute loop
Interface Segregation Small MarsRoverCommand with a single execute method
Dependency Inversion MarsRover depends on the command abstraction, not concrete F/B/L/R classes

This is not a full multi-layer clean architecture (no use-case / infrastructure rings). It is a kata with domain-first structure and a thin entrypoint — accurate to call clean architecture–inspired.


Summary

Topic Takeaway
Starting point One procedural class with duplicated conditionals
End state Separated model (Grid, Direction, Position, MarsRover) + command objects
Patterns Command, Template Method, State (Direction), Factory, Flyweight
Principles SOLID + clean architecture–inspired domain / UI split
Goal of the refactor Same rover rules, clearer design, easier tests and extensions

Keep LegacyMarsRoverOld in the repo on purpose: it is the baseline that shows why the patterns were introduced.

About

Refactoring a procedural Mars Rover into clean OOP with classic design patterns.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors