I have a generic Person
class, and two types of people Student
and Teacher
that extends the Person
class. I also have teaching sessions which will store a list of students and a list of teachers that will be in that session.
class Person {}
class Student extends Person {}
class Teacher extends Person {}
class Session {
List<Student> students = new ArrayList();
List<Teacher> teachers = new ArrayList();
// add a person to the teaching session.
// They should be placed into the appropriate lists
public void enroll(Person p)
{
if (p instanceof Student)
students.add((Student) p)
else if (p instanceof Teacher)
teachers.add((Teacher) p)
}
}
The idea is that some other code will have a list of people and iterate the list to enroll them into the appropriate sessions as needed. However, the enroll
method currently explicitly checks the object's type, which to me is undesirable and seems like bad design.
I have tried writing the enroll
method like this using method overloading, which looks a lot cleaner
public void enroll(Student p)
{
students.add(p)
}
public void enroll(Teacher p)
{
teachers.add(p)
}
but it seems like the code that iterates through the list of Person
objects would need to then figure out whether the current person is a student or teacher instance, and type-cast appropriately before passing it to an enroll
method.
Is there a way for me to design this so that I do not need to invoke instanceof
at any point in time in my own code?
Session
needs to have an overloaded enroll method as in your question. Add an abstract enroll
method to Person
class that takes Session
as a parameter
public abstract void enroll (Session s);
Teacher
and Student
each override enroll
public void enroll (Session s) { s.enroll(this); }
The simplest way to do this is to add an isTeacher()
method to the person class.
Here is a basic implemention (with the parts you already wrote left out)
abstract class Person {
public abstract boolean isTeacher();
}
class Teacher extends Person {
public boolean isTeacher() { return true; }
}
class Student extends Person {
public boolean isTeacher() {return false; }
}
abstract class Session {
public void enroll(Person p) {
if (p.isTeacher()) teachers.add((Teacher)p);
else student.add((Student)p);
}
}
If you also want to avoid the typecast, try this...
abstract class Person {
public abstract Teacher asTeacher();
public abstract Student asStudent();
}
class Student extends Person {
public Student asStudent() { return this; }
public Teacher asTeacher() { return null; }
}
class Teacher extends Person {
public Student asStudent() { return null; }
public Teacher asTeacher() { return this; }
}
class Session extends Person {
public void enroll(Person p) {
Teacher t = p.asTeacher();
if (t != null) teachers.add(t);
else students.add(p.asStudent());
}
}
This implementation has a minor weakness that it assumes Teachers and Students are the only types of people, but extending it to multiple types is fairly straightforward.
The issue with p instanceof Student
is that you change the behavior based on something that is not a method invocation, so future changes will be hard (ie. if you add another sub-class of Person).
There are a couple of ways to look at this design problem:
Why you need to create a hierarchy of Person
? It's possible in your system that someone that is a student can be a teacher in another course? If that is the case, you have some information about a person, and what changes is the role of that person for a course. So is not convenient to use inheritance for that, use composition and create an object that represents the role of a Person
in a course.
You can add a isTeacher
method, or use a visitor pattern to split the collection. That will be a little bit better than the instanceof
. But in my opinion the problem is still in the incorrect class hierarchy.
For example, using "roles":
enum Role { TEACHER, STUDENT; }
class Session {
List<SessionEnrolment> enrollments = new ArrayList<>();
public void enroll(Person p, Role role)
{
enrollments.add(new SessionEnrollment(p, role));
}
public List<Person> getTeachers() {
List<Person> result = new ArrayList<>();
for (SessionEnrolment e : enrollments) {
if (e.isTeacher()) { result.add(e.getPerson()); }
return result;
}
}
}
Using a isTeacher
method:
public void enroll(Person p)
{
if (p.isTeacher()) { teachers.add(p); } else { students.add(p); }
}
As you can see having a isTeacher
method for all persons looks awkward. And the role solution reflects better the temporal property of being a student or teacher.
You can use a visitor, and it will save you from the ugly isTeacher
in Person
; but to me that solution is overcomplicated, and you'll translate the "uglyness" to the Visitor
interface.
You can do what you propose with an overloaded method per concrete Person
implementation, and in addition to that add a Type to the List:
List<Student> students = new ArrayList<>();
List<Teacher> teachers = new ArrayList<>();
Then in your iteration logic you know the type.
You can turn it around:
class Session {
StudentList students = new StudentList();
TeacherList teachers = new TeacherList();
// add a person to the teaching session.
// They should be placed into the appropriate lists
public void enroll(Person p)
{
p.addMe(students, teachers);
}
}
public class StudentList extends ArrayList<Student> {
}
public class TeacherList extends ArrayList<Teacher> {
}
public abstract class Person {
public abstract void addMe(StudentList sList, TeacherList tList);
}
public class Student extends Person {
public void addMe(StudentList sList, TeacherList tList) {
sList.add(this);
}
}
public class Teacher extends Person {
public void addMe(StudentList sList, TeacherList tList) {
tList.add(this);
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.