Как избежать накапливания обратных вызовов или "callback hell"?
Моя программа сильно использует (возможно) асинхронные вызовы, где возвращаемое значение не доступно сразу, поэтому существует много таких способов:
// A simple callback interface
public interface GetFooCallback
{
void onResult(Foo foo);
};
// A method that magically retrieves a foo
public void getFoo(long fooID, GetFooCallback callback)
{
// Retrieve "somehow": local cache, from server etc.
// Not necessarily this simple.
Foo foo = ...;
callback.onResult(foo);
}
Однако, поскольку есть много вещей, которые зависят от последнего вызова, подобного этому, они начинают накапливаться:
// Get foo number 42
fooHandler.getFoo(42, new GetFooCallback()
{
@Override
public void onResult(final Foo foo)
{
// Foo 42 retrieved, get a related Bar object
barHandler.getBar(foo.getBarID(), new GetBarCallback()
{
@Override
public void onResult(final Bar bar)
{
// Send a new Zorb(foo, bar) to server
zorbHandler.sendZorbToServer(new Zorb(foo, bar), new ZorbResponseHandler()
{
@Override
public void onSuccess()
{
// Keep dancing
}
@Override
public void onFailure()
{
// Drop the spoon
}
});
}
});
}
});
Это "работает", но он начинает чувствовать себя довольно нехорошо, когда куча продолжает расти, и трудно отслеживать, что происходит. Итак, вопрос в следующем: как мне избежать этого нагромождения? Поскольку я googled "callback hell", во многих местах предлагается RxJava или RxAndroid, но на самом деле я не нашел примеров, показывающих, как можно преобразовать что-то вроде вышеупомянутого примера в более сжатое целое.
Ответы
Ответ 1
Это спорная тема с большим количеством мнений; позвольте мне сосредоточиться на конкретном решении и попытаться утверждать, почему он лучше, чем обратные вызовы.
Решение обычно известно как будущее, обещание и т.д.; ключевым моментом является то, что функция async не выполняет обратный вызов; вместо этого он возвращает будущее/обещание, которое представляет текущее действие async. Здесь позвольте мне использовать термин Async
для представления асинхронных действий, потому что я считаю это гораздо лучшим именем.
Вместо принятия обратного вызова
void func1(args, Callback<Foo>)
возвращает вместо Async
Async<Foo> func2(args)
Async
содержит методы, которые принимают обратные вызовы при завершении, поэтому func2
можно использовать так же, как func1
func1(args, callback);
// vs
func2(args).onCompletion( callback )
В этом отношении Async
по крайней мере не хуже решения обратного вызова.
Как правило, Async не используется с обратными вызовами; вместо этого, Asyncs закованы в цепи, как
func2(args)
.then(foo->func3(...))
.then(...)
...
Первое, что нужно заметить, это то, что он плоский, в отличие от вложенности обратного вызова.
Помимо эстетических соображений, какая грань Async
? Некоторые люди утверждают, что это по сути то же самое, что и обратный вызов, просто с альтернативным синтаксисом. Однако есть большая разница.
Самый большой секрет ООП заключается в том, что вы можете использовать объекты для представления материала... Как это секрет? Разве это не ООП-101? Но на самом деле люди часто забывают об этом.
Когда у нас есть объект Async для представления асинхронного действия, наша программа может легко выполнять действия с помощью действия, например, передавать действия через API; отменить действие или установить тайм-аут; составлять несколько последовательных/параллельных действий как одно действие; и т.д. Эти вещи можно сделать в решении обратного вызова, однако это намного сложнее и неинтуитивно, потому что нет никаких осязаемых объектов, с которыми может играть программа; вместо этого понятие действия находится только в нашей голове.
Единственный реальный судья - это решение, простое приложение. Вот моя асинхронная библиотека, посмотрите и посмотрите, поможет ли она.
Ответ 2
В зависимости от конкретного варианта использования и требований могут быть разные подходы к программированию или парадигмы, которые могут быть подходящими для вашей конкретной задачи. Они могут включать различные формы Message Passing или Модели актеров, Реактивное программирование (например, с помощью RxJava, о котором вы уже упоминали), или вообще, в какой-то форме Flow-based programming. Возможно даже можно использовать Event Bus для обмена "событиями" (которые являются вычисленными результатами в вашем случае).
Однако большинство из них построены на определенных инфраструктурах, особенно в библиотеках, которые требуют, чтобы ваша система была смоделирована соответствующим образом. Например, вашим обратным вызовам, возможно, придется реализовать определенные интерфейсы, чтобы получать уведомления о (асинхронных) результатах. Кроме того, вы в каком-то месте должны будете выполнить "проводку": вам нужно будет указать, какой конкретный обратный вызов следует вызывать, когда результат станет доступным, даже если это может быть так же просто, как зарегистрировать этот обратный вызов для определенного события тип".
Конечно, можно было бы создать необходимую инфраструктуру для этого вручную. Вы можете реализовать свой класс FooHandler
соответственно:
class FooHandler
{
// Maintain a list of FooCallbacks
private List<FooCallback> fooCallbacks = new ArrayList<>();
public void addFooCallback(FooCallback fooCallback)
{
fooCallbacks.add(fooCallbacks);
}
public void getFoo(long fooID)
{
// Retrieve "somehow": local cache, from server etc.
// Not necessarily this simple.
Foo foo = ...;
publish(foo);
}
// Offer a method to broadcast the result to all callbacks
private void publish(Foo foo)
{
for (FooCallback fooCallback : fooCallbacks)
{
fooCallback.onResult(foo);
}
}
}
class BarHandler implements FooCallback
{
// Maintain a list of BarCallbacks, analogously to FooHandler
...
@Override
public void onResult(Foo foo)
{
Object id = foo.getBarID();
Bar bar = getFrom(id);
publish(bar);
}
}
Чтобы вы могли собрать свою структуру обратного вызова с кодом, который выглядит следующим образом:
FooHandler fooHandler = new FooHandler();
FooCallback fooCallback = new BarHandler();
fooHandler.addFooCallback(fooCallback);
...
barHandler.add(new MyZorbResponseHandler());
Это в основном сводится к тому, чтобы не передавать обратные вызовы как параметры метода, а вместо этого поддерживать их в выделенном списке. Это, по крайней мере, упрощает и упрощает проводку. Но это все равно сделало бы общую структуру скорее "жесткой", а не так слабо связанной, как может быть, с выделенной инфраструктурой, которая моделирует эту концепцию "слушателя" и обмена информацией в более абстрактной форме.
Если ваша основная цель заключалась в том, чтобы избежать "укладки", о котором вы говорили, с точки зрения удобочитаемости и ремонтопригодности кода (или просто: уровня отступов), одним из подходов может быть просто извлечение создания новых экземпляров обратного вызова в полезные методы.
Хотя это, безусловно, не является заменой полноценной, сложной архитектуры передачи сообщений, вот небольшой пример того, как это может выглядеть на основе кода, который вы предоставили:
class CompactCallbacks
{
public static void main(String[] args)
{
FooHandler fooHandler = null;
BarHandler barHandler = null;
ZorbHandler zorbHandler = null;
fooHandler.getFoo(42,
createFooCallback(barHandler, zorbHandler));
}
private static GetFooCallback createFooCallback(
BarHandler barHandler, ZorbHandler zorbHandler)
{
return foo -> barHandler.getBar(
foo.getBarID(), createGetBarCallback(zorbHandler, foo));
}
private static GetBarCallback createGetBarCallback(
ZorbHandler zorbHandler, Foo foo)
{
return bar -> zorbHandler.sendZorbToServer(
new Zorb(foo, bar), createZorbResponseHandler());
}
private static ZorbResponseHandler createZorbResponseHandler()
{
return new ZorbResponseHandler()
{
@Override
public void onSuccess()
{
// Keep dancing
}
@Override
public void onFailure()
{
// Drop the spoon
}
};
}
}
//============================================================================
// Only dummy classes below this line
class FooHandler
{
public void getFoo(int i, GetFooCallback getFooCallback)
{
}
}
interface GetFooCallback
{
public void onResult(final Foo foo);
}
class Foo
{
public int getBarID()
{
return 0;
}
}
class BarHandler
{
public void getBar(int i, GetBarCallback getBarCallback)
{
}
}
interface GetBarCallback
{
public void onResult(final Bar bar);
}
class Bar
{
}
class ZorbHandler
{
public void getZorb(int i, GetZorbCallback getZorbCallback)
{
}
public void sendZorbToServer(Zorb zorb,
ZorbResponseHandler zorbResponseHandler)
{
}
}
interface GetZorbCallback
{
public void onResult(final Zorb Zorb);
}
class Zorb
{
public Zorb(Foo foo, Bar bar)
{
}
public int getBarID()
{
return 0;
}
}
interface ZorbResponseHandler
{
void onSuccess();
void onFailure();
}
Ответ 3
Я не уверен, поможет ли это, но здесь идея дизайна, которая может вдохновить на решение. Вдохновение исходит от Python Twisted. Конечный результат будет выглядеть следующим образом (объяснение следует):
Generator<Deferred> theFunc = Deferred.inlineCallbacks(new Generator<Deferred>() {
public void run() throws InterruptedException {
Foo foo = (Foo)yield(fooHandler.getFoo(42));
Bar bar = (Bar)yield(barHandler.getBar(foo.getBarID());
try {
yield(zorbHandler.sendZorbToServer(new Zorb(foo, bar));
} catch (Exception e) {
// Drop the sooon
return;
}
// Keep dancing
}
});
theFunc();
Обратите внимание на отсутствие вложенности и обратного вызова ада.
Чтобы объяснить, я объясню, как это будет работать в Twisted.
С помощью Twisted ваш код будет выглядеть примерно так:
@defer.inlineCallbacks
def the_func():
foo = yield fooHandler.getFoo(42)
bar = yield barHandler.getBar(foo.getBarID())
try:
yield zorbHandler.sendZorbToServer(Zorb(foo, bar))
# Keep dancing
except Exception:
# Drop the spoon
the_func()
В этом фрагменте the_func
на самом деле generator, который дает Отложенные объекты. fooHandler.getFoo
вместо того, чтобы принимать обратный вызов и затем вызывать обратный вызов с результатом, вместо этого возвращает объект "Отложен". Обратные вызовы могут быть добавлены к объекту "Отложен", который вызывается при срабатывании объекта. Таким образом, вместо getFoo
выглядит примерно так:
def getFoo(self, value, callback):
# do some stuff and then in another thread
doSomeStuffInThread(value, lambda foob: callback(foob))
# Usage:
getFoo(42, myCallback)
Он выглядит следующим образом:
def getFoo(self, value):
deferred = Deferred()
doSomeStuffInThread(value, lambda foob: deferred.callback(foob))
return deferred
deferred = getFoo(42)
deferred.addCallback(myCallback)
Отсрочки кажутся похожими, если не то же самое, что CompletableFutures (сравните addCallback
с thenApply
).
Затем defer.inlineCallbacks
магически связывает все обратные вызовы и все отложенные действия, делая что-то вроде следующего:
- Запустите генератор, получив первый отложенный.
- Добавить обратный вызов первого Отложенного, который берет результат и отправляет результат в генератор.
- Повторяйте со следующим отложенным и следующим результатом и так далее, пока генератор не будет исчерпан.
Возможно, вы можете реализовать что-то подобное в Java - см. этот ответ для эквивалента генератора (вам нужно будет изменить yield
для возврата значений), а Скрученный исходный код для реализации defer.inlineCallbacks
.