Использование шаблона посетителя вместо приведения - PullRequest
0 голосов
/ 26 мая 2018

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

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

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 непосредственно внутри посетителя, потому что это не финальная переменная, поэтомусписок.

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

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

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

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

Ответы [ 7 ]

0 голосов
/ 23 сентября 2018

На самом деле вам не нужно 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();
        }
    }
}

Производные классы Dog и Cat.

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
    }
}
0 голосов
/ 23 сентября 2018

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

0 голосов
/ 21 сентября 2018

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

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);
0 голосов
/ 21 сентября 2018

Я знаю, что вы явно просили решение, не использующее 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
  • Его легко использовать и легко расширять (что не всегда верно для шаблона Visitor)

Морисверно конечно.Вы можете просто заменить посетителя AnimalFinder на простой Predicate (на самом деле это в значительной степени то, что делает Гуава 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.

0 голосов
/ 19 сентября 2018

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

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, чтобы вернуть логическое значение, если хотите, так что это какпредикат.

0 голосов
/ 19 сентября 2018

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

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

Часть «собирать в необязательные» хорошо работает и извлекается в отдельный класс:

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 несколько лямбд можно объединить для создания более полного посетителя.

0 голосов
/ 27 мая 2018

Я опробовал универсальное решение для посетителей.Это на самом деле не так уж и плохо - главное уродство - использование 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);
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...