问题
Here is a problem statement: We have interfaces/super classes Student and Teacher
Student has two implementations/sub clasees, ScienceStudent and PhysicalEducationStudent
Teacher has ScienceTeacher and PhysicalEducationTeacher.
We want to implement a method getMeetingPoint(Student s, Teacher t) which returns a place where they meet based on the type of Student and Teacher.
For example, if its a ScienceStudent and ScienceTeacher they meet at Lab if PEStudent and PETeacher they meet on the Ground and if its a ScienceStudent and PETeacher or vice versa, they meet at cafeteria
We can write a naive method, which checks using instanceof
. But the problem is, this becomes complex when Teacher or Student gets extended, and tough to maintain.
something like this:
public class MeetingPointDecider {
getMeetingPoint(Student s,Teacher t) {
if(s instanceof ScienceStudent && t instanceof ScienceTeacher) {
return "Lab";
} else if (s instanceof PhysicalEducationStudent && t instanceof PhysicalEducationTeacher) {
return "GRound";
}
.
.
.
}
}
Another option is writing a factory, which accepts a Student and a Teacher and returns something like MeetingPointDecision [Ground or Lab], but the problem persists.
Is there any good pattern we can use, where we do not have to modify existing classes (or minimal modification) when a new class is added, Say instanceof
ScienceStudent we have ChemistryStudent, PhysicsStudent and ChemistryLab, PhysicsLab.
There is also a chance of adding more actions, which differs in implementation based on the
Student and Teacher type ( Where Visitor is an option, but not sure how to implement with two deciding classes)
Can someone please suggest a good way to implement this?
Thanks!
回答1:
I would solve this using a map. The key should identify the teacher + student combination and the value would be the meeting point. for the key I would combine the class names. Here is the solution:
public class MeetingPointDecider
{
public enum MeetingPoint { Ground, Lab, Cafeteria }
private static MeetingPoint defaultmp = MeetingPoint.Cafeteria;
private static Map<String, MeetingPoint> studentTeacherCombinations = new HashMap<>();
static {
studentTeacherCombinations.put(getMapKey(ScienceTeacher.class, ScienceStudent.class), MeetingPoint.Lab);
studentTeacherCombinations.put(getMapKey(PETeacher.class , PEStudent.class) , MeetingPoint.Ground);
}
public static MeetingPoint getMeetingPoint(Student s,Teacher t)
{
String mapKey = getMapKey(t.getClass(), s.getClass());
return studentTeacherCombinations.containsKey(mapKey) ?
studentTeacherCombinations.get(mapKey) : defaultmp;
}
private static String getMapKey (Class<? extends Teacher> tCls, Class<? extends Student> sCls)
{
return tCls.getName() + "_" + sCls.getName();
}
}
The logic part is in the static ctor where the map gets populated. It is easy to support future classes.
回答2:
This is interesting topic because recently Eric Lippert has written article that discuss about this. It is divided in five parts:
- Part One
- Part Two
- Part Three
- Part Four
- Part Five
The code is written in C# language but I think it should be understandable enough from Java perspective, at least.
In short, you won't get better result with factory or visitor pattern. Your MeetingPointDecider
implementation is already on track. If you still need something that can be less hardcoded or mapped, try sharonbn's solution or similar.
Or if you need extendable rules, you can try something similar like Decorator pattern:
public class MeetingPointDecider {
// the rules, you can add/construct it the way you want
Iterable<MeetingPointDeciderRule> rules;
string defaultValue;
getMeetingPoint(Student s,Teacher t) {
string result;
for(MeetingPointDeciderRule rule : rules){
result = rule.getMeetingPoint(s, t);
//check whether the result is valid and not null
//if valid, return result
}
//if not valid, return default value
return defaultValue;
}
}
//this can be easily extended
public abstract class MeetingPointDeciderRule {
getMeetingPoint(Student s,Teacher t) {
}
}
Last but not recommended, but if you still need the flexibility, you can try to runtime compile the class and use it as rule engine. But not recommended.
Note: I am not answering the original question hence the community wiki answer. If this answer format is wrong, I will delete it.
回答3:
What if you add a getMeetingKeyPart() method to the interfaces (Student and Teacher) and implement to return specific key parts for each Student and Teacher implementation.
E.g. ScienceStudent returns "ScienceStudent" and ScienceTeacher returns "ScienceTeacher".
Then you can define a .properties file where meeting points are defined for any desired key combination. E.g.
ScienceStudent-ScienceTeacher=Lab
PhysicalEducationStudent-PhysicalEducationTeacher=Ground
...
If there is no match for the key combination you return "cafeteria"
回答4:
Assuming you can't change the interfaces, you can create a Faculty
enum and add support to it to derive the faculty from the class type.
public enum Faculty {
SCIENCE("Lab", Arrays.asList(ScienceStudent.class, ScienceTeacher.class)),
PHYSICAL_EDUCATION("Ground", Arrays.asList(PhysicalEducationStudent.class, PhysicalEducationTeacher.class)),
UNKNOWN("Unknown", Collections.<Class<?>>emptyList());
private final List<Class<?>> types = new LinkedList<>();
public final String meetingPlace;
Faculty(String meetingPlace,
List<Class<?>> types) {
this.meetingPlace = meetingPlace;
this.types.addAll(types);
}
public static Faculty getFaculty(Class<?> type) {
Faculty faculty = UNKNOWN;
final Faculty[] values = values();
for (int i = 0; faculty == UNKNOWN && i < values.length; i++) {
for (Iterator<Class<?>> iterator = values[i].types.iterator(); faculty == UNKNOWN && iterator.hasNext(); ) {
final Class<?> acceptableType = iterator.next();
faculty = type.isAssignableFrom(acceptableType) ? values[i]
: UNKNOWN;
}
}
return faculty;
}
}
In your meeting place decider, you can then get the faculties and compare them.
final Faculty studentFaculty = Faculty.getFaculty(student.getClass());
final Faculty teacherFaculty = Faculty.getFaculty(teacher.getClass());
return studentFaculty == teacherFaculty ? teacherFaculty.meetingPlace
: "cafeteria";
Ideally, you would be able to alter the Teacher
and Student
interfaces to get the ´Faculty´ directly, and then you could simply it.
final Faculty studentFaculty = student.getFaculty();
final Faculty teacherFaculty = teacher.getFaculty();
return studentFaculty == teacherFaculty ? teacherFaculty.meetingPlace
: "cafeteria";
Of course, this is not always possible, hence the first solution.
回答5:
The visitor pattern for 2 arguments will work the same way as for single argument. You just need to use concrete implementations for the method parameters so that the compiler can pick the correct method based on the invocation context.
public class MeetingPointDecider implements StudentTeacherVisitor {
Decision getMeetingPoint(ScienceStudent s, ScienceTeacher t) {
// return result
}
Decision getMeetingPoint(PEStudent s, PETeacher t) {
// return result
}
// etc.
}
Of course this may not be what you want since when calling a specific visitor method you need to know the concrete types of Student and Teacher so the resolution happens at compile time. As others suggested you can use a Map/Properties approach.
回答6:
I would create an interface to model the behavior of anyone who can meet. The interface would be implemented by students, teachers, gymnasts, scientists, etc. The implementors would utilize default behavior from the interface or override it with their own. New implementors could be added at any time.
public static void main(String... args) {
Meeter scienceTeacher = new ScienceTeacher();
Meeter scienceStudent = new ScienceStudent();
Meeter gymTeacher = new GymTeacher();
Meeter gymStudent = new GymStudent();
System.out.println("Science Teacher & Science Student meet in the " + scienceTeacher.findMeetingPointWith(scienceStudent));
System.out.println("Science Teacher & Gym Student meet in the " + scienceTeacher.findMeetingPointWith(gymStudent));
System.out.println("Gym Teacher & Science Student meet in the " + gymTeacher.findMeetingPointWith(scienceStudent));
System.out.println("Gym Teacher & Gym Student meet in the " + gymTeacher.findMeetingPointWith(gymStudent));
}
interface Meeter {
enum MeetingPoint { LAB, GYM, CAFETERIA }
MeetingPoint preferredMeetingPoint();
default MeetingPoint findMeetingPointWith(Meeter other) {
MeetingPoint myPreference = preferredMeetingPoint();
return myPreference == other.preferredMeetingPoint() ? myPreference : defaultMeetingPoint();
}
default MeetingPoint defaultMeetingPoint() {
return MeetingPoint.CAFETERIA;
}
}
interface Scientist extends Meeter {
@Override default MeetingPoint preferredMeetingPoint() {
return MeetingPoint.LAB;
}
}
interface Gymnast extends Meeter {
@Override default MeetingPoint preferredMeetingPoint() {
return MeetingPoint.GYM;
}
}
static class ScienceTeacher implements Scientist {}
static class ScienceStudent implements Scientist {}
static class GymTeacher implements Gymnast {}
static class GymStudent implements Gymnast {}
Note the above example is not commutative, i.e. A meets B could produce a different result from B meets A. If this is undesirable, consider adding a priority()
method to the Meeter
which can determine the order of comparison.
来源:https://stackoverflow.com/questions/30450793/visitor-pattern-for-two-arguments