refactor: ScoreDirector is no longer public#2129
refactor: ScoreDirector is no longer public#2129triceo wants to merge 3 commits intoTimefoldAI:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request refactors the ScoreDirector from public API to internal implementation as part of Timefold Solver 2.0. The changes involve moving ScoreDirector from ai.timefold.solver.core.api.score.director to ai.timefold.solver.core.impl.score.director, and introducing a new PhaseCommandContext interface to replace the direct use of ScoreDirector in the public API. This is a significant breaking change that improves API encapsulation.
Changes:
- ScoreDirector moved from public API to internal implementation package
- PhaseCommand interface refactored to use PhaseCommandContext instead of ScoreDirector and BooleanSupplier parameters
- SolutionPartitioner.splitWorkingSolution() signature changed to accept Solution_ instead of ScoreDirector<Solution_>
- Documentation updated to reflect new API patterns and remove deprecated content
- Test domain classes updated (comparator/factory naming consistency improvements)
- MoveDirector enhanced with new assignValuesAndAdd method and executeTemporary variants
Reviewed changes
Copilot reviewed 123 out of 123 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirector.java | Moved from api package to impl package, marked as non-public API |
| core/src/main/java/ai/timefold/solver/core/api/solver/phase/PhaseCommand.java | Interface refactored to use PhaseCommandContext parameter |
| core/src/main/java/ai/timefold/solver/core/api/solver/phase/PhaseCommandContext.java | New interface providing access to working solution and move execution |
| core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultPhaseCommandContext.java | Implementation of PhaseCommandContext |
| core/src/main/java/ai/timefold/solver/core/impl/partitionedsearch/partitioner/SolutionPartitioner.java | Interface updated to remove ScoreDirector dependency |
| core/src/main/java/ai/timefold/solver/core/impl/move/MoveDirector.java | Enhanced with new assignValuesAndAdd method and executeTemporary variants |
| docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc | Updated PhaseCommand documentation with new API |
| docs/src/modules/ROOT/pages/responding-to-change/responding-to-change.adoc | Removed deprecated PinningFilter section, updated ProblemChange references |
| docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc | Removed deprecated constraint configuration section |
| Multiple test files | Updated PhaseCommand implementations to use new API, comparator/factory naming improvements |
| Multiple impl files | Updated imports from api.score.director to impl.score.director |
docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc
Outdated
Show resolved
Hide resolved
docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc
Outdated
Show resolved
Hide resolved
core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityProperty.java
Outdated
Show resolved
Hide resolved
core/src/main/java/ai/timefold/solver/core/impl/move/MoveDirector.java
Outdated
Show resolved
Hide resolved
f3948c7 to
903c988
Compare
903c988 to
012ee2e
Compare
core/src/test/java/ai/timefold/solver/core/impl/solver/SolverMetricsIT.java
Outdated
Show resolved
Hide resolved
core/src/main/java/ai/timefold/solver/core/api/solver/phase/PhaseCommandContext.java
Outdated
Show resolved
Hide resolved
012ee2e to
b1a2c0b
Compare
|
Christopher-Chianelli
left a comment
There was a problem hiding this comment.
I dislike renaming Rebaser to Lookup; the methods are more awkward, and I believe some changes should be made; in particular, the lookup/rebase method should not have "return null if does not exist" method, and instead I propose these two methods:
@NullMarked
interface Rebaser {
@Nullable
<Type_> rebase(@Nullable Type_ toRebase);
<Type_> rebaseExisting(Type_ toRebase);
}Both methods fail fast if toRebase does not exist in the ScoreDirector. They only differ in one accepts null (and thus return a nullable), the other does not (and thus always return non-null).
| * As defined by {@link #execute(Move, boolean)}, | ||
| * but with the guarantee of a fresh score. | ||
| */ | ||
| default void execute(Move<Solution_> move) { |
There was a problem hiding this comment.
I think the execute commands should return the score (that way, users don't need to go through getWorkingSolution).
| isInitializedPredicate = this::isInitialized; | ||
| if (entityClass.getPackage() == null) { | ||
| LOGGER.warn("The entityClass ({}) should be in a proper java package.", entityClass); | ||
| LOGGER.warn("The entityClass ({}) is in an unnamed package.", entityClass); |
There was a problem hiding this comment.
Why do we warn about this?
|
|
||
| @Override | ||
| public SelectorBasedKOptListMove<Solution_> rebase(Rebaser rebaser) { | ||
| public SelectorBasedKOptListMove<Solution_> rebase(Lookup lookup) { |
There was a problem hiding this comment.
I dislike replacing Rebaser with Lookup. First, when rebasing, you always want to fail-fast (whereas with Lookup, you might want to return null if the entity does not exist for problem changes). Second, the method name is long and awkward; rebase is much nicer.
| public SelectorBasedListSwapMove<Solution_> rebase(Lookup lookup) { | ||
| return new SelectorBasedListSwapMove<>(variableDescriptor, | ||
| Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(leftEntity)), | ||
| leftIndex, Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(rightEntity)), rightIndex); |
There was a problem hiding this comment.
Objects.requireNonNull is worthless, since lookUpWorkingObjectOrFail will fail-fast if there no corresponding object, and leftEntity/rightEntity are guarantee to not be null.
| sourceIndex, Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex); | ||
| public SelectorBasedListChangeMove<Solution_> rebase(Lookup lookup) { | ||
| return new SelectorBasedListChangeMove<>(variableDescriptor, | ||
| Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(sourceEntity)), |
| return new ListAssignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(planningValue)), | ||
| Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex); | ||
| public Move<Solution_> rebase(Lookup lookup) { | ||
| return new ListAssignMove<>(variableMetaModel, Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(planningValue)), |
| return new ListChangeMove<>(variableMetaModel, | ||
| Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex, | ||
| Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex); | ||
| Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(sourceEntity)), sourceIndex, |
| public Move<Solution_> rebase(Rebaser rebaser) { | ||
| return new ListUnassignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex); | ||
| public Move<Solution_> rebase(Lookup lookup) { | ||
| return new ListUnassignMove<>(variableMetaModel, Objects.requireNonNull(lookup.lookUpWorkingObjectOrFail(sourceEntity)), |
| assertThat(entity.getValueList()).hasSize(1); | ||
| assertThat(TestdataObjectSortableDescendingFactory.extractCode(entity.getValueList().get(0).getCode())) | ||
| assertThat( | ||
| TestdataObjectSortableDescendingComparator.extractCode(entity.getValueList().get(0).getCode())) |
There was a problem hiding this comment.
| TestdataObjectSortableDescendingComparator.extractCode(entity.getValueList().get(0).getCode())) | |
| TestdataObjectSortableDescendingComparator.extractCode(entity.getValueList().getFirst().getCode())) |
| } | ||
|
|
||
| @Test | ||
| void assignValuesAndAddToEmptyList() { |
There was a problem hiding this comment.
Are these duplicates of the tests above?



No description provided.