Как использовать проверенные исключения в шаблоне посетителя

Предположим, что у меня есть набор классов, которые принимают посетителя (шаблон посетителя), но из-за характера этих классов или конкретного посетителя, работа над ними, вероятно, будет способна выбрасывать проверенное исключение.

Пользовательский интерфейс:

public interface Mammal
{
    void accept(MammalVisitor visitor);
}

Интерфейс посетителя:

public interface MammalVisitor
{
    void visit(Cat m);
    void visit(Dog m);
    void visit(Cow m);
}

И реализации млекопитающих:

public class Cat implements Mammal
{
    public void accept(MammalVisitor visitor)
    {
        visitor.visit(this);
    }
}

Мы предположим, что Dog и Cow реализованы идентично Cat

Теперь предположим, что мой посетитель:

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");
    }
}

И я печатаю результат в stdio:

Mammal m = MammalFactory.getMammal();
MammalPrinter mp = new MammalPrinter(System.out);
m.accept(mp);

Однако, MammalPrinter выше синтаксически некорректен, потому что Appendable.append(String) выбрасывает java.io.IOException. Я не могу объявить бросок для каждого метода посещения, потому что он не объявлен в интерфейсе посетителя.

Решения, которые я рассмотрел:

  • Объявите throws IOException на Mammal.accept(), все три MammalVisitor.visit() и все три MammalPrinter.visit()
    • Очень сомнительно: интерфейсы Mammal и MammalVisitor теперь знают о своем потенциальном использовании с использованием IO, что противоречит целям использования шаблона посетителя.
  • Объявите throws Throwable на Mammal.accept() и всех трех MammalVisitor.visit() и объявите throws IOException на всех трех MammalPrinter.visit()
    • Лучше, чем указанное решение: Mammal и MammalVisitor теперь являются агностиками. Тем не менее, они также теперь громоздки для использования: посетители, которые не исключают исключений, по-прежнему вынуждены обрабатывать Throwable из метода accept().

У меня есть два других решения, которые я одобряю выше, с которыми я сам отвечу на мой пост. Я хотел бы видеть, какой из них предпочитает сообщество в целом.

Ответ 1

Вы можете поймать проверенные исключения и бросить их в непроверенные исключения. См. Например, как Spring преобразует исключения JDBC или JMS в непроверенные.

Ответ 2

Я собирался упомянуть о непроверенном завершенном подходе к повторному броску, но Джиодуд избил меня. Вместо этого я предлагаю другой подход, который я называю исключением вежливости (поскольку он интегрирован в интерфейсы в качестве вежливости для разработчиков).

При проектировании интерфейсов посетителя и млекопитающих я наряда им на обработку одного исключения, выбранного пользователем. Посетитель:

public interface MammalVisitor<T extends Throwable>
{
    void visit(Cat m) throws T;
    void visit(Dog m) throws T;
    void visit(Cow m) throws T;
}

И Млекопитающее:

public interface Mammal
{
    <T extends Throwable> void accept(MammalVisitor<T> visitor) throws T;
}

И реализация млекопитающего:

public class Cat implements Mammal
{
    @Override
    public <T extends Throwable> void accept(MammalVisitor<T> visitor) throws T
    {
        visitor.visit(this);
    }
}

Собака и корова реализуются одинаково. И посетитель печати:

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");
    }
}

И использование:

Mammal m = MammalFactory.getMammal();
MammalPrinter mp = new MammalPrinter(System.out);
try
{
    m.accept(mp);
}
catch (IOException e)
{
    System.err.println("An IOException occurred");
}

Это приводит к гораздо более интуитивно понятным и простым в использовании использованию с точки зрения конечных пользователей.

С помощью этого шаблона, если у посетителя нет проверенного исключения для броска, они определяют какое-то неконтролируемое исключение как общее в своей реализации:

public class MammalPrinter implements MammalVisitor<RuntimeException>
{

Когда Mammal.accept() вызывается с указанным выше посетителем, для синтаксически правильной фиксации не требуется. Возможно, вы могли бы увеличить удобочитаемость, расширив RuntimeException под названием "NeverThrown", у которого есть частный конструктор.

Ответ 3

Вам не хватает опции, которая предназначена для внутренней обработки IOException. Параметры должны игнорировать его, сообщать об этом или реконструировать его как исключение 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);
    }
}

Ответ 4

Я полагаю, это зависит от того, хотите ли вы, чтобы на ваш клиентский код повлияли эти исключения или нет. Вы можете усвоить их в коде, обернуть их в Runtime Exceptions, как указано выше, или, альтернативно, вы можете добавить броски Exception к методам "посещения" и что-то сделать с ним или переустановить его в методе accept.

Выполнение последней опции будет подтверждать, что все, что имеет посетитель, имеет возможность создать условие исключения, и этот код вызова должен знать об этом.