Wednesday, March 15, 2006

A notification strategy for business errors

In one of my past posts we talked about implementing specifications with Apache Commons Collections.
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?

5 comments:

Sergio Bossa said...

Johannes Brodwall said...

> Our implementation differs from the
> one you have in that the
> Specicifications would have a
> validate(Object, Notification)
> method which adds the errors to the
> Notification. I think this might be
> a better approach than forcing the
> client to accumulate the errors.

Yes, I agree with you.
The only problem with your implementation is that it brokes compatibility between Specifications and Commons Collections Predicates ... at least, you cannot collect Notifications when combining Specifications with, say, PredicateUtils.
Any thought?

> PS: FYI The formatting of the code
> is pretty horrible when viewed in
> an RSS reader

Too bad, I've just noticed this ... I'm evaluating the possibility to not post any code directly into the blog entry, rather using some external service like http://www.bytemycode.com/
What do you think?

Thanks for your feedback,
Regards,

Sergio B.

Anonymous said...

Considering that you seem to be interested in DDD I would like to mention that you seem to not have applied the DDD concept of
"side-effect free functions" (aka Command Query Separation Principle).
The query method "isSatisfiedBy" will result in an observable side effect
(it will set the error).

Also, if you try to reuse the specification, the error will remain, for example if you would do something like this:

EnoughCreditsSpecification ecs = new EnoughCreditsSpecification();
if(ecs.isSatisfiedBy(student1))
{
// let's say you do get here
// now you have an error in ecs.getError()
}

if(ecs.isSatisfiedBy(student2))
{
// let's say you do NOT get here
}

// now you still have an error in ecs.getError()

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

Useful bit of code that. Ta very much

Website Hosting said...

Thanks for the nice post.