Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ScheduledTransitLeg, FlexibleTransitLeg fully immutable #6386

Open
wants to merge 26 commits into
base: dev-2.x
Choose a base branch
from

Conversation

leonardehrenfried
Copy link
Member

Summary

Right now the ScheduledTransitLeg is mostly immutable apart from a few fields

  • distanceMeters due to testing
  • alerts because they are added later
  • fareProducts they are also added later

Because Aracadis has a few sandbox features that manipulate the legs over the years I had to fix many bugs because of the leg is half immutable and half mutable. I would like to fix all of them at once and therefore I'm making the ScheduledTransitLeg and FlexibleTransitLeg immutable and all fields must now be set via a builder.

There is also the special sandbox leg ConsolidatedStopLeg which also required some special handling but it is now also fully immutable and has a builder.

Unit tests

Lots of tests added and some re-written.

Documentation

Javadoc.

Copy link

codecov bot commented Jan 15, 2025

Codecov Report

Attention: Patch coverage is 94.26230% with 7 lines in your changes missing coverage. Please review.

Project coverage is 69.75%. Comparing base (448ad8f) to head (5b5937d).
Report is 13 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...er/routing/algorithm/mapping/AlertToLegMapper.java 77.77% 2 Missing and 2 partials ⚠️
...ntripplanner/ext/fares/FaresToItineraryMapper.java 60.00% 1 Missing and 1 partial ⚠️
...terchain/filters/transit/DecorateTransitAlert.java 83.33% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #6386      +/-   ##
=============================================
+ Coverage      69.72%   69.75%   +0.03%     
- Complexity     18016    18051      +35     
=============================================
  Files           2057     2059       +2     
  Lines          76967    77024      +57     
  Branches        7844     7842       -2     
=============================================
+ Hits           53666    53731      +65     
+ Misses         20550    20543       -7     
+ Partials        2751     2750       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@leonardehrenfried leonardehrenfried changed the title Make ScheduledTransitLeg, FlexibleTransitLeg immutable Make ScheduledTransitLeg, FlexibleTransitLeg fully immutable Jan 17, 2025
Copy link
Contributor

@habrahamsson-skanetrafiken habrahamsson-skanetrafiken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great work. Making these model classes fully immutable makes stuff so much simpler to reason about.

setDistanceMeters(getDistanceFromCoordinates(transitLegCoordinates));
this.distanceMeters =
builder
.overrideDistanceMeters()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a potential problem with the overrideDistanceMeters thing. If someone were to do this:

// Given an existing leg1 with some geometry
var leg2 = leg1.copy().withAlightStopIndexInPattern(newIndex).build();
// Now leg2 will have another geometry but the same distance as leg1

It's probably not an issue right now but there is a potential for bugs. One solution would be to remove the builder.setOverrideDistanceMeters() and rewrite the test that uses it to generate some actual coordinates.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the override is ugly and dangerous. I will try to get rid of it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is only a single test GroupByDistanceTest that actually requires the ability to directly set the distance. It will be a bit of work to re-write it. Let's discuss in the meeting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Legs should be reduced to a DTO - carrying data - not responsible for data consistency. For values calculated e.g. in routing we want them to propagate to the output - not recomputing them even if they are slightly off due to some optimization. It is ok to fallback to some default algorithm to compute unset values. The pattern I used in builders to achieve this is like this (pseudo code), b can be derived from a:

class A {
  a, b : AType;  
}

class ABuilder {
  A original;
  a = NOT_SET, b=NOT_SET;  
  withA(...)
  withB(...)
  computeB() {
    if(b != NOT_SET) return b;
    if(a != NOT_SET) return <compute b based on a>;
    return original.b();
  }
}

computeB() is called in the build() method or As constructor. The trick is to keep a ref to the original so you know if a and b is changed or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided on the developer meeting to accept the issue with the overrideDistance for now and solve it in an upcoming PR.

With that caveat, I'm fine with this PR.

@@ -22,12 +22,15 @@ public DecorateTransitAlert(

@Override
public void decorate(Itinerary itinerary) {
boolean firstLeg = true;
for (Leg leg : itinerary.getLegs()) {
final var firstLeg = new AtomicBoolean(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this an AtomicBoolean ? Consider changing it to a 'boolean'.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to be an atomic boolean because you cannot use a primitive value in a lambda.

Comment on lines +126 to +130
return switch (leg) {
case ScheduledTransitLeg l -> l.copy().withAlerts(totalAlerts).build();
case FlexibleTransitLeg l -> l.copy().withAlerts(totalAlerts).build();
default -> leg;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the if (!leg.isTransitLeg()) is fragile. Another way to solve it is to use an interface:

interface AlertsAware<T extends AlertsAware<T>> { <access alert methods>; T decorateWithAlerts(alerts); }

The if would then be if(!(leg instanceof AlertsAware legWithAlerts)) ... and the implementation of decorateWithAlerts will copy, add and build. The nice thing about using interfaces is that any class can implement it, and having a list of Alerts is no longer tied to the Leg type hierarchy.Later we can add TaxiLeg extends CarLeg implements WithAlerts.

The minimum change is to throw an exception in the default case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative here is to not pass in Leg at all, just the fields that the mapper need and return a list of Allerts. Then there is not need to create the leg, and rebuild it with alerts as the next step.

Comment on lines 35 to 43
LocalDate.of(2025, 1, 15),
new FlexPath(1000, 600, () -> GeometryUtils.makeLineString(1,1,2,2))
);
private static final TransitAlert ALERT = TransitAlert.of(id("alert")).withHeaderText(I18NString.of("alert 1")).build();
private static final Duration TIME_SHIFT = Duration.ofHours(5);

@Test
void listsAreInitialized(){
var leg = new FlexibleTransitLegBuilder().withStartTime(ZONED_DATE_TIME_1).withEndTime(ZONED_DATE_TIME_2).withFlexTripEdge(EDGE).build();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a logical fragile dependency on the values for date here. DateTimes.ZONED_DATE_TIME_1 should only be used in cases where you do not care if what the date is - if someone changes the date in DateTimes your test should stil run green - if not define the testdata inside the test.

setDistanceMeters(getDistanceFromCoordinates(transitLegCoordinates));
this.distanceMeters =
builder
.overrideDistanceMeters()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Legs should be reduced to a DTO - carrying data - not responsible for data consistency. For values calculated e.g. in routing we want them to propagate to the output - not recomputing them even if they are slightly off due to some optimization. It is ok to fallback to some default algorithm to compute unset values. The pattern I used in builders to achieve this is like this (pseudo code), b can be derived from a:

class A {
  a, b : AType;  
}

class ABuilder {
  A original;
  a = NOT_SET, b=NOT_SET;  
  withA(...)
  withB(...)
  computeB() {
    if(b != NOT_SET) return b;
    if(a != NOT_SET) return <compute b based on a>;
    return original.b();
  }
}

computeB() is called in the build() method or As constructor. The trick is to keep a ref to the original so you know if a and b is changed or not.

@@ -22,8 +25,12 @@ public static void addFaresToLegs(ItineraryFares fares, Itinerary i) {

i.transformTransitLegs(leg -> {
var legUses = fares.getLegProducts().get(leg);
leg.setFareProducts(ListUtils.combine(itineraryFareUses, legUses));
return leg;
var allUses = ListUtils.combine(itineraryFareUses, legUses);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for the Alerts, maybe use interface ProductAware

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants