Просидев на одном предприятии несколько лет, я решил поискать альтернативы. Специально не привожу детали по моей должности, квалификации и стажу, чтобы не создавать предвзятое впечатление и не влиять на объективность оценки выполнения тестового задания. По моему профилю вакансий оказалось довольно много. Откликнулся на первую попавшуюся вакансию очень близко к дому. Перезвонили в течении нескольких часов, обрисовали буквально в двух словах чем занимается контора (обмен данными между системами разных уровней) и предложили сделать тестовое задание. Выполнив задание примерно за сутки, я его отправил и через пару часов получил ответ: «задание Вы выполнили действительно отвратительно, халтурно» и отказ от дальнейших комментариев. По месту своей основной работы я много раз выполнял очень разные задания от очень разных людей, но такого ответа никогда не было даже близко. Что же тут произошло?
Поскольку я не принимал никаких обязательств по неразглашению, привожу задание полностью. Обратите внимание, никаких дополнительных сведений не предоставлено!
Во вложение класс C#, который предлагается реализовать. Описание методов - в xml-комментах. Обращаю Ваше внимание, что класс должен быть эффективным и не использовать много памяти и ресурсов даже тогда, когда в расписании задано много значений. Например очень много значений с шагом в одну миллисекунду.
Вложенный в задание файл schedule.cs
using System; namespace Test { /// <summary> /// Класс для задания и расчета времени по расписанию. /// </summary> public class Schedule { /// <summary> /// Создает пустой экземпляр, который будет соответствовать /// расписанию типа "*.*.* * *:*:*.*" (раз в 1 мс). /// </summary> public Schedule() { } /// <summary> /// Создает экземпляр из строки с представлением расписания. /// </summary> /// <param name="scheduleString">Строка расписания. /// Формат строки: /// yyyy.MM.dd w HH:mm:ss.fff /// yyyy.MM.dd HH:mm:ss.fff /// HH:mm:ss.fff /// yyyy.MM.dd w HH:mm:ss /// yyyy.MM.dd HH:mm:ss /// HH:mm:ss /// Где yyyy - год (2000-2100) /// MM - месяц (1-12) /// dd - число месяца (1-31 или 32). 32 означает последнее число месяца /// w - день недели (0-6). 0 - воскресенье, 6 - суббота /// HH - часы (0-23) /// mm - минуты (0-59) /// ss - секунды (0-59) /// fff - миллисекунды (0-999). Если не указаны, то 0 /// Каждую часть даты/времени можно задавать в виде списков и диапазонов. /// Например: /// 1,2,3-5,10-20/3 /// означает список 1,2,3,4,5,10,13,16,19 /// Дробью задается шаг в списке. /// Звездочка означает любое возможное значение. /// Например (для часов): /// */4 /// означает 0,4,8,12,16,20 /// Вместо списка чисел месяца можно указать 32. Это означает последнее /// число любого месяца. /// Пример: /// *.9.*/2 1-5 10:00:00.000 /// означает 10:00 во все дни с пн. по пт. по нечетным числам в сентябре /// *:00:00 /// означает начало любого часа /// *.*.01 01:30:00 /// означает 01:30 по первым числам каждого месяца /// </param> public Schedule(string scheduleString) { } /// <summary> /// Возвращает следующий ближайший к заданному времени момент в расписании или /// само заданное время, если оно есть в расписании. /// </summary> /// <param name="t1">Заданное время</param> /// <returns>Ближайший момент времени в расписании</returns> public DateTime NearestEvent(DateTime t1) { } /// <summary> /// Возвращает предыдущий ближайший к заданному времени момент в расписании или /// само заданное время, если оно есть в расписании. /// </summary> /// <param name="t1">Заданное время</param> /// <returns>Ближайший момент времени в расписании</returns> public DateTime NearestPrevEvent(DateTime t1) { } /// <summary> /// Возвращает следующий момент времени в расписании. /// </summary> /// <param name="t1">Время, от которого нужно отступить</param> /// <returns>Следующий момент времени в расписании</returns> public DateTime NextEvent(DateTime t1) { } /// <summary> /// Возвращает предыдущий момент времени в расписании. /// </summary> /// <param name="t1">Время, от которого нужно отступить</param> /// <returns>Предыдущий момент времени в расписании</returns> public DateTime PrevEvent(DateTime t1) { } } }
Если коротко, то предлагается реализовать парсинг строки, определяющей расписание событий, а также пару методов получения времени события, ближайшего к указанному времени.
Меня сразу насторожило неконкретное требование «класс должен быть эффективным и не использовать много памяти и ресурсов», ведь понятия «эффективно» и «много» каждый понимает по-своему. Чтобы грубо не нарушать эти требования, я решил сразу отметать плохо зарекомендовавшие себя в плане эффективности практики типа регулярных выражений и частого выделения объектов в «куче» (heap) чтобы не нагружать сборщик мусора. А также предусмотреть потенциальные пути оптимизации на случай если нужно будет улучшать быстродействие или уменьшать выделяемую память. Добиваться каких то экстремальных показателей в плане оптимизации нет смысла, потому что это приведёт к снижению такого важного показателя как поддерживаемость кода, а будет ли от это��о польза — непонятно, поскольку неизвестны условия эксплуатации. На случай будущего сравнения разных оптимизаций, сразу добавил в проект бенчмарки.
Главное, на чём я решил сосредоточиться при выполнении задания — аккуратность обращения с календарём. Ведь, как известно, наш Григорианский календарь является нерегулярным. Все знают, что не каждый год содержит 365 дней и не каждый месяц содержит 31 день. В дополнение к этому, не каждая минута содержит 60 секунд. Не говоря уже о введениях/отменах перехода на зимнее время. Поэтому сразу было решено отказаться от арифметических операций с временами и датами и использовать для этого только библиотечные методы в классах DateTime или DateTimeOffset.
Первым делом написал модульные тесты используя примеры, указанные заказчиком. Также добавил от себя несколько тестов по граничным значениям. Хотя сделать тесты мог бы и сам заказчик для экономии времени на тестирование кандидатов.
Перебирая возможные способы реализации, понял, что это можно делать очень долго. Учитывая объём функциональности класса в сравнении с объёмом моих типичных проектов, решил ограничить себя одним рабочим днём. В результате появилось приемлемое решение, которое не является ни экстремально плохим, ни экстремально хорошим по эффективности. Зато легко для понимая кода и (как было замечено комментирующими, не так ��ж легко, но выводы можно будет делать только когда будет предложена другая реализация) содержит простор для дальнейшей оптимизации. Для всех имеющихся циклов было оценено количество максимально возможных итераций, а также количество итераций при типичном использовании. Выделение памяти из «кучи» присутствует только при создании объекта. В методах создаются только объекты-значения, которые располагаются в стэке и бесследно исчезают при завершении метода.
Моё решение размещено на гитхабе в виде проекта Visual Studio. Я не понимаю, почему я получил оценку «отвратительно, халтурно»! И неужели сейчас принято так оценивать задания: не говорить в чём проблема, не давать направлений для дальнейшего совершенствования специалиста? Я показал проект уважаемому коллеге, он указал только на те недостатки, которые я и сам вижу и это не объясняет низкой оценки. Уважаемые специалисты, объясните, что не так с моим тестовым заданием?
Огромное спасибо всем комментаторам, вы прояснили многое. Поскольку многие указали, что надо было уточнять детали у заказчика, поясняю: я пытался, но меня принципиально не хотели соединять со специалистами, со мной общался только HR.
Суммирую, что произошло с моим тестовым заданием. Априори исключаю обман и некомпетентность самого заказчика, это отдельная тема.
У меня выявилась определённая проф.деформация. Я годами работал в исследовательском подразделении, где код надо писать быстро, потом использовать его в реальных условиях у заказчика, а потом по результатам использования он может вообще не понадобится. Переход в продакшн использовался рудиментарный. Заказчик относился лояльно и даже порой требовал, чтобы как то заработало побыстрее, хоть и с недочётами. Код поддерживал только один человек — его автор. Это не значит, что я писал плохой код. Но, конечно, к некоторым из правил, применяемых в серьёзных фирмах по разработке софта, я просто не стремился.
Я неверно оценил чего от меня ожидает заказчик. Я оценивал буквально, по фразам в задании. Соответственно, предположил, что в приоритете — высокая оптимизация по скорости и памяти. Как заметили тут в комментариях, скорее всего ожидалось лишь «избегать лишних созданий коллекций, ставить Capacity по возможности и не конкатенировать строки в цикле».
Я недостаточно обосновал принятые решения, такие как отказ от регулярок и парсинг числа кустарным методом. Собственно, для меня образцом всегда был и остаётся код самого .Net. И такие решения я смотрю именно там. Наверное, надо было написать в комментариях побольше обоснований и возможных альтернатив.
Основная претензия к моему решению. «Код плохо читаемый из за множества вложенных If, я такое тоже не люблю и на проектах стараюсь избегать если можно без них». Я не думал, что количество вложений if считается таким уж страшным злом если строки короткие и понятные. Я добивался цели удобно увидеть алгоритм целиком на одном экране. Для сравнения можете посмотреть метод GetTimeAfter(), который делает примерно тоже самое в библиотеке Quartz. У них максимально 7 отступов, у меня — 11 потому что добавляются доли секунды. Но их код размазан на много экранов и понять алгоритм очень трудно.
Единственный однозначный мой недочёт — не объединил два похожих метода в один с дополнительным параметром.