Какое-то время назад по Хабру прокатилась волна статей о поиске работы и прохождению собеседований. Многократно высказались и работодатели и соискатели. Но, к сожалению, не была в достаточной степени затронута тема тестовых заданий.
Ведь, тестовое задание не ограничивается категориями «выполнил» и «не выполнил». Внимательно наблюдая за процессом и изучая финальный результат, можно в итоге многое сказать о человеке ни дня с ним вместе не поработав. А порой, и научиться чему-то новому.
Предлагаю вашему вниманию тестовое задание, которое я уже довольно давно даю кандидатам в компании, где я работаю:
На экране есть сетка M на N из цветных квадратиков. Нужно реализовать на этой сетке следующий эффект — по клику слева на право со скоростью V пробегает волна, меняя цвет квадратиков на другой (единый для всей волны). Эффект должен работать при любых значениях M, N, V. Волна начинается всегда у левой стенки. Одновременно может идти несколько волн разного цвета.
Анимационный пример: http://dl.dropbox.com/u/3601116/wave.swf (покликать по флэшке).
Я не сомневаюсь, что это задание с легкостью сделают все программисты посетители Хабра.
А у меня получилась следующая статистика:
- В итоге, задание взяли чуть больше 20 человек.
- Пара человек ничего не сделали.
- Половина из оставшихся (по моим критериям) с ним не справились.
- Кандидаты четко разделились на весьма интересные группы.
Подумайте немного, как бы вы выполнили это задание, и заходите под кат, где я расскажу обо всем подробнее.
Disclaimer
В этой статье я не затрагиваю этичность, сложность и необходимость тестовых заданий как таковых — я понимаю, что у каждого здесь по данной теме сложилось своё мнение. Я буду рад услышать его в комментариях как дополнение к материалу, но не приветствую разжигания каких-либо холиворов.
Задание
Пара слов о задании. Мне было все равно на чём человек его реализует, но сразу же обговаривались два условия:
- я смогу его запустить,
- я смогу понять код, если меня разбудят в 7 часов утра.
Группы
Как я уже сказал, кандидаты разделились на отчетливые (пересекающиеся) группы, о которых пойдет речь ниже. Попадание человека в определенную группу само собой уже может многое о нем рассказать, как с хорошей, так и с плохой стороны.
Щаз все будет! Завтра сделаю!
Конечно же, несколько человек рьяно ринулись в бой… но, так ничего и не сделали. На тех, которые пропали сразу, я сразу и забил, но пара человек упорно кормили завтраками, и потом в итоге все равно куда-то слились.
Вывод: Буду знать, с кем не стоит работать ни в офисе, ни на фрилансе. Вполне можно было написать письмо и отказаться от дальнейшего общения, чтобы не тратить ни моё время, ни время кандидата. Как показывает практика, рынок-то на самом деле небольшой.
Всё просто, надо сделать вот так и вот так
К своему удивлению, я наткнулся на людей, которые мне долго (и объёмно) объясняли, как нужно делать это задание, как построить архитектуру, как оптимизировать отрисовку. Сыпались технические термины и на показ выставлялось знание win32 api. В конце концов, я не выдерживал и говорил прямо, мол, отлично, теперь идите и сделайте это задание.
И знаете что?.. У всех у них возникли с ним сложности.
Вывод: на практике, теория с практикой сильно расходятся. Часто люди создают вокруг себя тонкую оболочку профессионализма, за которой скрывается откровенное дилетантство. Хорошо это вовремя понять и правильно использовать.
Нам в универе преподавали С на втором курсе
На двери этой группы огромными желтыми буквами было написано IMPERATIVE. Как и полагается для решения задачи со второго курса университета, в коде фигурировала матрица, какое-то количество переменных с началами и концами волн, а также переменные i, j, k, m, n и другие в огромных количествах.
Код невозможно было понять, а в большинстве случаев он работал с косяками и/или медленно.
Так, i от нуля до точки распространения волны, j и k симметрично в обе стороны… а это граничное условие… так, а это что за m пошло теперь обратно до границы текущего цвета… так, а это вообще зачем и каким образом работает??! argh! fuck it!
Диалог с кандидатом затягивался на десяток писем, в котором каждое новое начиналось со слов «всё поправил, теперь работает». Почувствовал себя учителем информатики. Надо ли говорить, что до конца из этой группы проект так никто и не доделал. Вот тебе и Российское образование.
Вывод: К сожалению, на перегретом рынке IT бОльшая часть кандидатов обладают весьма посредственными знаниями и хотят сразу кучу денег и BMW. Тестовое задание (даже по факту выполненное) может многое сказать о том, как человек привык писать код и его подход к решению алгоритмических задач.
Add(new Wave());
В основном народ, конечно же, сразу в формулировке увидел разбиение задачи на классы и элементарную архитектуру — кто кем управляет. Создаем волны, а они там уже пусть сами что-то делают.
При этом, тоже умудрялись проявлять чудеса ООП Головного
Где-то тут были и пара человек, которые очень старались не попасть в предыдущую группу, но все равно, местами не выдерживали и в хрестоматийном ООП коде грубо нарушали инкапсуляцию.
Вывод: хороший структурированный по канонам ООП код — признак опытного разработчика, который может грамотно разбить задачу по модулям и собрать из них приложение. Стоит внимательно относиться к признакам overengineering и нарушению абстракций.
Специфика моей работы заключается в сжатых сроках, на которых никак нельзя себе позволять удаляться глубоко в абстракции архитектуры. И в то же время, необходимо придерживаться правил и не разбрасывать себе же граблей по пути следования.
Я все пишу с нуля
В задании специально не ставились рамки на использование языков, фреймворков и библиотек. Но люди из этой группы упорно выбирали непростой путь решения и сами писали для себя инфраструктуру (например, вывод на экран меша в 3d пространстве). В итоге с заданием справились все.
Вывод: На первый взгляд, кандидаты обладали достаточными знаниями, чтобы с нуля на любимом инструменте реализовать решение задачи и не запутаться в алгоритме. Но сразу же появилось подозрение в «Not Invented Here Mentality» и сомнения в том, что человек сможет эффективно использовать уже существующие наработки и сторонние библиотеки.
Я не ищу простых путей!
Сильно пересекающаяся с предыдущей, эта группа включает в себя всех тех, кто был уже близок с созданию фреймворка для разработки приложений с бегающими цветными волнами в вакууме: волны, двигающиеся в любых направлениях, каждая в своем потоке, отрисовка картинки unmanaged кодом из managed приложения, и другие монструозные решения.
Человек пытался показать, что может писать многопоточные высоконагруженные системы? На таком тестовом задании?..
— Почему ты сделал это вот так? Ведь, можно было сделать гораздо проще.
— Зачем? Я же могу так!
Вывод: Стоит задуматься. Человек, скорее всего, специалист в своем деле и обладает обширными знаниями, но сможет ли он решать конкретные задачи быстро, не создавая под каждую фреймворк для решения подобных задач? Есть ли у вас фреймворк, за который его можно посадить?
Я все перерисовываю / я не все перерисовываю
Многие сразу же сообразили, что в алгоритме движения наложенных друг на друга волн слева на право можно сильно оптимизировать отрисовку. Но, как не печально, многие этим вырыли себе могилу, запутавшись в своём же собственном коде.
Но, была и парочка интересных моментов. Например, трое или четверо тем или иным способом вместо добавления двигающейся волны в терминах ООП, считали когда и в какой цвет должна перекраситься ячейка. По сути, «добавление» волны лишь изменяло существующую временную программу переключения цвета.
Вывод: С одной стороны, мне было совершенно плевать на лишнюю перерисовку, если приложение работало правильно и быстро. С другой стороны, человек понимал, что можно все сделать оптимальнее. Но, сделал ли он сначала рабочий неоптимальный вариант, а только потом стал его оптимизировать? Зачастую, нет. А это заставляет задуматься.
Мне пофиг, как оно выглядит, если оно компилируется
Несколько человек прислали выполненные задания с откровенными графическими косяками. Даже не читая код, можно было понять, что что-то там внутри не так. Не заметить их было невозможно, так что, вывод напрашивался сам собой: человеку плевать.
Почти все с той или иной скоростью согласились проблемы решить, но один товарищ меня удивил. Видите ли, он прекрасно знает как исправить косяки и скорость отрисовки, но считает, что для тестового задания он тратить час своего времени не будет.
Вывод: Очень важно, чтобы человек закрывал таск лишь в тот момент, когда уверен, что работа сделана. Передавая на проверку приложение с очевидным косяком (в надежде, что его не заметят?) он тратит не только своё время, но и время проверяющего. А товарищ, который знает как решить проблему, но не будет ее решать, потому что… — будет вести себя так же и на работе.
Мне пофиг как они будут это запускать
Несколько раз у меня возникли трудности с запуском приложения:
- Использовались какие-то библиотеки, которые не понятно где быстро взять.
- Нужно было ставить IDE, SDK и всё на свете, чтобы лишь скомпилировать апп.
- У меня были явно не все файлы проекта, а с незнакомым языком возиться его настраивать было не много желания.
Понятен был код, но не было возможности увидеть результат. Один из кандидатов магическим образом отвалился на этом этапе…
Вывод: Тоже какое-то неприятное пофигистическое отношение к проверяющему. В итоге на дополнительную переписку была потрачена еще куча времени.
Резюме
У каждого кандидата был тем или иным образом уникальный подход к решению задачи. Изучив его, можно было сразу многое сказать о человеке как о разработчике:
- Какой у него опыт и в каких проектах.
- Насколько он абстрактно мыслит.
- Способен ли он реализовать простой алгоритм.
- Может ли он сам четко закрывать поставленные задачи.
Нахождение в той или иной группе само по себе не означает, что человек плохой или хороший. У меня одни потребности, у вас другие. Я ищу одни качества и настораживаюсь при виде других, у вас же, наверняка, какие-то свои критерии отбора.
Одни решения заставляли меня потерять веру в человечество, от других глаза становились квадратными, третьи казались в той или иной степени интересными и необычными. Читаешь код, и как-будто проникаешь в голову его автору — как он подошел к проблеме, как он строил алгоритм и на какие трудности наткнулся. После каждого присланного решения я начинал по-другому смотреть на задачу целиком.
Конечно, самым популярным был ООП подход с созданием экземпляров класса Wave, апдейтом его по таймеру и очисткой при достижении некоторого критерия. Но, при этом, люди разным образом подходили к распространению волны и перерисовке сетки.
Я рад, что некоторые исходники позволили мне самому чему-то научиться!
Решение
История была бы неполной, если бы я не приложил здесь канонически правильного решения.
На самом деле, изначально его не было. Лишь к концу второго десятка «проверенных работ» я стал задумываться, а как бы я решил эту задачу… Интересной была одна особенность всех решений — все… абсолютно все смотрели на задачу, следуя буква к букве её описанию в начале поста: сетка, по ней пробегает волна…
Если абстрагироваться от волны и в какой-то мере от сетки, посмотреть на задачу с другой стороны, можно увидеть, что этот эффект есть ни что иное, как клеточный автомат на квадратиках с некоторыми правилами. Но какими?
Действительно, пусть клетка принимает целое положительное значение от 0 до бесконечности. Каждый тик клетка принимает максимальное значение своих четырех соседей. Не трудно увидеть, что устанавливая значение одной из левых граничных клеток в N+1 (где N — максимальное значение клеток на сетке), получится ровно нужный нам эффект распространения волны.
for (var i:uint = 1; i < Width-1; i++) {
for (var j:uint = 1; j < Height-1; j++) {
buffer.setPixel(i, j, Math.max(bmp.getPixel(i, j), bmp.getPixel(i-1, j), bmp.getPixel(i, j-1), bmp.getPixel(i, j+1)));
}
}
Можно было бы так всё и оставить, но проще завести еще один массив с рандомными цветами. В итоге, весь код на флэше выглядит так (https://gist.github.com/valyard/6084814):
var Size:uint = 10;
var Width:uint = int(stage.stageWidth/Size);
var Height:uint = int(stage.stageHeight/Size);
var bmp:BitmapData = new BitmapData(Width, Height, false, 0x000000);
var buffer:BitmapData = new BitmapData(Width, Height, false, 0x000000);
var display:BitmapData = new BitmapData(Width*Size, Height*Size, false);
var zeroPoint:Point = new Point(0, 0);
var colors:Array = [0xFFFFFF];
addChild(new Bitmap(display));
stage.addEventListener(MouseEvent.CLICK, clickHandler);
stage.addEventListener(Event.ENTER_FRAME, frameHandler);
function clickHandler(e:MouseEvent):void {
addWave((e.localY % display.height)/Size);
}
function addWave(position:uint):void {
if (position == 0) position = 1
else if (position >= Height-1) position = Height-2;
bmp.setPixel(1, position, colors.length);
colors.push(0xFF000000 + int(Math.random()*0xFFFFFF));
}
function frameHandler(e:Event):void {
for (var i:uint = 1; i < Width-1; i++) {
for (var j:uint = 1; j < Height-1; j++) {
buffer.setPixel(i, j, Math.max(bmp.getPixel(i, j), bmp.getPixel(i-1, j), bmp.getPixel(i, j-1), bmp.getPixel(i, j+1)));
}
}
bmp.copyPixels(buffer, buffer.rect, zeroPoint);
var rect:Rectangle = new Rectangle(0, 0, Size, Size);
for (i = 0; i < Width; i++) {
for (j = 0; j < Height; j++) {
rect.x = i*Size;
rect.y = j*Size;
display.fillRect(rect, colors[bmp.getPixel(i,j)]);
}
}
}
Как видите, код минимальный и совсем простой. Но, ничего подобного никто мне так и не показал.
Эпилог
Все имена выдуманы, все совпадения случайны. Если вы когда-либо выполняли такое же тестовое задание, я об этом ничего не знаю, придумал его не я, и вообще, ничего не происходит… нечего тут смотреть… расходимся, господа!
Автор: valyard