Structural Patterns «Prev Next»

Lesson 9Course Project, Part 5
ObjectiveModel the vehicles queued at an intersection without mixing shared descriptions and live traffic state.

Class Vehicle Intersection

The course project now needs an intersection model that records vehicles waiting in each approach. This is where the preceding Flyweight lessons become concrete. A shared VehicleType can describe stable properties, while each queued VehicleEvent records occurrence-specific state. The intersection owns queues and traffic-control decisions; the flyweight factory owns canonical descriptions. Keeping those responsibilities separate prevents the optimization from controlling the simulation.

Model Directions and Occurrences Explicitly

Strings such as "north" and "car" are easy to mistype and make invalid states possible. Java enums provide a closed direction set, and records provide a compact immutable event value. The event refers to a shared vehicle type but retains its own identifier, arrival time, lane, and direction.

enum Direction { NORTH, SOUTH, EAST, WEST }

record VehicleEvent(
    long id,
    VehicleType type,
    Direction direction,
    int lane,
    long arrivalTick
) {
    VehicleEvent {
        java.util.Objects.requireNonNull(type);
        java.util.Objects.requireNonNull(direction);
        if (lane < 0) throw new IllegalArgumentException("lane");
    }
}

The record does not store current queue position because position follows from the queue. It also does not mutate VehicleType. Two events can share the same type and remain distinct through their event IDs.

Own One Queue per Direction

final class VehicleIntersection {
    private final java.util.EnumMap<Direction,
        java.util.ArrayDeque<VehicleEvent>> queues =
        new java.util.EnumMap<>(Direction.class);

    VehicleIntersection() {
        for (Direction direction : Direction.values()) {
            queues.put(direction, new java.util.ArrayDeque<>());
        }
    }

    void arrive(VehicleEvent event) {
        queues.get(event.direction()).addLast(event);
    }

    int waiting(Direction direction) {
        return queues.get(direction).size();
    }

    java.util.Optional<VehicleEvent> release(Direction direction) {
        return java.util.Optional.ofNullable(
            queues.get(direction).pollFirst());
    }
}

EnumMap documents the closed key set and avoids four nearly identical fields. ArrayDeque provides queue operations without the node allocation of LinkedList. Returning Optional makes an empty approach explicit. Production code may release several vehicles during a green phase rather than clearing an entire queue instantaneously.

Separate Scheduling Policy

Choosing the largest queue is a useful baseline, but it can starve a quieter direction when traffic continuously arrives elsewhere. The structural model should expose queue observations to a separate controller policy. That policy can consider waiting time, pedestrian phases, emergency priority, minimum green duration, yellow clearance, and fairness. A deterministic policy is easy to test with a fixed event sequence.

This separation resembles Bridge: the intersection abstraction can work with different control policies. A simple strategy may be a lambda or small class, so a full pattern hierarchy is unnecessary until multiple policies and independent evolution justify it. The key is that queue storage does not hide global scheduling decisions.

Concurrency and Simulation Time

A deterministic simulation should advance from an explicit clock rather than wall-clock time. Process arrivals, signal changes, and departures in a defined order for each tick. If several threads produce events, collect them through a controlled boundary and apply them in sequence, or protect queues with a documented synchronization policy. An immutable flyweight does not make the mutable intersection queues thread safe.

Tests That Establish Correctness

  1. Events are released first-in, first-out within one lane or approach.
  2. One direction's release does not modify another direction's queue.
  3. Equal vehicle-type keys reuse one immutable flyweight.
  4. Different events sharing that flyweight retain independent state.
  5. The controller eventually serves a low-volume direction under sustained load.
  6. Repeated runs with the same seed and inputs produce identical results.

These tests distinguish structural correctness from a visually plausible animation. They give the following exercise a concrete contract to implement and evaluate.

Operational Signals for the Model

Collect queue length, oldest waiting time, departures per phase, and controller decisions for each direction. Flyweight metrics should separately report catalog entries, hits, and misses. Keeping these signals separate makes it possible to distinguish a scheduling defect from an object-sharing defect. Logs should reference event and type identifiers rather than dumping mutable object graphs.

Set explicit invariants: no event may appear in two queues, a released event cannot be released again, queue counts cannot be negative, and every accepted direction has a queue. Validate those invariants during development and in property-based tests with generated arrival sequences. Observability then supports both algorithm evaluation and structural maintenance. It also supplies evidence for comparing controller strategies under the same repeatable traffic workload. Preserve the event stream and configuration with the test result so performance and fairness comparisons remain reproducible after implementation changes.

