-
Notifications
You must be signed in to change notification settings - Fork 1k
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
base: dev-2.x
Are you sure you want to change the base?
Make ScheduledTransitLeg, FlexibleTransitLeg fully immutable #6386
Conversation
Codecov ReportAttention: Patch coverage is
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. |
There was a problem hiding this 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.
application/src/ext-test/java/org/opentripplanner/ext/flex/FlexibleTransitLegBuilderTest.java
Outdated
Show resolved
Hide resolved
application/src/ext-test/java/org/opentripplanner/ext/flex/FlexibleTransitLegBuilderTest.java
Outdated
Show resolved
Hide resolved
application/src/ext/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolver.java
Outdated
Show resolved
Hide resolved
...cation/src/ext/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLeg.java
Outdated
Show resolved
Hide resolved
application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java
Show resolved
Hide resolved
application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLegBuilder.java
Outdated
Show resolved
Hide resolved
…xibleTransitLegBuilderTest.java Co-authored-by: Henrik Abrahamsson <[email protected]>
setDistanceMeters(getDistanceFromCoordinates(transitLegCoordinates)); | ||
this.distanceMeters = | ||
builder | ||
.overrideDistanceMeters() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 A
s constructor. The trick is to keep a ref to the original so you know if a
and b
is changed or not.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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'.
There was a problem hiding this comment.
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.
return switch (leg) { | ||
case ScheduledTransitLeg l -> l.copy().withAlerts(totalAlerts).build(); | ||
case FlexibleTransitLeg l -> l.copy().withAlerts(totalAlerts).build(); | ||
default -> leg; | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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(); |
There was a problem hiding this comment.
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.
application/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java
Show resolved
Hide resolved
application/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLegBuilder.java
Show resolved
Hide resolved
setDistanceMeters(getDistanceFromCoordinates(transitLegCoordinates)); | ||
this.distanceMeters = | ||
builder | ||
.overrideDistanceMeters() |
There was a problem hiding this comment.
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 A
s constructor. The trick is to keep a ref to the original so you know if a
and b
is changed or not.
.../src/main/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReference.java
Outdated
Show resolved
Hide resolved
application/src/test/java/org/opentripplanner/_support/time/DateTimes.java
Outdated
Show resolved
Hide resolved
@@ -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); |
There was a problem hiding this comment.
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
Summary
Right now the ScheduledTransitLeg is mostly immutable apart from a few fields
distanceMeters
due to testingalerts
because they are added laterfareProducts
they are also added laterBecause 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.