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