Статьи

API дизайн и производительность

Когда вы разрабатываете новый API, вы должны принять много решений. Эти решения основаны на ряде принципов проектирования. Джошуа Блох суммировал некоторые из них в своей презентации «Как разработать хороший API и почему это важно» . Основные принципы, которые он упоминает:

  • Легко учить
  • Легко использовать
  • Трудно злоупотреблять
  • Легко читать и поддерживать код, который его использует
  • Достаточно мощный, чтобы удовлетворить требования
  • Легко продлить
  • Подходит для аудитории

Как видно из приведенного выше списка, Джошуа Блох делает упор на удобочитаемость и использование. Точка, которая полностью отсутствует в этом списке — это производительность. Но может ли производительность повлиять на ваши дизайнерские решения вообще?

Чтобы ответить на этот вопрос, давайте попробуем разработать простой вариант использования в виде API и измерить его производительность. Затем мы можем посмотреть на результаты и решить, влияют ли соображения производительности на API или нет. В качестве примера мы берем классический вариант загрузки списка клиентов из какого-либо сервиса / хранилища. Мы также хотим учесть тот факт, что не всем пользователям разрешено выполнять эту операцию. Следовательно, нам нужно будет выполнить некоторую проверку разрешений. Чтобы реализовать эту проверку и вернуть эту информацию обратно вызывающей стороне, у нас есть несколько способов сделать это. Первая попытка будет выглядеть так:

1
List<Customer> loadCustomersWithException() throws PermissionDeniedException

Здесь мы моделируем явное исключение для случая, когда вызывающая сторона не имеет права получать список клиентов. Метод возвращает список объектов Customer, в то время как мы предполагаем, что пользователь может быть извлечен из некоторого контейнера или реализации ThreadLocal и не должен передаваться каждому методу.
Сигнатура метода, приведенная выше, проста в использовании и злоупотребляет. Код, который использует этот метод, также легко читается:

1
2
3
4
5
6
try {
        List<Customer> customerList = api.loadCustomersWithException();
        doSomething(customerList);
    } catch (PermissionDeniedException e) {
        handleException();
    }

Читатель сразу же видит, что список клиентов загружен и что мы выполняем некоторые последующие действия только в том случае, если мы не получаем PermissionDeniedException. Но с точки зрения исключений производительности стоит некоторое время процессора, так как JVM должна остановить нормальное выполнение кода и пройти вверх по стеку, чтобы найти позицию, где выполнение должно быть продолжено. Это также чрезвычайно сложно, если мы рассмотрим архитектуру современных процессоров с их энергичным выполнением последовательностей кода в конвейерах. Так что было бы лучше с точки зрения производительности представить другой способ информирования звонящего о пропущенном разрешении?

Первой идеей было бы создать другой метод, чтобы проверить разрешение перед вызовом метода, который в конечном итоге выдает исключение. Код вызывающего абонента будет выглядеть так:

1
2
3
4
if(api.hasPermissionToLoadCustomers()) {
        List<Customer> customerList = api.loadCustomers();
        doSomething(customerList);
    }

Код по-прежнему читабелен, но мы ввели еще один вызов метода, который также стоит времени выполнения. Но теперь мы уверены, что исключение не будет брошено; следовательно, мы можем опустить блок try / catch. Этот код теперь нарушает принцип «Простота использования», так как теперь мы должны вызывать два метода для одного варианта использования вместо одного. Вы должны обратить внимание, чтобы не забыть дополнительный вызов для каждой операции поиска. Что касается всего проекта, ваш код будет загроможден сотнями проверок разрешений.

Другая идея для преодоления исключения — предоставить пустой список для вызова API и позволить реализации заполнить его. Тогда возвращаемое значение может быть логическим значением, указывающим, было ли у пользователя разрешение на выполнение операции или список пуст, потому что клиенты не были найдены. Поскольку это звучит как программирование на C или C ++, где вызывающая сторона управляет памятью структур, которые используют вызываемые, такой подход стоит создание пустого списка, даже если у вас нет разрешения на получение списка вообще:

1
2
3
4
5
List<Customer> customerList = new ArrayList<Customer>();
    boolean hasPermission = api.loadCustomersWithListAsParameter(customerList);
    if(hasPermission) {
        doSomething(customerList);
    }

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

1
2
3
4
CustomerList customerList = api.loadCustomersWithReturnClass();
    if(customerList.isUserHadPermission()) {
        doSomething(customerList.getCustomerList());
    }

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

После введения этих различных подходов настало время измерить их производительность: один раз для случая, когда вызывающий абонент имеет разрешение, и один раз для случая, когда вызывающий абонент не имеет необходимого разрешения. Результаты в следующей таблице показаны для первого случая с 1.000.000 повторений:

измерение Время [мс]
testLoadCustomersWithExceptionWithPermission 33
testLoadCustomersWithExceptionAndCheckWithPermission 34
testLoadCustomersWithReturnClassWithPermission 41
testLoadCustomersWithListAsParameterWithPermission 66

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

измерение Время [мс]
testLoadCustomersWithExceptionNoPermission 1187
testLoadCustomersWithExceptionAndCheckNoPermission 5
testLoadCustomersWithReturnClassNoPermission 4
testLoadCustomersWithListAsParameterNoPermission 5

Неудивительно, что подход, в котором выбрасывается выделенное исключение, гораздо медленнее, чем другие подходы. Масштабы этого воздействия намного выше, чем можно было ожидать раньше. Но из приведенной выше таблицы мы уже знаем решение для этого случая: просто представьте другой метод, который можно использовать для проверки наличия разрешения на случай, если вы ожидаете много вариантов использования с отказом в разрешении. Огромная разница во времени выполнения между вариантами использования с разрешением и без него может быть объяснена тем фактом, что я возвратил ArrayList с одним объектом Customer в случае, если вызывающий объект обладал разрешением; следовательно, loadCustomer () вызывает там, где немного дороже, чем в случае, если пользователь не обладает этим разрешением.

Вывод

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

Ссылка: Дизайн API и производительность от нашего партнера JCG Мартина Моиса в блоге Martin’s Developer World .