Unit 08 · lab
Lab: Build a Swappable Range Source and Inspect a ROS Interface
This lab combines the week's Java interface, polymorphism, exception, and ROS interface concepts into one boundary review.
You will build two different Java implementations of the same RangeSource contract, use both through one list, trigger and preserve an invalid update, then inspect sensor_msgs/msg/Range in the controlled ROS 2 terminal.
Your final artifact is a Component Boundary Review. It should make clear what belongs to Java, what belongs to ROS 2, and what integration work is still missing.
Guided example: one caller, two implementations
Start with the interface:
interface RangeSource {
String getId();
double getRangeMeters();
}
Two classes can satisfy it in different ways:
class FixedRangeSource implements RangeSource {
private final String id;
private final double rangeMeters;
FixedRangeSource(String id, double rangeMeters) {
this.id = id;
this.rangeMeters = rangeMeters;
}
@Override
public String getId() {
return id;
}
@Override
public double getRangeMeters() {
return rangeMeters;
}
}
and:
class MutableRangeSource implements RangeSource {
private final String id;
private double rangeMeters;
MutableRangeSource(String id, double rangeMeters) {
this.id = id;
this.rangeMeters = rangeMeters;
}
@Override
public String getId() {
return id;
}
@Override
public double getRangeMeters() {
return rangeMeters;
}
}
A caller can use either object through RangeSource:
static void printRange(RangeSource source) {
System.out.println(
source.getId()
+ " range=" + source.getRangeMeters()
+ " m"
);
}
That is the important result. The caller depends on the contract, not on one concrete implementation.
You will now build a stronger version with validation and use it beside a fixed source.
Part 1: Define the Java contract
Use:
interface RangeSource {
String getId();
double getRangeMeters();
}
In your artifact, explain each method in one sentence.
Do not add ROS methods, update methods, or hardware-control methods to this interface. Keep it focused on what a consumer of range data needs.
Part 2: Implement a fixed source
Create:
class FixedRangeSource implements RangeSource {
private final String id;
private final double rangeMeters;
FixedRangeSource(String id, double rangeMeters) {
this.id = id;
this.rangeMeters = rangeMeters;
}
@Override
public String getId() {
return id;
}
@Override
public double getRangeMeters() {
return rangeMeters;
}
}
Create this object:
RangeSource testNear =
new FixedRangeSource("test-near", 0.20);
Record what the interface guarantees about this reference and what it does not guarantee.
Part 3: Implement a validated source
Create a class named:
ValidatedRangeSensor
It must implement RangeSource and store:
private final String id;
private final double minRangeMeters;
private final double maxRangeMeters;
private double rangeMeters;
Its constructor must reject:
- a blank ID;
- a negative minimum range;
- a maximum less than or equal to the minimum;
- an initial reading outside the declared interval.
Add:
void updateRange(double newRangeMeters)
The method must reject an out-of-range value before assigning it to rangeMeters.
Use an exception message that includes:
- component ID;
- rejected value;
- unit;
- declared interval.
Part 4: Use both implementations through one list
Create:
List<RangeSource> sources = new ArrayList<>();
Add:
ValidatedRangeSensor frontSensor =
new ValidatedRangeSensor(
"range-front",
0.10,
2.00,
0.42
);
RangeSource fixedNear =
new FixedRangeSource("test-near", 0.20);
Then:
sources.add(frontSensor);
sources.add(fixedNear);
Loop through the collection:
for (RangeSource source : sources) {
System.out.println(
source.getId()
+ " range=" + source.getRangeMeters()
+ " m"
);
}
Explain why the loop does not need an if statement to determine which concrete class each element came from.
Part 5: Apply one decision rule to both implementations
Create:
class ObstacleGuard {
private final double cautionDistanceMeters;
ObstacleGuard(double cautionDistanceMeters) {
this.cautionDistanceMeters =
cautionDistanceMeters;
}
boolean shouldStop(RangeSource source) {
return source.getRangeMeters()
< cautionDistanceMeters;
}
}
Use:
ObstacleGuard guard =
new ObstacleGuard(0.30);
Predict the result for both objects before writing it into your record.
| Source | Reading | Expected shouldStop |
|---|---|---|
range-front | 0.42 m | false |
test-near | 0.20 m | true |
Then explain why ObstacleGuard can remain unaware of the concrete implementation.
Part 6: Trigger an invalid update on purpose
Attempt:
frontSensor.updateRange(3.70);
Wrap the call in a specific catch block:
try {
frontSensor.updateRange(3.70);
} catch (IllegalArgumentException error) {
System.out.println(
"rejected id=" + frontSensor.getId()
+ " reason=" + error.getMessage()
);
}
Preserve three pieces of evidence:
- the rejected request,
3.70 m; - the exception message;
- the range value stored after the failure.
The post-failure reading should still be 0.42 m if validation happens before assignment.
If your reasoning produces 3.70 m as the stored state, locate the design error before continuing.
Part 7: Inspect a ROS 2 message interface
The terminal below is deterministic. It does not execute your Java classes or create a Java-to-ROS connection.
Build a bounded sensor-evidence record
Inspect a declared interface and one simulated message, then keep the result separate from a movement claim.
This is a controlled command simulator. It does not execute Java, ROS 2, shell commands, or network requests on your device.
source /opt/ros/jazzy/setup.bashros2 interface show sensor_msgs/msg/Rangeros2 topic echo /range --oncejavac SensorEvidence.javajava SensorEvidence
Run:
source /opt/ros/jazzy/setup.bash
ros2 interface show sensor_msgs/msg/Range
Record the fields shown by the terminal.
Then run:
ros2 topic echo /range --once
Record the controlled message values.
Your artifact must keep these observations labeled as ROS 2 interface/runtime simulation evidence, not Java output.
Part 8: Build the translation-gap table
Create a table with these rows:
Java getId()
Java getRangeMeters()
ROS header.frame_id
ROS min_range
ROS max_range
ROS field_of_view
ROS range
For each row, classify the information as one of:
- available directly from the current Java contract;
- available only from the concrete Java implementation;
- missing from the current Java design;
- supplied by the ROS-side runtime/configuration in a future integration.
Do not invent missing values to make every row complete.
The goal is to discover the boundary, not hide it.
Part 9: Diagnose a false integration claim
A teammate writes:
ValidatedRangeSensor implements RangeSource, so it already implements the ROSsensor_msgs/msg/Rangeinterface.
Repair this statement in your own words.
Your corrected explanation must say that:
RangeSourceis a Java interface;ValidatedRangeSensorimplements that Java contract;sensor_msgs/msg/Rangeis a ROS 2 message interface;- no ROS publisher or translation layer has been demonstrated by the Java class alone.
Part 10: Produce the Component Boundary Review
Your final artifact must contain:
Java interface
Show RangeSource and explain the behavior callers can rely on.
Two implementations
Show the fixed source and validated source. Explain one important difference in their internal behavior.
Polymorphic use
Include the List<RangeSource> and the ObstacleGuard result for each object.
Invalid request evidence
Preserve the 3.70 m failure, exception context, and last valid state.
ROS 2 interface inspection
Record the fields from sensor_msgs/msg/Range and the one controlled /range message.
Translation gap
Include your Java-to-ROS field table and identify at least two pieces of information your Java interface does not currently supply.
Boundary conclusion
Write the strongest conclusion your work supports. Do not claim that the Java class publishes ROS data or that any physical sensor was tested.
Success criteria
Your lab is complete when another student can answer all of these from your record:
- What does the Java interface require?
- How can two different classes satisfy the same contract?
- Why can one list contain both implementations?
- Which invalid request was rejected?
- What state remained after the failure?
- What fields belong to the ROS 2 message interface?
- Which ROS fields are missing from the Java contract?
- What implementation would still be required before claiming Java-to-ROS communication?
The finished artifact should make the boundary easier to see, not easier to ignore.