Как стать автором
Обновить

История одного фееричного провала тестового задания на C#

.NET *C# *Карьера в IT-индустрии

Просидев на одном предприятии несколько лет, я решил поискать альтернативы. Специально не привожу детали по моей должности, квалификации и стажу, чтобы не создавать предвзятое впечатление и не влиять на объективность оценки выполнения тестового задания. По моему профилю вакансий оказалось довольно много. Откликнулся на первую попавшуюся вакансию очень близко к дому. Перезвонили в течении нескольких часов, обрисовали буквально в двух словах чем занимается контора (обмен данными между системами разных уровней) и предложили сделать тестовое задание. Выполнив задание примерно за сутки, я его отправил и через пару часов получил ответ: «задание Вы выполнили действительно отвратительно, халтурно» и отказ от дальнейших комментариев. По месту своей основной работы я много раз выполнял очень разные задания от очень разных людей, но такого ответа никогда не было даже близко. Что же тут произошло?

Поскольку я не принимал никаких обязательств по неразглашению, привожу задание полностью. Обратите внимание, никаких дополнительных сведений не предоставлено!

Во вложение класс 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.

Суммирую, что произошло с моим тестовым заданием. Априори исключаю обман и некомпетентность самого заказчика, это отдельная тема.

  1. У меня выявилась определённая проф.деформация. Я годами работал в исследовательском подразделении, где код надо писать быстро, потом использовать его в реальных условиях у заказчика, а потом по результатам использования он может вообще не понадобится. Переход в продакшн использовался рудиментарный. Заказчик относился лояльно и даже порой требовал, чтобы как то заработало побыстрее, хоть и с недочётами. Код поддерживал только один человек — его автор. Это не значит, что я писал плохой код. Но, конечно, к некоторым из правил, применяемых в серьёзных фирмах по разработке софта, я просто не стремился.

  2. Я неверно оценил чего от меня ожидает заказчик. Я оценивал буквально, по фразам в задании. Соответственно, предположил, что в приоритете — высокая оптимизация по скорости и памяти. Как заметили тут в комментариях, скорее всего ожидалось лишь «избегать лишних созданий коллекций, ставить Capacity по возможности и не конкатенировать строки в цикле».

  3. Я недостаточно обосновал принятые решения, такие как отказ от регулярок и парсинг числа кустарным методом. Собственно, для меня образцом всегда был и остаётся код самого .Net. И такие решения я смотрю именно там. Наверное, надо было написать в комментариях побольше обоснований и возможных альтернатив.

  4. Основная претензия к моему решению. «Код плохо читаемый из за множества вложенных If, я такое тоже не люблю и на проектах стараюсь избегать если можно без них». Я не думал, что количество вложений if считается таким уж страшным злом если строки короткие и понятные. Я добивался цели удобно увидеть алгоритм целиком на одном экране. Для сравнения можете посмотреть метод GetTimeAfter(), который делает примерно тоже самое в библиотеке Quartz. У них максимально 7 отступов, у меня — 11 потому что добавляются доли секунды. Но их код размазан на много экранов и понять алгоритм очень трудно.

  5. Единственный однозначный мой недочёт — не объединил два похожих метода в один с дополнительным параметром.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Версии неожиданной оценки:
21.43% Заказчику хотелось получить красиво выглядящий в IDE код и тонну комментариев, по объёму втрое больше кода. 63
15.31% Не хватило обоснования выбранных решений. Непонятно почему кто-то выделил дополнительную память или не применил регулярные выражения. 45
34.35% Тестовое задание ни о чем. Видимо ищут джуна, чтобы валить на него косяки и пинать его без повода. 101
52.38% Нужен был код в production, а делать не хотели. И кинули, как тестовое задание. 154
Проголосовали 294 пользователя. Воздержались 272 пользователя.
Теги:
Хабы:
Всего голосов 36: ↑22 и ↓14 +8
Просмотры 40K
Комментарии Комментарии 417