I'm developing a hobby project to properly understand encapsulation, what classes can be responsible for, and rules. I asked for a code review and assistance in another forum, but I don't agree with the approach given.
I have the following requirements:
- An international student requires documents to complete the registration process, but domestic students don't.
StudentStatus Interface:
public interface StudentStatus {
Collection<String> retrieveDocuments();
StudentType retrieveStatus();
}
public final class Domestic implements StudentStatus {
private final StudentType type;
private final Collection<String> documents;
public Domestic() {
this.type = StudentType.Domestic;
this.documents = Collections.emptyList();
}
@Override
public Collection<String> retrieveDocuments() {
return this.documents;
}
@Override
public StudentType retrieveStatus() {
return type;
}
}
public final class International implements StudentStatus {
private final StudentType type;
private Collection<String> documents;
public International(Collection<String> documents) {
this.type = StudentType.International;
this.documents = Collections.unmodifiableCollection(documents);
}
@Override
public Collection<String> retrieveDocuments() {
return Collections.unmodifiableCollection(documents);
}
@Override
public StudentType retrieveStatus() {
return type;
}
}
Student class:
public final class Student {
//left out constructor and getters for other attributes.
public Collection<String> retrieveDocuments() {
return status.retrieveDocuments();
}
public StudentType retrieveStatus() {
return status.retrieveStatus();
}
public boolean isVerified(StudentType type) {
return this.retrieveStatus() == type;
}
}
University class:
public class University {
private final Map<Student,Collection<String>> registeredStudents;
private final StudentType type;
public University()
{
registeredStudents = new HashMap<Student,Collection<String>>();
type = StudentType.International;
}
public void add(Student student){
if (student.isVerified(type)){
registeredStudents.put(student, student.retrieveDocuments());
}else {
//throw an exception or handle error accordingly
}
}
}
Before I continue, I understand that this is a really over simplified application process. In the real world, a lot more has to happen before a Student can register. The student may have to go through entrance exams, and payment before registration begins. Also, in a realistic environment, this information would probably be stored in a database that the campus employees can access.
In the other forum, the conversation went into what information is being given out, and approaches were given.
- Have a rule class, that takes the Student object and verifies that it is in fact international and has documents.
The problem I have with this, is you're still going to have to ask the Student his/her status either with the retriveStatus()
or isVerified()
, I don't really see how to do it any other way.
- Pass the Student and collection of documents separately to be added to the Map.
In the real world, the University set the rule as stated above and it's responsibility is to check if International students have documentation.
When I suggested the approach above with the add(Student student)
they stated it wasn't a good idea because the rules can change, and you'll have to change the Student class as well as the University class.
However, in the real world, a student is well aware of his/her status and if he/she is domestic/international and in possession of documents that can be given to the school.
Given the above approach, is writing the add method this way a good idea? Is there a better approach than the add method?
tl;dr - If a Student has to follow the rules set by the University, how then would the Student object communicate with the University to get the data so that the University can ensure the student object is complying with the rules without breaking encapsulation?
The conversation in previous post was probably leading you in a generally good direction. The principle that applies most is the Open / Closed principle. https://en.wikipedia.org/wiki/Open/closed_principle.
Don't set yourself up to have to constantly modify a particular class or set of classes (in OO world at least) in an area you know is going to be a frequent vector of change. The principle applies equally in the functional world, but your example is using an OOPL.
Little hand-built rules engine is a pretty good solution for your stated problem. Particularly if you know the rule flows on pretty fixed inputs - like the University and the Student. DocumentsRequiredForInternationalStudents is a rule class in that architecture - only needs to change if something about that rule itself changes. New rule, which is going to happen a lot = add new class, not modify existing one.
Sometimes you don't know vector of change, harder to make decisions, but if it's obvious, don't architect a system where you'll have to violate open/closed constantly due to an known change vector.
There are different ways to implement little rules engines. One option (this is crappy pseudo-code so it takes less space)
That's just one way to throw it together, but you can see it's not really a lot of code. You have an interface, an implementation class for each rule, and a little rules engine class that applies each rule for you. New rule = new class, modify your constructor in the rules engine to add an instance of that class, done.
This pattern begins to struggle a bit when the properties and behavior that are needed in the rules are very diverse and not concentrated in the same small set of classes.
You said:
This means that the verification responsibility lies on University (and can be different for each University) and not the Student. All student can do is provide necessary information for University to verify.
The
add
method should get documents fromretrieveDocuments
and run through its rules to determine is student is allowed to be accepted.