Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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.

You are missing an option which is to internally handle the IOException. Options are to ignore it, report it, or rethrow it wrapped as a RuntimeException....

// Ignore it (swallow it).
@Override 
public void visit(Cow m) {
    try {
        out.append("I'm a cow");
    } catch (IOException ioe) {
        // swallow this exception - it will never happen
    }
}

// report it
@Override 
public void visit(Cow m) {
    try {
        out.append("I'm a cow");
    } catch (IOException ioe) {
        ioe.printStackTrace();
    }
}

// wrap and rethrow it.
@Override 
public void visit(Cow m) {
    try {
        out.append("I'm a cow");
    } catch (IOException ioe) {
        throw new IllegalStateException("Unable to append to output", ioe);
    }
}

I suppose it depends on whether you want your client code to be impacted by those exceptions or not. You could swallow them within the code, wrap them in Runtime Exceptions as above, or alternatively you could add throws Exception to the 'visit' methods and do something with it or rethrow it in the accept method.

Doing the last option would be acknowledging that whatever the visitor does has the capacity to create an exception condition and that calling code should be aware of that.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top