Некоторое время назад мы создавали распределенную серверную службу, в которой несколько экземпляров службы отправляли бы запросы друг другу для координации разделения работы и доступности. Мы были обеспокоены рядом потенциальных препятствий, в том числе перегрузкой приемников и перегруженностью нашего сетевого трафика. Мы также поняли, что в этом случае немедленная согласованность не была главным приоритетом – мы могли бы потерпеть несколько секунд задержки, и до тех пор, пока мы в конечном итоге будем согласованы, система будет вести себя так, как мы ожидали.
Мы черпали вдохновение для решения этой проблемы из механизмов отмены в веб-разработке. В Интернете этот метод используется для таких функций, как автозаполнение или предложения в строках поиска. Как правило, вы на самом деле не хотите, чтобы эти компоненты выполняли запросы при каждом нажатии клавиши, особенно если пользователь знает, что он вводит, и печатает много, поэтому решение состоит в том, чтобы отменить запросы.
Что такое Осуждение?
Название “дебаунс” на самом деле происходит от разработчиков оборудования, которым нужен был точный способ определить, нажал ли кто-то кнопку, потому что физическая кнопка будет немного отскакивать от нее, поэтому похоже, что они нажали кнопку, отпустили ее и снова нажали очень быстро … но я отвлекся.
По сути, мы ограничиваем количество выполняемых запросов. Но существует множество вариантов того, что может означать “ограничение скорости”: выполняется ли первый запрос сразу? Помещаются ли последующие запросы в очередь или просто отбрасываются? Если в настоящее время запрос находится в очереди, и поступает еще один запрос, будет ли он удален? Тоже в очереди? Продлевается ли тайм-аут?
Если вы будете искать значение “разоблачение”, вы, как правило, найдете любую их комбинацию.
Для наших целей мы остановились на следующем поведении:
Когда мы делаем запрос:
- Если это не было сделано в течение последних
миллисекунд задержкимиллисекунд (и вызов не поставлен в очередь), то запрос выполняется немедленно - Если есть недавний вызов, то мы проверяем, не стоит ли он в очереди
- Если вызов стоит в очереди, мы отбрасываем текущий вызов
- Если нет, мы ставим текущий вызов в очередь для вызова через
миллисекунды задержкимиллисекунды проходить
Если вы представите, что запросы будут поступать на временной шкале, где наша задержка составляет 10 мс, это будет выглядеть примерно так:
Это имеет приятный эффект, заключающийся в том, что первый запрос всегда выполняется немедленно, и мы по-прежнему остаемся в пределах наших ограничений по тарифам.
Запросы на Java
В Vena все наши серверные сервисы написаны на Java 8, что добавляет дополнительный уровень сложности. Обычно выполнение запроса в Java представляет собой простой вызов метода. И если бы мы реализовывали это в JavaScript, это не было бы проблемой – в JavaScript есть первоклассные функции, и мы могли бы просто передавать ссылки на функции, без проблем. На Java мы немного более ограничены.
В Java все является объектом, даже вызовы функций. Мы знали, что метод, который мы вызывали для отправки нашего запроса API, не будет принимать никаких параметров, что немного упростило задачу; это означало, что мы могли использовать интерфейс Runnable .
Интерфейс Runnable изначально был разработан для многопоточных вычислений: вы бы создали объект с методом run() , и вы могли бы передать этот объект отдельному потоку и вызвать метод run() в этом втором потоке. Для наших целей, однако, это эффективный интерфейс для представления вызова функции, который не принимает аргументов, выполняет некоторые действия (т.Е. имеет побочный эффект) и не возвращает никаких значений.
Итак, мы собираемся создать Отмененный запускаемый . Он принимает Работоспособный , а также реализует Запускаемый сам интерфейс, что означает, что он может быть заменой Работоспособный мы даем это.
Хватит теории, давайте покажем немного кода. Полный курс можно найти по адресу эта суть .
Это может показаться многовато, но в середине есть большой комментарий, занимающий массу места, ха-ха. Давайте пройдемся по нему шаг за шагом.
Вверху у нас есть несколько полей экземпляра и класса:
Верхняя строка определяет регистратор, поэтому мы можем контролировать уровни журналов и перенаправлять ведение журнала по классам.
Следующие несколько строк просто определяют некоторые константы. У нас есть ScheduledExecutorService , который поставляется из пакета java.util.concurrent и позволяет нам запланировать выполнение определенного запускаемого через определенное время. Затем у нас есть наша операция для запуска, имя операции, чтобы мы могли отслеживать ее для регистрации, и delayMillis , который контролирует количество времени, которое нам нужно ждать между вызовами. Они являются окончательными , потому что, как только мы получим их в конструкторе, они никогда не изменятся.
Затем у нас есть пара других изменяемых полей. Мы будем использовать их для отслеживания нашего расписания.
У нас также есть конструктор, который устанавливает эти поля, и имеет приятный сочный JavaDoc, описывающий поведение этого кода. Всегда комментируйте свой код, дети!
Фактическое значение этого класса находится в методе run() :
Этот метод синхронизирован , что означает, что одновременно его может вызывать только один поток. Это помогает нам избежать условий гонки, если несколько потоков вызывают run() в первый раз одновременно .
Поведение таково, как мы описали выше: если у нас есть вызов в очереди, мы игнорируем текущий вызов и ничего не делаем. В противном случае мы проверяем, следует ли запускать его сейчас или запланировать его запуск позже (в фоновом потоке), а затем мы делаем именно это, отслеживая Последнее время и ставится в очередь соответственно.
Еще одна вещь, которую следует отметить, – это необычный синтаксис при вызове планировщика. Если вы раньше не видели Java 8, синтаксис this::запланированный запуск покажется вам странным. Это в основном означает “текущий объект запланированный запуск метод”. Давайте взглянем на этот метод:
Это очень похоже на описанный выше случай, когда мы только что вызвали run() напрямую, но сообщение журнала немного отличается, и мы также снимаем флаг isQueued .
Все остальные методы в классе небольшие:
Первый – это наша проверка, чтобы узнать, разрешено ли нам запускать в текущее время, а остальные 2 – это простые оболочки вокруг нашего планировщика и интерфейса времени, чтобы мы могли правильно протестировать этот класс.
Однако, прежде чем мы поговорим о тестировании, давайте поговорим о том, как мы на самом деле использовали это Осужденный Управляемый .
В нашем вызывающем коде была операция под названием волонтер() . Детали здесь не важны – все сводилось к отправке запроса API, и, как я уже упоминал в начале, мы были согласны с тем, что он не срабатывал сразу в 100% случаев, а вместо этого был ограничен по скорости.
Раньше этот класс выглядел бы так:
Нам пришлось немного раздуть этот класс обслуживания и немного изменить его внешний вид:
Итак, вместо того, чтобы просто привязывать метод к классу, нам нужно взять этот метод, обернуть его в Отмененный запускаемый , а затем сохраните ссылку на него, чтобы мы могли вызвать его позже. Мы также скрываем фактический вызов API в устаревшем методе с намеренно уродливым именем volunteer_yes Я знаю, что я делаю так что это выскакивает в обзорах кода, и сразу становится очевидно, что это не просто какой-то другой метод. Затем наш метод volunteer() вызывает run() для запуска, который выполняет свою отмененную функцию: вызывает volunteer_yesIKnowWhatImDoing
Тестирование Времени и пространства
Итак, теперь, когда мы увидели, как выглядит наш вызывающий код, нам нужно ответить еще на один вопрос: как мы это тестируем? Код, который взаимодействует со временем, как известно, сложно тестировать, поскольку модульные тесты не гарантируют, что они будут занимать одинаковое количество времени от запуска до запуска. Кроме того, код, который взаимодействует с отдельными потоками, как правило, также сложнее тестировать.
И здесь у нас есть и то, и другое.
Для начала давайте взглянем на переменные экземпляра вверху.
Мы используем Java AtomicInteger как простой объект-контейнер. Он представляет целое число и определяет для него метод incrementAndGet() , который будет увеличивать значение целого числа, поэтому значение целого числа будет точно соответствовать количеству раз, когда вызывался этот метод.
У нас также есть наш DebouncedRunnable , вокруг метода incrementAndGet() . Это нормальный отмененный запускаемый , но мы также собираемся использовать Mockito шпионить за ним, что означает, что мы можем перехватывать вызовы методов по своему усмотрению.
И, наконец, Список<Запускаемый> для хранения всех наших операций в очереди.
Наша настройка класса (с пометкой @Перед в JUnit) также интересна. Проницательные читатели заметили бы, что выше я определил две однострочные функции-оболочки, которые, как я сказал, помогут нам в модульном тестировании, и вот как:
do Answer() – это метод из Mockito для запуска которого требуется лямбда и Шпион . По сути, поскольку мы следим за нашим Отмененным запускаемым , вот как мы указываем, что хотим перехватить метод schedule() . Вместо вызова метода real schedule() (который является однострочным, который помещает вещи в отдельный поток), мы отслеживаем вызовы и сохраняем их в нашем массиве.
Есть еще один последний вспомогательный метод, который делает наши тесты намного более удобочитаемыми:
Мы используем тот же метод do Return() , что и выше, но он работает с методом getCurrentTimeMillis() . Обычно это поступает прямо в систему, но мы перехватываем его и вручную указываем возвращаемое значение, эффективно позволяя нам замораживать время и манипулировать им по своему усмотрению.
Теперь, когда у нас есть все это, мы можем писать такие тесты, как этот:
или как это, когда мы проверяем, что даже если есть задержка в планировании и между нашим последним запуском и сейчас прошло достаточно времени, того факта, что вызов стоит в очереди, достаточно, чтобы мы сбросили вызов:
Полный тест JUnit с еще несколькими тестовыми примерами, которые я не рассматривал, доступен в суть здесь .
Написание описательных модульных тестов, подобных этому, действительно приносит удовлетворение. Как только вы найдете правильный уровень абстракции и сможете описать требуемую настройку, действия, которые вы хотите предпринять, и ожидаемые результаты, вам станет намного проще писать тесты и укреплять уверенность в том, что ваш код делает то, что вы хотите, чтобы он делал.
Вы придумали интересные стратегии тестирования? Приходилось решать подобные проблемы? Напишите мне в твиттере или оставьте комментарий ниже.
Оригинал: “https://dev.to/mustafahaddara/how-to-building-a-debouncer-in-java-18g3”