Почти год назад делал тестовое задание для одной компании. Но тогда проходил собеседования сразу в несколько контор и было довольно много работы и домашних дел. Плюс, решил в своём решении показать сразу всё, на что был способен и сильно переусложнил его для тестового задания. Поэтому сделать тогда задание за два выходных дня не успел. Застрял на дедлоках и не смог быстро найти места, где они возникают.
То решение было стыдно показывать ревьюеру. Но ещё хуже было не показать ничего. Поэтому у меня весь год “чесались руки” сделать его как следует. И вот, наконец, этот час настал.
Решение получилось без изысков. Я знаю, что многое в нём можно доделать и улучшить. Но, выполняя задачу, своей цели я достиг, и решение на правильных данных работает быстро и считает, как надо.
Немного отойдя от темы статьи, хочу сказать, что после одного неудачно опыта выполнения тестового задания, когда я в результате не получил никакого фидбэка вообще, я, как правило, отказываюсь выполнять тестовые задания. Даже если вакансия выглядит для меня очень интересной.
В тот раз я решил сделать исключение, так как собеседование проводил технически очень грамотный специалист, который не только спрашивал меня, но и, увлёкшись дискуссией, рассказывал сам. При этом рассказывал интересно и такие полезные вещи, о которых не пишут в учебниках и не рассказывают на конференциях. То собеседование произвело на меня сильное впечатление и я сам не заметил, как согласился сделать тестовое, чтобы применить несколько практических советов, которые услышал от более умудрённого опытом коллеги.
Возвращаюсь к теме статьи. Если коротко, то задание звучало так: написать программу, которая будет парить данные из файла и для разных данных (разных инструментов сделок) считать разные статистики. Текст задания можно посмотреть в моём GitHub-репозитории.
Решение задачи состоит из следующих частей:
- парсер файлов. Как понятно из названия, его задача - открыть файл и распарсить его содержимое;
- модель - представляет собой распарсенные данные;
- очереди - содержат распарсенные данные для разных инструментов (число очередей равно числу инструментов, для которых необходимо посчитать статистику);
- калькуляторы статистики - рассчитывают статистику для разных инструментов сделок;
- класс, содержащий основную связующую логику приложения.
Processor.
Класс с основной логикой приложения. В нём мы создаём объекты классов нашего приложения, очереди для данных для калькуляторов, защёлку CountDownLatch и пул потоков, в которых параллельно будут исполняться: парсер и калькуляторы. После завершения выполнения своих задач парсером и калькуляторами, производится вывод результатов в консоль.
Parser.
Построчно считывает данные из файла и складывает их в очереди. После завершения своей работы, помещает в каждую очередь маркерную модель, которая будет сигнализировать потребителям данных, что новых даных не будет и пора завершать работу. Также по завершении своей работы Parser открывает защёлку CountDownLatch - это сигнал для начала работы калькулятором, считающим статистику по десяти самым последним по дате данным инструмента №3.
Очереди.
Две очереди являются экземплярами LinkedBlockingQueue.
Если при вызове метода put() такой очереди она была заполнена полностью, выполнение метода блокируется в ожидании, когда в очереди появится свободное место.
Если при вызове метода take() такой очереди она была пуста, выполнение метода блокируется в ожидании, когда в очереди появится хотя бы один элемент.
Очередь BoundedPriorityBlockingQueue является кастомной реализацией блокирующей очереди с приоритетом. Её задача - хранить в себе только 10 записей. Приоритет на помещение в эту очередь имеют последние по дате данные для инструмента №3. Приоритет определяется с помощью метода compareTo(), реализованного в модели Quotation.
Калькуляторы.
AverageValueByMonthCalculator - считает среднее значение цены за определённый месяц.
AverageValueCalculator - считает среднее значение цены за всё время.
LastTenSumCalculator - считает сумму последних десяти по дате цен.
Калькуляторы считают данные каждый из своей очереди. И прекращают работу после получения из своих очередей маркерных данных.
Калькулятор LastTenSumCalculator начинает свои расчёты после того, как Parser опустит защёлку CountDownLatch, что будет означать, что в очереди лежат последние 10 по дате данные для инструмента №3 и помещение туда новых данных не предвидится.
Благодаря такой реализации, парсинг данных и подсчёт статистики производятся параллельно и практически одновременно.
Потоковая обработка данных при помощи блокирующих очередей защищает приложение от переполнения памяти. Файл для парсинга может быть какого угодно большого размера.
Завершение парсером работы путём помещения в очередь маркерных данных, защищает приложение от live lock.