Since we are on the "rethinking everything" side, may I suggest to push for a proper generic and simple solution interface?
This is what I started to do in my generalization branch for jMetal 5:
https://github.com/jMetal/jMetal/pull/406/commits
Observations
Here are the facts:
- Variables are already a
List, so we don't need to add get and set methods to interact with each variable, since the List already provides them.
- Objectives are stuck with arrays, which are poorly featured (especially in terms of behavior customization), so we should make them proper
List too (which I did here, in a way which does not break the legacy code).
- Any additional value, like bounds and other things, are required only by some solutions, so they should not be part of the interface to not bother those which don't need it (and don't take more space they need). I tend to think that constraints are the same: some have constraints, others not, so don't impose constraints on all of them.
- The attributes map is the best way to make dynamic extensions of the solution: just design a "property" or "metadata" object which represents an additional property of the solution (constraint, bound, etc.). If this property is specific to the solution, it can use the attributes map to store and retrieve it. If it is a constant, it just returns it (if all your solutions have the same constraints, don't make thousands of them store the same value). If an algorithm requires specific properties, it just ask for them (
Function<S, T>). If it needs to write to it, it can require it too (BiConsumer<S, T>). I made something a bit more integrated here.
Suggestion
So basically, without loosing any functionality, the solution interface can be reduced to that:
public interface Solution<T> extends Serializable {
List<T> variables() ;
List<Double> objectives() ;
Map<Object, Object> attributes();
}
And here are the removed methods and their new forms:
| Legacy |
New |
solution.getVariable(index) |
solution.variables().get(index) |
solution.setVariable(index, value) |
solution.variables().set(index, value) |
solution.getNumberOfVariables() |
solution.variables().size() |
solution.getObjective(index) |
solution.objectives().get(index) |
solution.setObjective(index, value) |
solution.objectives().set(index, value) |
solution.getNumberOfObjectives() |
solution.objectives().size() |
solution.getAttribute(id) |
solution.attributes().get(id) |
solution.setAttribute(id, value) |
solution.attributes().put(id, value) |
solution.hasAttribute(id) |
solution.attributes().containsKey(id) |
solution.getConstraints() |
constraintsProperty.read(solution) |
solution.getConstraint(index) |
constraintsProperty.read(solution).get(index) |
solution.setConstraint(index, value) |
constraintsProperty.read(solution).set(index, value) |
solution.getNumberOfConstraints() |
constraintsProperty.read(solution).size() |
And like constraints, if you need to add boundaries, just create the dedicated property rather than an interface extension which requires its own implementations:
| Legacy |
New |
solution.getLowerBound(index) |
lowerBounds.read(solution).get(index) |
solution.setLowerBound(index, value) |
lowerBounds.read(solution).set(index, value) |
solution.getUpperBound(index) |
upperBounds.read(solution).get(index) |
solution.setUpperBound(index, value) |
upperBounds.read(solution).set(index, value) |
In the case of constraints and boundaries, they all apply to specific variables, so you have to deal with a list of values the same size as the variables list. Which means that for all these properties, you need a single class:
public class VariablesSizedProperty<T> {
void write(Solution<?> s, List<T> values) {
// do some size checks here, as much as variables
s.attributes().put(this, values);
}
List<T> read(Solution<?> s) {
return (List<T>) s.attributes().get(this);
}
}
Actually, since we only use read tog et the list and then read or write it, we can actually replace the write by an automatic generation. Or even combine the two strategies to provide more flexibility.
What about copy?
If you paid attention, you may have seen that I also removed the copy method from the interface.
So aren't we loosing something here?
Actually, it becomes useless, and here is why.
We have no more extensions of the interface, since all extensions are dealt with properties.
Which means that all the algorithms consume and produce Solution instances.
Not even a S extends Solution, but directly Solution itself.
No need to support extensions at all.
Since all implementations are fine, then the algorithm can just pick the implementation it needs.
Just optimize the choice based on the algorithm itself.
All you need then is for this implementation to have a factory method to copy:
static <T> Solution<T> createCopy(Solution<T> original) {
// Create the instance based on the original
// Whatever the original class was
}
If you prefer to keep the possibility to customize the copying process, then rather than having the algorithm decide about it, make it an argument of the algorithm:
UnaryOperator<Solution<T>> copier = // copier given in argument
// ...
Solution<T> copy = copier.apply(solution);
If the user wants to use a specific factory method, he just have to give it as a lambda:
UnaryOperator<Solution<T>> copier = AwesomeSolution::createCopy;
algorithm = new MyAlgorithm(..., copier, ...);
Since we are on the "rethinking everything" side, may I suggest to push for a proper generic and simple solution interface?
This is what I started to do in my generalization branch for jMetal 5:
https://github.com/jMetal/jMetal/pull/406/commits
Observations
Here are the facts:
List, so we don't need to add get and set methods to interact with each variable, since theListalready provides them.Listtoo (which I did here, in a way which does not break the legacy code).Function<S, T>). If it needs to write to it, it can require it too (BiConsumer<S, T>). I made something a bit more integrated here.Suggestion
So basically, without loosing any functionality, the solution interface can be reduced to that:
And here are the removed methods and their new forms:
solution.getVariable(index)solution.variables().get(index)solution.setVariable(index, value)solution.variables().set(index, value)solution.getNumberOfVariables()solution.variables().size()solution.getObjective(index)solution.objectives().get(index)solution.setObjective(index, value)solution.objectives().set(index, value)solution.getNumberOfObjectives()solution.objectives().size()solution.getAttribute(id)solution.attributes().get(id)solution.setAttribute(id, value)solution.attributes().put(id, value)solution.hasAttribute(id)solution.attributes().containsKey(id)solution.getConstraints()constraintsProperty.read(solution)solution.getConstraint(index)constraintsProperty.read(solution).get(index)solution.setConstraint(index, value)constraintsProperty.read(solution).set(index, value)solution.getNumberOfConstraints()constraintsProperty.read(solution).size()And like constraints, if you need to add boundaries, just create the dedicated property rather than an interface extension which requires its own implementations:
solution.getLowerBound(index)lowerBounds.read(solution).get(index)solution.setLowerBound(index, value)lowerBounds.read(solution).set(index, value)solution.getUpperBound(index)upperBounds.read(solution).get(index)solution.setUpperBound(index, value)upperBounds.read(solution).set(index, value)In the case of constraints and boundaries, they all apply to specific variables, so you have to deal with a list of values the same size as the variables list. Which means that for all these properties, you need a single class:
Actually, since we only use
readtog et the list and then read or write it, we can actually replace thewriteby an automatic generation. Or even combine the two strategies to provide more flexibility.What about
copy?If you paid attention, you may have seen that I also removed the
copymethod from the interface.So aren't we loosing something here?
Actually, it becomes useless, and here is why.
We have no more extensions of the interface, since all extensions are dealt with properties.
Which means that all the algorithms consume and produce
Solutioninstances.Not even a
S extends Solution, but directlySolutionitself.No need to support extensions at all.
Since all implementations are fine, then the algorithm can just pick the implementation it needs.
Just optimize the choice based on the algorithm itself.
All you need then is for this implementation to have a factory method to copy:
If you prefer to keep the possibility to customize the copying process, then rather than having the algorithm decide about it, make it an argument of the algorithm:
If the user wants to use a specific factory method, he just have to give it as a lambda: