Unit 08 · lesson

Review the Contract Before You Connect the Systems

A good component boundary makes responsibilities visible before the code grows around them.

This week introduced three different kinds of boundary:

  • a Java interface that defines behavior callers may use;
  • an implementation that decides how that behavior works and how invalid requests are rejected;
  • a ROS 2 interface that defines communication data for a ROS runtime.

The names can look related. The runtime responsibilities are not interchangeable.

Before the lab, you will review one design and decide whether its boundaries are actually clear.

Start with a leaky design

Consider this interface:

interface RangeSource {
  double getRangeMeters();
  void updateRange(double value);
  void setRosTopic(String topic);
  void reconnectHardware();
  void resetEverything();
}

The interface technically compiles. Its design is weak.

Why?

The name RangeSource suggests one focused capability: provide range information. But the interface also exposes update behavior, ROS configuration, hardware recovery, and an undefined reset operation.

Different callers may need very different responsibilities, yet every implementation is forced to pretend it supports all of them.

A smaller contract is easier to reason about:

interface RangeSource {
  String getId();
  double getRangeMeters();
}

Now a consumer such as ObstacleGuard depends only on what it needs.

Separate read capability from update responsibility

A simulator or sensor wrapper may need an update method internally:

class ValidatedRangeSensor implements RangeSource {
  private final String id;
  private final double minRangeMeters;
  private final double maxRangeMeters;
  private double rangeMeters;

  ValidatedRangeSensor(
      String id,
      double minRangeMeters,
      double maxRangeMeters,
      double initialRangeMeters
  ) {
    this.id = id;
    this.minRangeMeters = minRangeMeters;
    this.maxRangeMeters = maxRangeMeters;
    updateRange(initialRangeMeters);
  }

  void updateRange(double value) {
    if (value < minRangeMeters
        || value > maxRangeMeters) {
      throw new IllegalArgumentException(
          "Sensor " + id
          + " rejected " + value + " m"
      );
    }

    rangeMeters = value;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public double getRangeMeters() {
    return rangeMeters;
  }
}

The class can have behavior that is not part of the consumer-facing RangeSource interface.

That is not a contradiction. An implementation can provide more behavior than the interface requires.

The interface simply says what code using a RangeSource is allowed to assume.

Decide where an exception belongs

Suppose a caller does this:

sensor.updateRange(3.70);

for a sensor whose valid interval ends at 2.00 m.

The implementation throws an IllegalArgumentException.

Now another design decision appears: who should catch it?

If the code attempting the update has enough context to record the component and decide whether processing should continue, catching there may be appropriate:

try {
  sensor.updateRange(3.70);
} catch (IllegalArgumentException error) {
  System.out.println(
      "update rejected id=" + sensor.getId()
      + " reason=" + error.getMessage()
  );
}

But the catch block should not pretend the cause is known.

It knows the request violated the component rule. It may not know whether the original bad value came from a test, a conversion bug, configuration, or hardware.

The caller should not quietly invent a replacement

This is a risky recovery:

try {
  sensor.updateRange(3.70);
} catch (IllegalArgumentException error) {
  sensor.updateRange(0.0);
}

The code silently replaces an invalid value with 0.0 meters.

That may create a completely different meaning. A zero range might represent an obstacle directly against the sensor, a special invalid condition, or something else depending on the system.

Recovery should come from an explicit policy, not from a convenient number.

A safer teaching example is to preserve the last valid state and record the rejected request.

Review the ROS boundary separately

Now imagine the same design will eventually provide data to a ROS 2 system using sensor_msgs/msg/Range.

Do not add ROS fields to the Java interface merely because a future bridge might need them.

Instead, ask what the translation layer needs.

Your current Java contract supplies:

id
rangeMeters

The ROS message includes additional fields such as:

header
radiation_type
field_of_view
min_range
max_range
range

The integration boundary therefore has missing information.

That is useful knowledge. It tells the architect where the design is incomplete.

Hiding the gap by inventing default values would make the integration look more finished than it is.

A boundary review asks different questions at each layer

For the Java interface, ask:

  • Is the contract focused?
  • Do callers depend only on behavior they need?
  • Can multiple implementations satisfy it?

For the implementation, ask:

  • Is internal state protected?
  • Are invalid requests rejected before state changes?
  • Are exceptions specific and useful?

For a potential ROS integration, ask:

  • Which ROS interface is appropriate?
  • Which fields can be populated from existing information?
  • Which fields require additional configuration or runtime data?
  • What component will own the translation?

Those questions belong together in an architecture review, but they should not be collapsed into one class.

Worked review: find the boundary problems

Consider this class:

class FrontRangeSource implements RangeSource {
  public double rangeMeters;

  @Override
  public String getId() {
    return "front";
  }

  @Override
  public double getRangeMeters() {
    return rangeMeters;
  }

  public void publishRosRange() {
    System.out.println("published ROS range");
  }
}

Several problems are visible.

First, rangeMeters is public. Any caller can bypass validation.

Second, the class claims a publishRosRange() operation, but the body only prints text. That is not ROS publication.

Third, the ID front may be too vague if a larger system needs a stable component identity such as range-front.

Fourth, there is no declared interval or invalid-input policy.

A stronger repair would remove the fake ROS method, encapsulate the state, validate updates, and leave real ROS integration to an explicit supported runtime boundary.

Build your Component Boundary Review

Your lab artifact will need six parts.

Java contract

Show the RangeSource interface and explain what a caller may rely on.

Implementations

Show at least two different classes that implement the contract and explain how their internal behavior differs.

Invalid request

Preserve one rejected input and the exact exception context.

State after failure

Show what valid state remains after the rejected update.

ROS interface inspection

Record the fields shown by:

ros2 interface show sensor_msgs/msg/Range

and identify which fields your Java contract can and cannot currently supply.

Boundary conclusion

State what the Java design proves and what would still have to be implemented before claiming ROS 2 communication.

Pre-lab design challenge

A teammate proposes this sentence:

RangeSource is our ROS interface, so any class that implements it can publish sensor_msgs/msg/Range.

Rewrite the sentence so it is technically correct.

Your answer should distinguish:

  • a Java interface;
  • a class implementing that Java interface;
  • a ROS 2 message interface;
  • the missing integration layer that would translate and publish data.

If you can explain those four pieces without merging them together, you are ready for the lab.