Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use checked exceptions in a visitor pattern

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:

  • Declare throws IOException on Mammal.accept(), all three MammalVisitor.visit(), and all three MammalPrinter.visit()
    • Very unsavoury: the Mammal and MammalVisitor interfaces are now aware of their potential usage involving IO, which is contrary to the whole point of using the visitor-pattern.
  • Declare throws Throwable on Mammal.accept() and all three MammalVisitor.visit(), and declare throws IOException on all three MammalPrinter.visit()
    • Better than the above solution: Mammal and MammalVisitor are now usage agnostic. However, they are also now unwieldy to use: visitors that throw no exceptions are still forced to handle Throwable from the accept() method.

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.

like image 615
Glenn Lane Avatar asked Nov 07 '13 16:11

Glenn Lane


People also ask

How do I use checked exceptions?

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.

What has checked exception and give an example?

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.

Do checked exceptions need to be caught?

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.

How does the visitor pattern work?

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.


2 Answers

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.

like image 189
Glenn Lane Avatar answered Nov 15 '22 14:11

Glenn Lane


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.

like image 33
Giovanni Botta Avatar answered Nov 15 '22 15:11

Giovanni Botta