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.
- Quick Start
- Project Structure
- Old Code vs New Architecture
- Refactoring Journey (Step by Step)
- Design Patterns Explained
- How to Run
- SOLID in this project
# 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.LegacyMarsRoverOldRequires JDK 17+ (tested with JDK 26). No Maven/Gradle or third-party libraries.
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)
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.
| 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.
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
Each step keeps behavior the same while improving structure. You can still run LegacyMarsRoverOld to compare outcomes.
Why: Primitive values and strings were scattered in main.
Positiongroupsx,y, and facing direction.Directionbecomes an enum withdeltaX/deltaYfor movement andturnLeft()/turnRight()using modulo arithmetic.Gridowns 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();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.
Why: The long if / else if on each character mixed parsing with execution.
MarsRoverCommandinterface:boolean execute(MarsRover rover).- Concrete commands:
MoveForwardCommand,MoveBackwardCommand,TurnLeftCommand,TurnRightCommand. - Each command encapsulates one action; the rover iterates and executes them.
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
MoveForwardCommand→super(1, "Moved Forward")MoveBackwardCommand→super(-1, "Moved Backward")
One place to fix bugs or change wrapping/obstacle rules.
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.
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.
LegacyMarsRoverDemoonly builds the model, reads commands, builds aList<MarsRoverCommand>, and prints the final state.MarsRoverTestuses Arrange–Act–Assert for move, turn, wrap, and obstacle abort — without JUnit, to keep the project dependency-free.
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
}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.
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.
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.
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.
compile.bat
java -cp out com.itbulls.learnit.javacore.Task.test.MarsRoverTest
java -cp out com.itbulls.learnit.javacore.Task.LegacyMarsRoverDemoCompile all sources under the project root into out/, then run the fully qualified class names above.
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.
| Test | Expectation |
|---|---|
should_move_forward_north |
(5,5,N) + F → 5:6:N |
should_turn_left_north |
(0,0,N) + L → 0:0:W |
should_turn_right_north |
(0,0,N) + R → 0:0:E |
should_wrap_from_top_to_bottom |
(0,9,N) + F → 0:0:N |
should_stop_and_abort_sequence_when_encounter_obstacle |
Obstacle at (0,1) → FRF aborts after failed F, stays 0:0:N |
| 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.
| 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.
