Вопрос по java, asynchronous, callback, design-patterns, nested – Шаблон Java для вложенных обратных вызовов?

40

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

public interface Request {} 

public interface Response {} 

public interface Callback<R extends Response> {
    void onSuccess(R response);
    void onError(Exception e);
}

Существуют различные парные реализацииRequest а такжеResponse интерфейсы, а именноRequestA + ResponseA (предоставляется клиентом),RequestB + ResponseB (используется внутри службы) и т. д.

Поток обработки выглядит так:

Sequence diagram showing nested callbacks.

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

До сих пор я пробовал два подхода к кодированию этого на Java:

anonymous classes: gets ugly quickly because of the required nesting inner classes: neater than the above, but still hard for another developer to comprehend the flow of execution

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

Будет лиRxJava Помогите? fifth
@Rob I: к сожалению, неблокирование является обязательным. Andrew Swan
Я знаю, что вы сказали, что неблокирование было необходимостью, но есть ли способ пересмотреть это? Например, не могли бы вы создать второй поток, блокирующий каждый запрос? Я мог видеть, что код был очень ясным с осторожностью. Rob I
Автономные операции все еще будут в некоторой форме классов, анонимных или нет, но все же физически присутствовать. придется ждать, пока проект лямбда получит более естественные конструкции для таких вещей, как вы. Я также хотел бы предположить, что вам нужно все это в рамках "обычной Java" парадигм? Не похоже, что вы хотели бы иметь внешнюю структуру оркестровки Pavel Veller
@AndrewSwan, так как реализация (не только интерфейс) не должна блокироваться, мне нравится ваша идея списка - настройте список & quot; операций & quot; (возможно,Futures?), для которого установка должна быть довольно понятной. Затем при получении каждого ответа должна быть вызвана следующая операция. С небольшим воображением это звучит какchain of responsibility. Rob I

Ваш Ответ

4   ответа
1

правильно ли я задаю вам вопрос. Если вы хотите вызвать сервис и по его завершению результат нужно передать другому объекту, который может продолжить обработку, используя результат. Вы можете посмотреть на использование Composite и Observer для достижения этой цели.

11

Поскольку реализация (не только интерфейс) не должна блокироваться, мне нравится ваша идея списка.

Составьте список «операций» (возможно,Futures?), для которого установка должна быть довольно понятной и читаемой. Затем при получении каждого ответа должна быть вызвана следующая операция.

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

public void setup() {
    this.operations.add(new Operation(new RequestA(), new CallbackA()));
    this.operations.add(new Operation(new RequestB(), new CallbackB()));
    this.operations.add(new Operation(new RequestC(), new CallbackC()));
    this.operations.add(new Operation(new RequestD(), new CallbackD()));
    startNextOperation();
}
private void startNextOperation() {
    if ( this.operations.isEmpty() ) { reportAllOperationsComplete(); }
    Operation op = this.operations.remove(0);
    op.request.go( op.callback );
}
private class CallbackA implements Callback<Boolean> {
    public void onSuccess(Boolean response) {
        // store response? etc?
        startNextOperation();
    }
}
...
Это поставило меня на правильный путь, поэтому я принял это. В конечном итоге он больше походил на связанный список, где каждая операция имела ссылку на следующую операцию, как описано в этом сообщении в блоге:seewah.blogspot.sg/2009/02/… (в моем случае код был более сложным, потому что было несколько типов запросов и ответов). Andrew Swan
2

Вы можете использовать модель вычисления актера. В вашем случае клиент, сервисы и обратные вызовы [B-D] могут быть представлены как акторы.

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

Вы, кажется, автор df4j, и в этом случае я думаю, что вы должны сообщить об этом, когда порекомендуете его. Andrew Swan
Есть ли какие-либо примеры использования df4j для решения проблемы вложенного обратного вызова, в частности? Andrew Swan
Я не рассматриваю «проблему вложенного обратного вызова» как проблема, поскольку, как только обратные вызовы представляются как акторы, вложение исчезает. У df4j есть много примеров использования актеров. Актеры - это объекты, которые работают асинхронно. Ссылки на актеров могут быть отправлены в сообщениях, так что субъект, который обрабатывает сообщение, может получить доступ к указанному субъекту. Идея df4j заключается в том, что пользователь может расширить его, чтобы он соответствовал конкретному случаю использования (скажем, создайте актероподобный класс, который реализует Callback & lt; R extends Response & gt; и другой класс с методом send (Request, Callback)).
Да, я автор df4j. Ответ обновлен.
7

На мой взгляд, наиболее естественным способом моделирования такого рода проблем являетсяFuture<V>.

Таким образом, вместо использования обратного вызова, просто верните & quot; thunk & quot ;: aFuture<Response> это представляет ответ, который будет доступен в некоторый момент в будущем.

Затем вы можете моделировать последующие шаги какFuture<ResponseB> step2(Future<ResponseA>)или используйтеListenableFuture<V> из гуавы. Тогда вы можете использоватьFutures.transform() или одна из ее перегрузок, чтобы естественным образом связать ваши функции, но при этом сохраняя асинхронную природу.

Если используется таким образом,Future<V> ведет себя как монада (на самом деле, я думаю, что она может быть квалифицирована как единая, хотя я не совсем уверен в этом), и поэтому весь процесс выглядит как IO в Haskell, как выполняется через монаду IO.

@AlexeiKaigorodov: я думаю, это безопасно, если у каждой задачи есть своиExecutor (что в основном то, что вы сказали). Это, вероятно, лучшая причина для использованияListenableFuture<V>: чтобы защитить вас от блокировки на вызовFuture.get().
Одна из проблем j.u.c.uture & lt; V & gt; является то, что Future.get () не может быть вызван из задачи, которая выполняется под j.u.c.Executor (может вызвать взаимоблокировку), только из отдельного потока.

Похожие вопросы