Suppose I have a suite of classes that accept a visitor (visitor-pattern), but due to the nature of those classes or a particular visitor, performing working on them will likely be capable of throwing a checked exception.
The visitor-accepting interface:
public interface Mammal
{
void accept(MammalVisitor visitor);
}
The visitor interface:
public interface MammalVisitor
{
void visit(Cat m);
void visit(Dog m);
void visit(Cow m);
}
And the implementations of Mammal:
public class Cat implements Mammal
{
public void accept(MammalVisitor visitor)
{
visitor.visit(this);
}
}
We'll assume Dog & Cow are implemented identical to Cat
Now suppose my visitor is:
public class MammalPrinter implements MammalVisitor
{
private final Appendable out;
public MammalPrinter(Appendable out)
{
this.out = out;
}
@Override
public void visit(Cat m)
{
out.append("I'm a cat");
}
@Override
public void visit(Dog m)
{
out.append("I'm a dog");
}
@Override
public void visit(Cow m)
{
out.append("I'm a cow");
}
}
And I print the result to stdio:
Mammal m = MammalFactory.getMammal();
MammalPrinter mp = new MammalPrinter(System.out);
m.accept(mp);
However, MammalPrinter above is syntactically incorrect, because Appendable.append(String) throws java.io.IOException
. I can't declare the throw on each visit method, because it is not declared in the visitor interface.
The solutions I've considered:
throws IOException
on Mammal.accept()
, all three MammalVisitor.visit()
, and all three MammalPrinter.visit()
throws Throwable
on Mammal.accept()
and all three MammalVisitor.visit()
, and declare throws IOException
on all three MammalPrinter.visit()
I have two other solutions that I favour over the above, with which I will self-answer my post. I'd like to see which one is favoured by the community at large.
Checked exceptions are checked at compile-time. It means if a method is throwing a checked exception then it should handle the exception using try-catch block or it should declare the exception using throws keyword, otherwise the program will give a compilation error.
Checked Exceptions In general, checked exceptions represent errors outside the control of the program. For example, the constructor of FileInputStream throws FileNotFoundException if the input file does not exist. Java verifies checked exceptions at compile-time.
There are 2 kinds of exceptions: Checked and Unchecked. A Checked exception can be considered one that is found by the compiler, and the compiler knows that it has a chance to occur, so you need to catch or throw it. For example, opening a file.
The Visitor pattern represents an operation to be performed on the elements of an object structure without changing the classes on which it operates. This pattern can be observed in the operation of a taxi company. When a person calls a taxi company (accepting a visitor), the company dispatches a cab to the customer.
I was going to mention the unchecked wrapped re-throw approach, but Giodude beat me to it. Instead, I'll suggest another approach that I call the courtesy exception (because it is integrated in the interfaces as a courtesy to implementers).
When designing the visitor and Mammal interfaces, I outfit them to handle one exception of the user's choosing. The visitor:
public interface MammalVisitor<T extends Throwable>
{
void visit(Cat m) throws T;
void visit(Dog m) throws T;
void visit(Cow m) throws T;
}
And Mammal:
public interface Mammal
{
<T extends Throwable> void accept(MammalVisitor<T> visitor) throws T;
}
And the implementation of Mammal:
public class Cat implements Mammal
{
@Override
public <T extends Throwable> void accept(MammalVisitor<T> visitor) throws T
{
visitor.visit(this);
}
}
Dog and Cow are implemented identically. And the printing visitor:
public class MammalPrinter implements MammalVisitor<IOException>
{
private final Appendable out;
public MammalPrinter(Appendable out)
{
this.out = out;
}
@Override
public void visit(Cat m) throws IOException
{
out.append("I'm a cat");
}
@Override
public void visit(Dog m) throws IOException
{
out.append("I'm a dog");
}
@Override
public void visit(Cow m) throws IOException
{
out.append("I'm a cow");
}
}
And usage:
Mammal m = MammalFactory.getMammal();
MammalPrinter mp = new MammalPrinter(System.out);
try
{
m.accept(mp);
}
catch (IOException e)
{
System.err.println("An IOException occurred");
}
Which results in a much more intuitive and easy to implement usage from the end-user's perspective.
With this pattern, if a visitor does not have a checked exception to throw, they specify some unchecked exception as the generic in their implementation:
public class MammalPrinter implements MammalVisitor<RuntimeException>
{
When Mammal.accept() is called with the above visitor, no catching is needed to be syntactically correct. Perhaps you could further increase readability by making an extend of RuntimeException called "NeverThrown" that has a private constructor.
You could catch the checked exceptions and throw them wrapped in unchecked exceptions. See for example how Spring converts JDBC or JMS checked exceptions to unchecked ones.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With