Separating Shared Vehicle Description from Live State

The intersection simulation provides a concrete place to apply structural reasoning. A vehicle type has descriptive values that many simulated vehicles can share, such as length, maximum speed, icon, and category. Each occurrence also has live state that cannot be shared, including lane, distance from the stop line, current speed, arrival time, and turning intention.

A VehicleSpec flyweight can hold the immutable description. A VehicleState or context holds the changing values and references one specification. This split prevents hundreds or thousands of identical descriptions from being copied while preserving independent movement. It also makes the model easier to test because specification rules and motion rules have separate responsibilities.

record VehicleSpec(
    String type,
    double lengthMeters,
    double maximumSpeedKph
) {}

final class VehicleState {
    private final VehicleSpec specification;
    private int lane;
    private double positionMeters;
    private double speedKph;

    VehicleState(VehicleSpec specification, int lane) {
        this.specification = specification;
        this.lane = lane;
    }

    void advance(double seconds) {
        double allowed = Math.min(
            speedKph, specification.maximumSpeedKph());
        positionMeters += (allowed / 3.6) * seconds;
    }
}

Factory Integration Without Hidden Global State

The prior module introduced a vehicle factory. A Flyweight version can obtain canonical specifications from a repository and create a new context for each arrival. The repository should be injected into the factory rather than accessed through a hidden global singleton. Injection exposes the dependency, supports deterministic tests, and allows a scenario to use a fixed catalog.

The factory's contract should distinguish two operations: retrieving a shared specification and creating a new simulated occurrence. Returning the same mutable VehicleState twice would be a defect. Returning the same immutable VehicleSpec twice is the intended optimization. Tests should assert both behaviors.

Intersection Coordination and Structural Boundaries

An intersection model has several other structural boundaries. An Adapter can translate a sensor feed into the simulation's arrival event. A Facade can offer operations such as advanceOneTick() and requestSignalChange() without exposing every queue and timing object. A Decorator can add tracing to a signal controller. A Proxy can enforce authorization around a remote control interface.

These components should not be combined merely to display many pattern names. Each pattern must protect a specific boundary. The Flyweight addresses duplicated immutable description. The signal-control algorithm belongs to a behavioral strategy introduced later. Queue data structures and timing rules remain domain concerns. Keeping those purposes separate prevents the structural layer from becoming a miscellaneous collection of wrappers.

Simulation Correctness and Test Scenarios

Memory optimization is useful only if the simulation remains correct. Create tests for several vehicle types entering different lanes at the same time. Confirm that their specification references are shared where expected, while position and speed change independently. Exercise red, yellow, and green transitions, queue capacity, pedestrian phases, and vehicles that turn across opposing traffic.

  1. Two cars share one immutable car specification.
  2. Changing one car's speed does not affect another car.
  3. A bus and car use different specifications and length constraints.
  4. Backed-up queues preserve arrival order and lane assignment.
  5. A simulation restart reconstructs contexts without relying on object identity.

For cloud-hosted simulations, include scenario identifiers and versions in persisted data. In-memory flyweight identity does not survive serialization or deployment. Persist the canonical key, then resolve it through the local repository when reconstructing the model.

Operational tests should record queue length, wait time, throughput, and specification-cache size. These measurements distinguish traffic-control quality from memory optimization. A change that reduces shared-object count but increases unsafe turns is unacceptable, while a signal algorithm that improves throughput may be valuable even if Flyweight has little effect at the course project's scale.

The same separation helps when the simulation is distributed. One service can own scenario state while another publishes sensor observations. An Adapter validates incoming messages, and the scenario service resolves a versioned vehicle key through its local specification repository. Messages should carry values and identifiers, not serialized assumptions about an object reference from another process.

Finally, preserve a simple non-flyweight implementation as a comparison. Run the same deterministic scenario against both designs and compare results, memory, and execution time. Equivalent outcomes demonstrate that optimization has not changed domain behavior. The measurements show whether the extra factory and context types are justified.

This comparison also creates a safe extension point for later lessons. A behavioral signal strategy can operate on the same intersection facade whether vehicle descriptions are shared or copied. Structural optimization remains an implementation choice behind a stable simulation contract.

Maintain that contract in both Java and C++ examples. Language-specific ownership and memory rules may differ, but vehicle creation, queue progression, and signal decisions should produce equivalent observable scenarios.

That equivalence keeps the pattern lesson focused on design rather than syntax.

Backedup Vehicles Intersection - Exercise

Now it is time to continue your course project.
Backedup Vehicles Intersection - Exercise

SEMrush Software 9 SEMrush Banner 9