Подтвердить что ты не робот

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

4b9b3361

Ответ 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.