Recall: we used specifications for validating a requestGraduationThesis method of a Student object.
One common need, as emerged here, is to communicate the result of this validation in case something goes wrong.
That is, we need to transfer errors resulted from business methods to the layers above, the service or presentation layers.
Here, I propose an approach using a Notification object and a "business oriented" exception as a means of transporting.
A Notification object is a container of Error objects, carrying information about errors.
Here is a simple implementation:
public class Notification {
private List errors = new LinkedList();
public List getErrors() {
return this.errors;
}
public boolean hasErrors() {
return errors.isEmpty();
}
public void addError(Error error) {
this.errors.add(error);
}
}
public class Error {
private String message;
public Error() {}
public Error(String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
}
Then, we need an exception for transporting our notification in the layers above, up till the layer interested in managing it:
public class StudentException extends Exception {
private Notification notification;
public StudentException(Notification notification) {
this.notification = notification;
}
public Notification getNotification() {
return this.notification;
}
}
Here, I used a checked exception because I think business errors should always be explicit, but if you think checked exception are too cluttering or invasive, you can safely use an unchecked one: I just suggest you to always declare your exceptions in javadocs.
Finally, we need to link together Errors with Specifications, and Notifications with Students:
public class EnoughCreditsSpecification extends BaseSpecification {
private static final int min = 10;
private static final String errorMessage = "credits.error";
private Error error;
public boolean isSatisfiedBy(Student s) {
if (s.getTotalCredits() >= EnoughCreditsSpecification.min) {
return true;
}
else {
error = new Error(EnoughCreditsSpecification.errorMessage);
return false;
}
}
public Error getError() {
return this.error;
}
}
public class EnoughCoursesSpecification extends BaseSpecification {
private static final int min = 3;
private static final String errorMessage = "courses.error";
private Error error;
public boolean isSatisfiedBy(Student s) {
if (s.getTotalCourses() >= EnoughCoursesSpecification.min) {
return true;
}
else {
error = new Error(EnoughCoursesSpecification.errorMessage);
return false;
}
}
public Error getError() {
return this.error;
}
}
public class Student {
private int totalCourses;
private int totalCredits;
private boolean thesisRequested;
private List> graduationSpecs;
/**
* Register an exam related to a given course.
* This increases the student credits.
*/
public void registerExam(Course course) {
this.totalCredits += course.getCredits();
this.totalCourses++;
}
/**
* Return true if this student can request a graduation thesis,
* depending on a given list of specifications.
*
* @throws StudentException If something with this student
* business method goes wrong ...
*/
public void requestGraduationThesis()
throws StudentException {
if (this.applyAllSpecs() == false) {
Notification n = new Notification();
for (BaseSpecification spec : this.graduationSpecs) {
if (spec.getError() != null) {
n.addError(spec.getError());
}
}
throw new StudentException(n);
}
else {
this.thesisRequested = true;
}
}
public int getTotalCredits() {
return this.totalCredits;
}
public int getTotalCourses() {
return this.totalCourses;
}
/**
* Get the list of specifications defining in which case a student can
* request a graduation thesis.
*/
public List> getGraduationSpecs() {
return graduationSpecs;
}
/**
* Set the list of specifications defining in which case a student can
* request a graduation thesis.
*/
public void setGraduationSpecs(List> graduationSpecs) {
this.graduationSpecs = graduationSpecs;
}
private boolean applyAllSpecs() {
boolean result = true;
for (BaseSpecification spec : this.graduationSpecs) {
if (!spec.isSatisfiedBy(this)) {
result = false;
}
}
return result;
}
}
Here is a simple main method for showing all this stuff:
public class TestStarter {
public static void main(String[] args) {
List<basespecification><student>> specs1 = new ArrayList();
specs1.add(new EnoughCoursesSpecification());
specs1.add(new EnoughCreditsSpecification());
Course c1 = new Course();
Course c2 = new Course();
c1.setCredits(5);
c2.setCredits(4);
Student s1 = new Student();
s1.setGraduationSpecs(specs1);
s1.registerExam(c1);
s1.registerExam(c2);
try {
s1.requestGraduationThesis();
}
catch(StudentException ex) {
Notification n = ex.getNotification();
List<error> errors = n.getErrors();
for (Error e : errors) {
System.out.println(e.getMessage());
}
}
}
}
Doing so, we still use wonderful specifications, avoiding the hard-to-manage validate() method, even if we need multiple error notifications to superior layers, and we do not write additional code for transporting them because we use existent language features.
An alternative implementation, as suggested by Martin Fowler, is to use event notification patterns.
We could explore this in another post.
In the meanwhile, what do you think about this?