Использование шаблона посетителя вместо литья

Я регулярно использую шаблон посетителя в своем коде. Когда иерархия классов имеет посетителя, я использую его как альтернативу instanceof и casting. Однако это приводит к некоторому довольно неудобному коду, который я хотел бы улучшить.

Рассмотрим надуманный случай:

interface Animal {
    void accept(AnimalVisitor visitor);
}

class Dog implements Animal {
    void accept(AnimalVisitor visitor) {
        visitor.visit(this);
    }
}

class Cat implements Animal {
    void accept(AnimalVisitor visitor) {
        visitor.visit(this);
    }
}

interface AnimalVisitor {
    default void visit(Cat cat) {};
    default void visit(Dog dog) {};
}

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

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

В этом случае я получаю довольно уродливый код:

List<Dog> dogs = new ArrayList<>();
animal.accept(new AnimalVisitor() {
    void visit(Dog dog) {
        dogs.add(dog);
    }
}
Optional<Dog> possibleDog = dogs.stream().findAny();

Я не могу назначить possibleDog Dog непосредственно внутри посетителя, потому что это не конечная переменная, следовательно, список.

Это довольно уродливое и неэффективное, просто для того, чтобы обойти требования для эффективной финальности. Меня интересовали бы идеи альтернатив.

Альтернативы, которые я рассмотрел:

Превращение посетителя в общий тип, которому может быть присвоено возвращаемое значение

interface Animal {
    <T> T accept(AnimalVisitor<T> visitor);
}

interface AnimalVisitor <T> {
    default Optional<T> visit(Dog dog) { return Optional.empty(); }
    default Optional<T> visit(Cat cat) { return Optional.empty(); }
}

Создание абстрактного посетителя, который содержит большую часть кода и может быть тривиальным, чтобы установить необязательный напрямую

abstract class AnimalCollector implements AnimalVisitor <T> {
    private Optional<T> result = Optional.empty;

    protected void setResult(T value) {
        assert !result.isPresent();
        result = Optional.of(value);
    }

    public Optional<T> asOptional() {
        return result;
    }
}

Используйте построитель потоков вместо списка

Stream.Builder<Dog> dogs = Stream.builder();
animal.accept(new AnimalVisitor() {
    void visit(Dog dog) {
        dogs.accept(dog);
    }
}
Optional<Dog> possibleDog = dogs.build().findAny();

Но я не считаю их особенно элегантными. Они включают в себя множество шаблонов только для реализации базовой логики asA. Я использую второе решение в своем коде, чтобы сохранить его в чистоте. Есть ли более простое решение, которое мне не хватает?

Чтобы быть ясным, меня не интересуют ответы с некоторым вариантом "use instanceof and casts". Я понимаю, что это будет работать в этом тривиальном случае, но ситуации, которые я рассматриваю, имеют довольно сложное использование посетителей, которые включают в себя посещение композитов и делегатов, которые делают литье непрактичным.

Ответы

Ответ 1

AnimalCollector идею AnimalCollector на вопрос в два, я думаю, что мы можем создать что-то довольно кратким.

(Эти примеры основаны на исходном интерфейсе AnimalVisitor в вопросе - например, при void visit(Cat cat); в качестве методов).

Часть "collect to Optional" отлично работает в отдельном классе:

public class OptionalCollector<T> {
    private Optional<T> result = Optional.empty();

    public void setResult(T value) {
        result = Optional.of(value);
    }

    public Optional<T> asOptional() {
        return result;
    }
}

Еще одна возможность, хотя и является немного "посещением лямбдой" кодирования стиля. Несколько статических заводских методов позволяют легко определить посетителя без объявления методов в анонимном внутреннем классе.

Вот некоторые примеры заводских методов:

import java.util.function.Consumer;

public class AnimalVisitorFactory {
    static AnimalVisitor dogVisitor(Consumer<Dog> dogVisitor) {
        return new AnimalVisitor() {
            @Override
            public void visit(Dog dog) {
                dogVisitor.accept(dog);
            }
        };
    }

    static AnimalVisitor catVisitor(Consumer<Cat> catVisitor) {
        return new AnimalVisitor() {
            @Override
            public void visit(Cat cat) {
                catVisitor.accept(cat);
            }
        };
    }
}

Затем эти две части можно комбинировать:

import static AnimalVisitorFactory.*;

OptionalCollector<Dog> collector = new OptionalCollector<>();
animal.accept(dogVisitor(dog -> collector.setResult(dog)));
Optional<Dog> possibleDog = collector.asOptional();

Это отвлекает от того, что необходимо для случая в вопросе, но обратите внимание, что идея может быть взята немного дальше в свободном стиле API. С аналогичными dogVisitor() и catVisitor() умолчанию на интерфейсе AnimalVisitor несколько lambdas могут быть соединены вместе, чтобы построить более полного посетителя.

Ответ 2

Я знаю, что вы явно попросили решение, не использующее instanceof или cast но на самом деле я думаю, что в этом специальном случае, когда вы хотите реализовать логику для фильтрации для определенного подтипа, это может стоить рассмотреть и схож с вашим подходом, это не уродливый ИМХО:

// as mentioned in the comment above I removed the Optional return types
interface AnimalVisitor<T> {
    T visit(Dog dog);

    T visit(Cat cat);
}

public class AnimalFinder<A extends Animal> implements AnimalVisitor<A> {

    final Class<A> mAnimalClass;

    public AnimalFinder(Class<A> aAnimalClass) {
        this.mAnimalClass = aAnimalClass;
    }

    @Override
    public A visit(Dog dog) {
        if (dog != null && mAnimalClass.isAssignableFrom(dog.getClass())) {
            return mAnimalClass.cast(dog);
        } else {
            return null;
        }
    }

    @Override
    public A visit(Cat cat) {
        if (cat != null && mAnimalClass.isAssignableFrom(cat.getClass())) {
            return mAnimalClass.cast(cat);
        } else {
            return null;
        }
    }
}

Теперь вы можете просто повторно использовать AnimalFinder и предоставить интересующий вас тип в качестве аргумента:

public static void main(String[] args) {
    Animal dog = new Dog();
    Animal cat = new Cat();

    AnimalVisitor<Dog> dogFinder = new AnimalFinder<>(Dog.class);

    System.out.println(Optional.ofNullable(dog.accept(dogFinder)));
    System.out.println(Optional.ofNullable(cat.accept(dogFinder)));

    // using AnimalFinder there is actually no need to implement something like DogPrinter
    // simply use a Consumer or a lambda expression
    Optional.ofNullable(dog.accept(dogFinder)).ifPresent(d -> System.out.println("Found dog" +  d));
    Optional.ofNullable(cat.accept(dogFinder)).ifPresent(d -> System.out.println("Found dog" +  d));
}

С моей точки зрения это решение имеет ряд преимуществ:

  • Нет дублированного кода для фильтрации для разных классов животных везде в коде
  • Единственное место, где нужно добавить новый метод посещения, если будет реализован новый тип Animal
  • Он прост в использовании и легко расширяется (что не всегда верно с шаблоном посетителя)

Конечно, Морис прав. Вы можете просто заменить посетителя AnimalFinder простым Predicate (на самом деле в значительной степени тем, что делает Guava Predicates.instanceOf):

public static <A, T> Predicate<A> instanceOf(final Class<T> aClass) {
    return a -> (a != null && aClass.isAssignableFrom(a.getClass()));
}

И используйте его так, чтобы отфильтровать Optional или Stream:

System.out.println(Optional.ofNullable(dog).filter(instanceOf(Dog.class)));

Это еще более многократно используется (как правило, не только для Animal), имеет меньше дублированного кода и может использоваться всякий раз, когда вы получаете Optional или Stream.

Ответ 3

Я не думаю, что вам нужен посетитель в этом случае; это делает код избыточным. Я бы предпочел использовать селектор:

public class AnimalSelector<T extends Animal> {
    private final Class<T> clazz;

    public AnimalSelector(Class<T> clazz) {
        this.clazz = clazz;
    }

    public T select(Animal a) {
        if (clazz.isInstance(a)) {
            return clazz.cast(a);
        } else {
            return null;
        }
    }
}

Теперь вы можете написать что-то вроде:

Dog dog = new AnimalSelector<>(Dog.class).select(animal);
Cat cat = new AnimalSelector<>(Cat.class).select(animal);

Ответ 4

На самом деле вам не нужен AnimalVisitor который возвращает Void. Основываясь на вашем коде и поддерживая шаблон Visitor, я бы сделал это следующим образом.

Интерфейс Animal.

public interface Animal {

    default <T extends Animal> Stream<T> accept(AnimalVisitor visitor) {
        try {
            return visitor.visit(this).stream();
        } catch (ClassCastException ex) {
            return Stream.empty();
        }
    }
}

Собаки и кошки производят классы.

public class Dog implements Animal {

    @Override
    public String toString() {
        return "Fido";
    }
}

public class Cat implements Animal {

    @Override
    public String toString() {
        return "Felix";
    }
}

Интерфейс AnimalVisitor.

public interface AnimalVisitor<T extends Animal> {

    Optional<T> visit(T animal);
}

И все вместе.

public class AnimalFinder {

    public static void main(String[] args) {

        Animal dog = new Dog();
        Animal cat = new Cat();

        /*
         * The default/old way
         * AnimalVisitor<Dog> dogFinder = new AnimalVisitor<Dog>() {
         *    @Override
         *    public Optional<Dog> visit(Dog animal) {
         *        return Optional.of(animal);
         *    }
         * };
         *
         * Or lambda expression
         *
         * AnimalVisitor<Dog> dogFinder = (animal) -> Optional.of(animal);
         *
         * Or member reference
         */
        AnimalVisitor<Dog> dogFinder = Optional::of;

        Optional fido = dog.accept(dogFinder).findAny();
        Optional felix = cat.accept(dogFinder).findAny();

        System.out.println(fido); // Optional[Fido]
        System.out.println(felix); // Optional.empty

        felix.ifPresent(a -> System.out.printf("Found %s\n", a));
        fido.ifPresent(a -> System.out.printf("Found %s\n", a)); // Found Fido
    }
}

Ответ 5

Я опробовал общее решение для посетителей. Это на самом деле не так уж плохо - главным уродством является использование Optional<Void> когда посетителю не требуется возвращаемое значение.

Меня все еще интересуют лучшие альтернативы.

public class Visitor {
    interface Animal {
        <T> Stream<T> accept(AnimalVisitor<T> visitor);
    }

    static class Dog implements Animal {
        @Override
        public String toString() {
            return "Fido";
        }

        @Override
        public <T> Stream<T> accept(AnimalVisitor<T> visitor) {
            return visitor.visit(this).stream();
        }
    }

    static class Cat implements Animal {
        @Override
        public String toString() {
            return "Felix";
        }

        @Override
        public <T> Stream<T> accept(AnimalVisitor<T> visitor) {
            return visitor.visit(this).stream();
        }
    }

    interface AnimalVisitor<T> {
        default Optional<T> visit(Dog dog) {
            return Optional.empty();
        }

        default Optional<T> visit(Cat cat) {
            return Optional.empty();
        }
    }

    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        AnimalVisitor<Dog> dogFinder = new AnimalVisitor<Dog>() {
            @Override
            public Optional<Dog> visit(Dog dog) {
                return Optional.of(dog);
            }
        };
        AnimalVisitor<Void> dogPrinter = new AnimalVisitor<Void>() {
            @Override
            public Optional<Void> visit(Dog dog) {
                System.out.println("Found dog " + dog);
                return Optional.empty();
            }
        };
        System.out.println(dog.accept(dogFinder).findAny());
        System.out.println(cat.accept(dogFinder).findAny());
        dog.accept(dogPrinter);
        cat.accept(dogPrinter);
    }
}

Ответ 6

Как насчет этого:

interface Animal {
    <T> T accept(AnimalVisitor<T> visitor);
}

static class Dog implements Animal {
    public <T> T accept(AnimalVisitor<T> visitor) {
        return visitor.visit(this);
    }
}

static class Cat implements Animal {
    public <T> T accept(AnimalVisitor<T> visitor) {
        return visitor.visit(this);
    }
}

interface AnimalVisitor<T> {
    default T defaultReturn(){ return null; }
    default T visit(Cat cat) { return defaultReturn(); };
    default T visit(Dog dog) { return defaultReturn(); };
    interface Finder<T> extends AnimalVisitor<Optional<T>> {
        @Override default Optional<T> defaultReturn(){ return Optional.empty(); }
    }
    interface CatFinder extends Finder<Cat> {
        @Override default Optional<Cat> visit(Cat cat){
            return Optional.ofNullable(find(cat));
        }
        Cat find(Cat cat);
        CatFinder FIND = c -> c;
    }
    interface DogFinder extends Finder<Dog> {
        @Override default Optional<Dog> visit(Dog dog){
            return Optional.ofNullable(find(dog));
        }
        Dog find(Dog dog);
        DogFinder FIND = d -> d;
    }
}

Затем использовать его...

Animal a = new Cat();
Optional<Cat> o = a.accept((CatFinder)c -> {
    //use cat
    return c; //or null to get an empty optional;
});

//or if you just want to always return the cat:
Optional<Cat> o = a.accept(CatFinder.FIND);

Вы можете изменить find чтобы вернуть логическое значение, если хотите, так что оно похоже на предикат.

Ответ 7

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