вторник, 1 июня 2010 г.

[work; prog] Так вот о тестовом задании для C++ников

Расскажу подробнее о тестовом задании (условие было опубликовано ранее), которое я давал кандидатам на должность C++ программиста.

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

Изначально задача возникла при необходимость обновить лицензионную информацию в небольшой библиотечке. Я прикинул, как её решить, оказалось, что проще 10 файлов вручную обработать, чем писать программу. Но тогда я вел что-то вроде факультатива по программированию и мне требовалось какое-то более-менее реальное упражнение для студентов. Я дал эту задачу им в качестве домашнего задания. К своему удивлению, я обнаружил, что с ходу ее никто не решил. Стал интересоваться почему и тут-то стало понятно, насколько данная задача удобна в качестве теста.

Ловушка первая, она же главная. Условие задачи не гарантирует того, что лицензионная информация всегда находится в начале файла. Да, пример приведен именно такой. Но условие этого не гарантирует.

Фактически, этот тест является главным тестом на внимательность изучения условия задачи. Более-менее опытные разработчики уточняют это в дополнительных вопросах. Такой вопрос – сразу же большой и жирный плюс кандидату. Еще более жирный плюс – если кандидат не задавал вопросов, а сделал универсальное решение сразу.

Поскольку лицензия может находится в любом месте файла, а весь файл не должен загружаться в память целиком, то сразу же вырисовывается чуть ли не единственный способ ее решения: принцип “скользящего окна”. Т.е. из файла считывается N строк, которые и сравниваются с текстом лицензии. Если не совпадают, то одна первая прочитанная строка выбрасывается, а еще одна считывается, и производится новое сравнение. Ничего сложного, имхо.

Еще один большой и жирный плюс можно заработать, если при несовпадении прочитанного из файла фрагмента выбрасывать не одну строку, а несколько. Здесь же принцип точно такой же, как и при поиске подстроки в строке – можно применять и метод Кнута-Морриса-Пратта, например.

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

Ловушка вторая, тест на максимализм и способность расставлять акценты. Это разбор аргументов командной строки и response-файла. Поскольку реализация response-файла объявлена бонусом, то самоуверенные разработчики сразу же берутся за нее. Изобретая при этом свой собственный маленький велосипед для универсального разбора аргументов (чтобы аргументы командной строки и содержимое response-файла обрабатывались единообразно).

А вот делать этого не следует. Сроки на решение небольшие (для последних вакансий я давал по два дня), если знаний и опыта не хватает, то такую библиотеку сделать не успеешь. Нужно рассчитывать силы и расставлять акценты. Главное что? Работа с лицензией. Вот она и должна быть сделана на пять баллов. И только после этого можно браться за response-файлы. Если получилось – отлично, если нет – ничего страшного, предъявляется минимальный вариант, который, тем не менее, полностью решает исходную задачу.

Как показывает практика, далеко не все могут расставить акценты именно так. Даже были случаи, когда кандидат успевал сделать response-файлы, но не успевал реализовать работу с лицензиями.

Неожиданным для меня явилось еще и то, что даже разбор аргументов командной строки оказался очень хорошим тестом. В частности на склонность людей к хардкодингу и дублированию кода. Очень многие решения грешили большими циклами по содержимому argc, внутри которых были развесистые if-ы с кучей strcmp и зашитыми прямо в код именами аргументов.

В принципе, в response-файлах так же есть маленькая ловушка. Хотя она ни разу не срабатывала (т.к. толком response-файлы никто и не сделал), но она есть: в именах файлов могут быть пробелы. И такие имена в response-файлах нужно задавать в кавычках. А раз так, то нельзя читать response-файл, например, с помощью штатного operator>>(istream&,string&) – будет идти разбиение по пробельным символам.

Ловушка третья, она же маленькая заноза в заднице. Это требование к учету различных концов строк (DOS-овский, Unix-овый и Mac-овый). Это самая настоящая мелочь. Но она определяет, каким образом в памяти должен храниться текст лицензии. И как нужно представлять в памяти фрагмент файла для сравнения.

Поскольку концы строк нужно отрезать при сравнении лицензии, сразу же отпадает возможность искать совпадение с помощью std::string::operator== или memcmp. Строки нужно хранить отдельно, концы строк – отдельно.

Мне казалось, что здесь нет ничего сложного. Ну нужно было сделать структуру, в которой был бы список строк и использованный в них маркер конца строки или же список объектов, в каждом из которых хранится строка и ее маркер. И всех делов. Так ведь нет! Каких только странных комбайнов я не насмотрелся :)

Ловушка четвертая, тестирование. Только считанные единицы соискателей вместе с решением присылают тестовые файлы на которых они проверяли свое решение. Не удивительно, что они не проходили даже поверхностной проверки (например, далеко не все решения могли выполнить цепочку из insert-change-delete). Во время последнего поиска людей первым, кого я взял, был программист, который прислал далеко не самое лучшее решение. Но вместе с решением шел набор тестовых файлов и батник, запустив который можно было эти тесты прогнать. И я взял его, даже не смотря на то, что опыта работы и знаний у него было намного меньше, чем у всех остальных претендентов. Зато задатки правильные :)


Вот, собственно, и все запланированные (точнее, оказавшиеся там случайно) сложности. Из незапланированного я бы выделил следующий список наиболее часто встречающихся дефектов:

  • дублирование кода и большие функции (по 100 и более строк);
  • хардкодинг. Суровый и повсеместный. Ладно когда имена аргументов командной строки задаются прямо в strcmp. Так ведь они затем используются для определения режима работы программы, при выдаче сообщений об ошибках и т.д.;
  • слабое знание STL. За счет использования std::ifstream, std::vector, std::string решение данной задачи не представляет сложности. Но если вместо них брать старые-добрые FILE*, самодельные списки с ручным управлением памятью, буфера для строк фиксированного размера и прочие plain-C прелести, то и решения получаются большие, сложные и ненадежные;
  • отсутствие контроля за успешностью операций ввода-вывода. Доходило даже до того, что люди открывали файл и даже не проверяли, открылся ли он;
  • отсутствие устоявшегося стиля кодирования. Хотя это объясняется тем, что многие имели опыт не более 1-2 лет работы. Но даже и опытные разработчики не могли объяснить, почему в одном месте они обозвали переменную в camelCase, а в другом – в lower_case.

Такие дела. Хорошая была задача. Теперь буду придумывать что-нибудь новое :)

Напоследок хочу предупредить тех, кто захочет попробовать практику тестовых задач. На анализ решений вам потребуется время. Иногда много. На некоторые решения у меня уходило часа по три, если не больше. Бывает так, что смотришь в код, чувствуешь, что здесь что-то не так. А точную причину беспокойства понять не можешь. Приходится вникать, писать собственные тесты, гонять программу в разных режимах. Для того, чтобы потом обстоятельно, с уликами на руках, объяснить кандидату что почем ;) Так что не самый легкий это путь отсева претендентов.

Отправить комментарий