От подкласса к суперклассу и обратно - PullRequest
2 голосов
/ 18 июня 2020

Итак, представьте себе это,

Учитывая иерархию классов:

S (abstract)
A extends S
B extends S

Допустим, у нас есть цепочка связанных компонентов, обрабатывающих объекты типов A и B. Например: K1.handle(A) -> K2.handle(A) -> K3.handle(A) означает, что K1 принимает A и вызывает K2 с помощью A, который вызывает K3 с помощью A.

Аналогичным образом мы имеем: K1.handle(B) -> K2.handle(B) -> K3.handle(B)

Дело в том, что logi K2 c не зависит от суб тип S. Таким образом, мы могли бы заменить K2.handle (A) и K2.handle (B) на K2.handle (S). Но проблема в том, что в какой-то момент нам нужно выполнить проверку типа фактического типа объекта, чтобы вызвать правильный метод K3.

Как решить эту проблему, не выполняя if или switch et c. операторы, которые проверяют тип / свойства реальных объектов?

Это шаблон посетителя, который мне нужно изучить?

Чтобы дать конкретный пример случая. Допустим, K1 - это контроллер REST с разными конечными точками POST для A и B. Он десериализует полезную нагрузку и проверяет данные, а затем передает объект в K2 для дальнейшей обработки. Все шаги, включенные в K2 (и другие вызываемые им компоненты), работают только с объектами типа S. Но в самом конце в К3 нам нужно знать реальный тип S.

Ответы [ 4 ]

0 голосов
/ 18 июня 2020

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

Вы можете использовать тот факт, что K3.handle(A) - это Consumer<A>, которое вы можете легко преобразовать в Function<A,A> с помощью используя Function<A,A> handleA = a -> k3.handle(a); return a;. Это возвращает элемент ввода, набранный как A, который затем вы можете compose() с другими функциями k2::handle и k1::handle. Вы не можете сделать это как связанный вызов в одной строке, потому что это нарушает вывод типа Java, но это работает:

K k1 = new K1();
K k2 = new K2();
// construct A chain
K3A specificA = new K3A();
Function<A, A> handleA = a -> specificA.handle(a);
handleA = handleA.compose(k2::handle);
handleA = handleA.compose(k1::handle);

В этом случае

class K {
    <T> T handle(T item);
}
class K3A {
    A handle(A item) { ... }
}

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

Не уверен, что вы сочтете это более читаемым / поддерживаемым, но опять же, мне так не нравится Visitor, что я пришлось попробовать.

Вот работающий образец , а здесь вилка с обернутой версией (которая представляет собой именно тот случай, который вы представили IIU C, и без каких-либо изменение кода в S, A, B или в любом из K s).

0 голосов
/ 18 июня 2020

Указанные образцы классов:

class S {}

class A extends S {
    public int getAInfo() {
        return 1;
    }
}

class B extends S {
    public int getBInfo() {
        return 2;
    }
}

Мы можем создать интерфейс K, который использует преимущества параметра ограниченного типа следующим образом:

@FunctionalInterface
interface K<T extends S> {
    void handle(T s);

    @SafeVarargs
    static <FT extends S> K<FT> compose(K<FT> finalK, K<? super FT>... ks) {
        return s -> {
            Stream.of(ks).forEach(k -> k.handle(s));
            finalK.handle(s);
        };
    }
}

Затем мы можем использовать K.construct для создания нового экземпляра K, который будет пересылать s по всему конвейеру K s. Этот конвейер K s структурирован таким образом, что все, кроме последнего k, являются универсальными обработчиками c, принимающими любой экземпляр S. Только последний примет экземпляр подкласса специфицированного c класса S, а именно FT (сокращение от «окончательного типа»).

С этим определением K все следующие утверждения compile:

K<S> k1 = s -> System.out.println("K1 handled.");
K<S> k2 = s -> System.out.println("K2 handled.");
K<A> k3A = a -> System.out.println(a.getAInfo());
K<B> k3B = b -> System.out.println(b.getBInfo());

K.compose(k3A, k1, k2).handle(new A()); // Propagates instance of A through pipeline
K.compose(k3B, k1, k2).handle(new B()); // Propagates instance of B through pipeline

, но ad-ho c полиморфизм всех K s в конвейере, которые не последний k, все еще сохраняется, поскольку оба эти операторы также компилируются:

k1.handle(new S());
k2.handle(new S());

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

0 голосов
/ 18 июня 2020

Да, это шаблон посетителя.

Посетитель

interface K {
    void handle(A a);
    void handle(B b);
}

и K1, K2, K3 реализует его.

S нужен метод visit(K) с реализациями

class A implements S {
    public void visit(K visitor) {
        visitor.handle(this); // calls the handle(A) method
    }
}
class B implements S {
    public void visit(K visitor) {
        visitor.handle(this); // calls the handle(A) method
    }
}

Ваш список (ie ваша цепочка) - это просто List<K> с чем-то вроде

List<K> handlers = Arrays.asList(new K1(), new K2(), new K3());
S sample = new A();
for (K visitor : handlers) {
    sample.visit(visitor);
}
sample = new B();
for (K visitor : handlers) {
    sample.visit(visitor);
}

Вот рабочая версия .

0 голосов
/ 18 июня 2020

Но в самом конце в K3 нам нужно знать фактический тип S.

Таким образом, все logi c в K2.handle() предшествуют вызову K3.handle().

Кроме того, поскольку K3.handle(A) и K3.handle(B) не могут быть K3.handle(S), в соответствии с формулировкой квеста, может показаться, что K3.handle() является перегруженным методом, хотя это явно не указано в вопросе.

Один из способов сделать это - переместить общие логики c во внутренний вспомогательный метод:

public class K2 {
    public void handle(A a) {
        handleInternal(a);
        K3.handle(a);
    }
    public void handle(B b) {
        handleInternal(b);
        K3.handle(b);
    }
    private void handleInternal(S s) {
        // K2 logic here
    }
}

Или вы можете использовать ссылку на метод, разрешающую вызов в K3 для встраивания в K2 logi c, а не только в конце:

public class K2 {
    public void handle(A a) {
        handleInternal(a, K3::handle); // method reference to K3.handle(A)
    }
    public void handle(B b) {
        handleInternal(b, K3::handle); // method reference to K3.handle(B)
    }
    private <T extends S> void handleInternal(S s, Consumer<T> k3_handle) {
        // Some K2 logic here
        k3_handle.accept(s);
        // More K2 logic here
    }
}
...