Unit 08 · lesson
One Interface, Different Implementations
An interface becomes valuable when more than one class can satisfy the same contract.
In the previous lesson, RangeSource promised two methods:
interface RangeSource {
String getId();
double getRangeMeters();
}
Now you will create two different classes that implement that contract.
The classes will not store or produce their values in the same way. Code using the interface will still be able to work with both.
Implementation 1: a mutable sensor model
class RangeSensor implements RangeSource {
private final String id;
private double rangeMeters;
RangeSensor(String id, double initialRangeMeters) {
this.id = id;
this.rangeMeters = initialRangeMeters;
}
@Override
public String getId() {
return id;
}
@Override
public double getRangeMeters() {
return rangeMeters;
}
public void updateRange(double newRangeMeters) {
rangeMeters = newRangeMeters;
}
}
This class stores a reading that can change.
The interface says nothing about updateRange(...), so callers that only know the object as a RangeSource do not depend on that method.
Implementation 2: a fixed source
A test or demonstration may need a source that always returns the same value.
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;
}
}
There is no update method. The value is fixed at construction.
Both classes still satisfy the same two-method contract.
Use either implementation through the interface
RangeSource liveModel =
new RangeSensor("range-front", 0.42);
RangeSource fixedModel =
new FixedRangeSource("test-source", 0.25);
Both variables have type RangeSource.
So the same method can consume either object:
static void printSource(RangeSource source) {
System.out.println(
source.getId()
+ " range=" + source.getRangeMeters()
+ " m"
);
}
Calls:
printSource(liveModel);
printSource(fixedModel);
might produce:
range-front range=0.42 m
test-source range=0.25 m
The caller did not need an if statement asking which concrete class it received.
It used the common contract.
This is polymorphism
Polymorphism means code can use objects of different concrete types through a shared type or contract.
In this example:
RangeSensoris one concrete class;FixedRangeSourceis another concrete class;RangeSourceis the common interface.
When Java executes:
source.getRangeMeters()
it runs the implementation belonging to the actual object referenced by source.
The caller uses the interface. The object supplies its own implementation.
Put different implementations in one list
Week 7 taught you collections. Interfaces let you make those collections more flexible.
List<RangeSource> sources = new ArrayList<>();
sources.add(
new RangeSensor("range-front", 0.42)
);
sources.add(
new FixedRangeSource("test-near", 0.20)
);
sources.add(
new FixedRangeSource("test-far", 1.50)
);
The list type is:
List<RangeSource>
That means every element must satisfy the RangeSource contract. The elements do not all need the same concrete class.
Now one loop works for all of them:
for (RangeSource source : sources) {
System.out.println(
source.getId()
+ "=" + source.getRangeMeters()
);
}
This is a strong connection between collections and interfaces.
Collections organize many objects. Interfaces let those objects vary in implementation while still providing behavior the caller expects.
Use the interface to test decision logic
Consider this class from the previous lesson:
class ObstacleGuard {
private final double cautionDistanceMeters;
ObstacleGuard(double cautionDistanceMeters) {
this.cautionDistanceMeters = cautionDistanceMeters;
}
boolean shouldStop(RangeSource source) {
return source.getRangeMeters()
< cautionDistanceMeters;
}
}
Create:
ObstacleGuard guard = new ObstacleGuard(0.30);
Then use fixed sources:
RangeSource safe =
new FixedRangeSource("safe", 0.50);
RangeSource close =
new FixedRangeSource("close", 0.20);
Results:
guard.shouldStop(safe); // false
guard.shouldStop(close); // true
The fixed implementation makes the decision rule easy to exercise with known values.
Later, unit tests will formalize this idea with assertions.
Why this design is useful in robotics
Robot software often has code that should depend on capability, not on one hardware vendor or one simulation class.
For example, a navigation rule might need a heading. It may not need to know whether that heading came from:
- one brand of gyro;
- another sensor implementation;
- a simulator;
- recorded test data.
An interface can isolate that dependency.
This does not mean every robot class needs an interface. Extra abstraction can make small programs harder to read.
Use an interface when multiple implementations or a clear contract genuinely improve the design.
Common mistake: casting back to the concrete class
Suppose the list type is:
List<RangeSource> sources
A student may write:
for (RangeSource source : sources) {
RangeSensor sensor = (RangeSensor) source;
sensor.updateRange(0.40);
}
This assumes every RangeSource is actually a RangeSensor.
But the list may also contain FixedRangeSource objects.
The cast can fail at runtime with a ClassCastException.
More importantly, it defeats the purpose of the interface. The caller claimed it only needed RangeSource, then reached behind the contract to require one concrete implementation.
If the algorithm truly needs updateRange(...), reconsider the interface or where that update responsibility belongs.
Common mistake: using an interface as a label only
An interface is not useful just because its name sounds architectural.
This is weak:
interface RobotThing {
}
if it does not define any behavior the program needs.
A useful contract says something concrete about what callers may rely on.
RangeSource is useful because it promises actual operations.
Worked example: evaluate every source
List<RangeSource> sources = new ArrayList<>();
sources.add(new RangeSensor("front", 0.42));
sources.add(new FixedRangeSource("test-near", 0.18));
sources.add(new FixedRangeSource("test-far", 1.20));
ObstacleGuard guard = new ObstacleGuard(0.30);
for (RangeSource source : sources) {
boolean stop = guard.shouldStop(source);
System.out.println(
source.getId()
+ " range=" + source.getRangeMeters()
+ " stop=" + stop
);
}
Expected output:
front range=0.42 stop=false
test-near range=0.18 stop=true
test-far range=1.2 stop=false
The loop is identical for every implementation. Only the objects' state and implementation differ.
Practice: create another implementation
Create a class named:
SequenceRangeSource
For this practice, it can hold one current value and provide the same RangeSource methods. Add a method that advances the current reading through a small array of values.
Then:
- create one
RangeSensor; - create one
FixedRangeSource; - create one
SequenceRangeSource; - store all three in
List<RangeSource>; - loop through the list and print each ID and range.
Explain why the loop does not need to know the concrete class of each element.
In the next lesson, you will deal with a harder question: what should an implementation do when a caller asks it to accept a value that violates its rules?