От переводчика
Эрик Липперт — прежде всего известен как ведущий разработчик языка C# (в прошлом), и многие наверняка читали его блог Fabulous adventures in coding. Ранее в MSDN публиковался даже официальный перевод этого блога, что прекратилось после ухода Липперта из Microsoft. Конечно же, нет ничего лучше чтения оригинала, но я решил для разнообразия перевести что-нибудь из недавних постов Эрика. Надеюсь, будет интересно.
Ранее, я повторно опубликовал две свои старые статьи (оригиналы: раз, два — прим. перев.), касающиеся процесса технических собеседований. Полагаю, что мог бы описать более подробно, как провожу собеседования и на что при этом обращаю внимание.
Вот мои главные цели:
- не нанимать плохих работников;
- нанимать хороших работников;
- оставить кандидата с положительным впечатлением о компании.
Конечно же, избежать найма плохого работника — гораздо важнее, чем нанять хорошего. Если мы не заполучим отличного сотрудника — это очень плохо, но неудачный подбор персонала может снизить производительность команды на годы.
Последний пункт также является ключевым; если мы хотим принять человека на работу, то, очевидно, должны произвести хорошее впечатление. Но мне также хочется, чтобы все, получившие отказ — также оставались хорошего мнения о нас. У них есть друзья, которые, возможно, захотят собеседоваться. Когда-нибудь они могут принимать решение о покупке нашего продукта, либо рекомендовать его. Собеседование — это дорогостоящий бизнес-процесс; если он не приведет к найму, то, быть может, мы получим клиента, или хотя бы большую благосклонность к себе.
Собеседование проходит следующим образом.
Прежде всего, я должен убедиться, что соискателю комфортно, есть чем утолить жажду, не хочется в туалет, и так далее. Затем, если человек уже собеседовался у кого-то до меня, то я спрашиваю «Как дела?». Цель этого вопроса — разрядить атмосферу, завязать разговор, и посмотреть, есть ли у кандидата какие-либо вопросы касательно прохождения собеседования. Иногда у меня есть информация о том, как уже показал себя кандидат, но она не предназначена для принятия решений. (В Microsoft поощрялось давать обратную связь следующим в цепочке интервьюерам как можно скорее, чтобы они могли копать в сторону потенциальных проблем; в Coverity мы более склонны к тому, чтобы каждый интервьюер начинал беспристрастным. Оба подхода имеют свои плюсы и минусы.)
После этого, я выбираю из резюме кандидата что-нибудь интересное и не слишком старое, сперва прошу дать мне «Elevator Pitch» — несколько предложений, описывающих проект, чем он был интересен, важен — а затем прошу более подробно описать решенную в этом проекте проблему.
Это, опять же, поможет кандидату расслабиться, поскольку он рассказывает о чем-то из своего резюме, и, вероятно, хорош в этом. Я хочу выяснить в первую очередь — может ли собеседуемый нормально общаться? Вы будете удивлены, какое количество людей не может рассказать одним-двумя предложениями о вещах из своего резюме. Во-вторых, чем на самом деле они занимались на этом проекте? Это может шокировать, но люди постоянно приукрашивают свои заслуги. Изучив подробности того, что именно сделал кандидат, я могу понять, действительно ли он «умен и доводит дело до конца» (smart and gets stuff done — прим. перев.) или нет.
Затем мы переходим к действительно интересному этапу: техническая задача. Цель — выяснить, способен ли кандидат писать тот сорт кода, который необходимо будет писать каждый день. Мне известно, что написание кода от руки на доске критикуют, и я с согласен с этой критикой. Это неестественная обстановка, не такая как при реальном написании кода, это стрессовая ситуация и т.д. Я стараюсь придумывать вопросы, минимизирующие подобные трудности, но все еще способные показать мне, справится ли кандидат с реальной работой. (А если человек не желает писать код на доске, мы можем сделать это на компьютере. Однако, задаваемые мной вопросы редко требуют написания более чем шести строк кода. Самые лучшие соискатели должны быть в состоянии написать полдюжины коротких строчек и без помощи текстового редактора.)
И этот этап действительно важен. Я проверяю, чтобы претенденты соответствовали необычному набору требований. Кандидаты в команды, работающие над инструментами анализа языка, должны быть сильны в теоретических основах информатики, и в то же время способны работать в реальном мире несовершенного кода. Я собеседовал прорву глубоко понимающих теорию ученых, при этом неспособных написать реальный код, а также опытных промышленных программистов, которые не могли сказать мне, что такое бинарное дерево.
Хорошая техническая задача имеет следующие характеристики:
- может быть поставлена и решена небольшим количеством кода за малое время;
- не требует внезапного озарения, чего трудно добиться в стрессовой ситуации;
- это не загадка с подвохом;
- по желанию может быть сделана труднее, либо легче, для того, чтобы лучше оценить уровень кандидата;
- похожа на проблемы, которые соискатель должен будет решать на рабочем месте;
- тестирует не только написание кода, но и другие навыки, такие как умение читать существующий код, или иметь дело с нечеткими требованиями;
Задача, которую я долго использовал в Microsoft, и которая работает даже лучше в Coverity, имеет следующую схему:
Во-первых, есть два написанных на C++ метода из публичного API, каждый из которых состоит из трех строк очень простого кода. API управляет «задачами» на «сервере», и используется «клиентом». Сценарий намеренно очень размыт, но код понятен. Я провожу кандидата по каждой строке и убеждаюсь, что он понимает код. Здесь мне необходимо только базовое понимание простого синтаксиса (подавляющее большинство кандидатов знакомы с C++ согласно их резюме, так как это необходимое требование для работы).
Далее, я поручаю кандидату исследовать этот код на наличие любых проблем. И наблюдаю, в какую сторону он копает при исследовании чужого кода. Представленный код имеет проблемы дизайна, надежности, безопасности, тестируемости и переносимости. Я смотрю, какие проблемы кандидат находит самостоятельно, а если он что пропускает, могу подсказать: «Если клиент намеренно попытается нарушить работу сервера, что вы сделаете?» или «Что будет, если скомпилировать этот код для запуска на 64-разрядной машине?» и посмотрю, поймет ли он намек.
Это реальный навык, который мне необходимо использовать каждый день — во-первых, при code review собственного кода и кода моих коллег. А во-вторых, мой бизнес — написание программ, анализирующих забагованный код и определяющих, что он забагован. Если соискатель не способен проверить чужой код — маловероятно, что он будет успешен вообще где-либо. У того, кто не может найти ни одну из десятка ошибок в программе, содержащей шесть инструкций — вряд ли получится написание анализатора кода, находящего ошибки.
Здесь я ищу понимание, критическое мышление и базовое знание предметной области. Часто на этом этапе проявляются сигналы опасности. У меня были претенденты с «PhD in computer science», которые не знали, например, что в 64-битной архитектуре длина указателя составляет 64 бита. Да и зачем им знать? Как говорится, астрономия — это не наука об изготовлении телескопов. Обширные знания теоретической информатики не обязательно влекут за собой знания о пропускной способности шины между процессором и памятью. Но кандидат, не понимающий, как указатели реализованы в распространенных архитектурах — вряд ли будет успешен, работая в команде по разработке анализаторов, которые ищут связанные с длиной указателя ошибки.
Также многие соискатели определяют ошибки в коде, но не могут описать их последствия. Неопределенное поведение по определению не определено. Но описание того, что может произойти, если вредоносный клиент назначает указатель на произвольный кусок памяти (англ. pointer to arbitrary memory — прим. перев.) и заставляет сервер выполнить виртуальный метод с таким указателем — демонстрирует понимание, как на самом деле компилятор генерирует код.
Это первая фаза технической задачи. Следующий этап включает в себя дизайн, и, возможно, некоторые небольшое количество кодинга. «Вы можете изменить реализацию API, но не сигнатуры методов — как бы вы исправили каждую из найденных ошибок?». Опять же, это основной рабочий навык, который нам приходится использовать каждый день: исправление ошибки на «сервере», будучи не в состоянии изменить поведение «клиента». Возможно, вызывающий код принадлежит заказчику, или другой команде, либо уже продан, да что угодно.
На данном этапе я обращаю внимание на следующее:
- Есть ли еще сигналы опасности? Боже, количество кандидатов, пытавшихся решить проблему переносимости путем сжатия 64-битного указателя в 32-разрядное целое число — огромно. Тут нет хитрой загадки, которую я прошу разгадать. Вы не можете поместить двадцать фунтов муки в десятифунтовый мешок. Должно быть очевидно, что необходимо найти другой способ решения проблемы.
- Какие инструменты могут быть в багаже соискателя? Все кандидаты в конечном итоге понимают, что им необходимо добавить вспомогательную структуру данных для маппинга из 32-битного целого числа в 64-битный указатель. Известны ли им существующие реализации такого маппинга? Если нет, уверены ли они, что смогут написать подобное?
- Может ли кандидат выделить действительно сложную часть задачи? Сложность здесь — не в осознании необходимости маппинга, а в том, чтобы обеспечить уникальность ключей при добавлении новой пары ключ-значение (как я упоминал в предыдущем эпизоде, оригинал — прим.перев.). Даже когда я неоднократно спрашиваю: «В вашем решении вы так и не рассказали, как вычислить значение ключа — так как генерируется это значение?», некоторые кандидаты просто не обращают на это внимания и продолжают объяснять мне, как работает маппинг. Либо они возвращаются к предыдущей точке и пытаются сжать 64-битное значение в уникальный ключ 32-бит, что невозможно, и что является причиной маппинга.
- Как кандидат выйдет из подобной неоднозначной ситуации? Проблема умышленно размыта, неясна, и трудная часть заключается в генерации уникальных значений. Если есть два клиента, каждый из которых создает по две «задачи», а затем отключается, то сгенерировать уникальные 32-битные целочисленные ключи несложно. Если есть миллионы клиентов, генерирующие миллионы «задач», то получать уникальные ключи может быть очень трудно. Многие кандидаты никогда не спрашивают меня, сколько клиентов и «задач» есть в этой системе, и поэтому некоторые выдают слишком переусложненные идеи, а кто-то предлагает решение, которое немасштабируемо в принципе.
- Может ли кандидат провести параллели с решенными ранее проблемами? Это трудно сделать в стрессовой ситуации, но на подобное все равно интересно посмотреть. Некоторые кандидаты очень быстро осознают, что проблема «сгенерировать уникальный 32-битный номер, и не использовать повторно его до тех пор, пока клиент не закончит с ним» должна была быть ранее решена тем, кто написал 32-битную версию менеджера памяти. В конце концов, что такое менеджер памяти (англ. memory manager — прим. перев.): устройство, которое предоставляет вам по запросу уникальный указатель, и не выдает этот же указатель вновь до тех пор, пока вы его не освободите. Поэтому сформулированная мной проблема может быть сведена к ранее решенной. Некоторые кандидаты рассуждают о выделении памяти и подобных вещах, как о магии. Многие поняли, что проблема аналогична реализации кучи; но только один сказал: «Я решу проблему, получив от Windows кучу объемом в 4 миллиарда байт, а затем буду выделять отдельные байты из кучи для того, чтобы генерировать уникальные 32-битные числа» (англ. I’ll solve the problem by having Windows make me a four billion byte heap, and then allocate single bytes out of it to generate unique 32 bit numbers — прим. перев.). Это действительно сведет задачу к уже решенной!
Если кандидат справляется очень хорошо, то усложнить задачу достаточно просто. Предположим, клиенты используются многими потоками; какая стратегия синхронизации сохранит высокую пропускную способность? Допустим, состояния задач должны быть сохранены при перезагрузке компьютера. Что если сервер в кластере и может перенаправить нагрузку на другие сервера. Я хочу выяснить, насколько глубоки их знания. Или я могу упростить задачу, ограничив число клиентов десятью. Предположить, что «задачи» стартуют и завершаются в некотором определенном порядке, так что следующая «задача» стартует после того, как завершается предыдущая. И так далее. Мне бы хотелось, чтобы соискатели почувствовали, что справились с хоть с какой-нибудь проблемой.
В конце собеседования я стараюсь выгадать немного времени, чтобы кандидат успел задать вопросы о команде, вакансии, компании и т.д. (никто ни разу не спросил: «Напишете код, разворачивающий связанный список?», и я был бы очень удивлен, если бы кто-то это сделал). Для меня это еще одна возможность «прорекламировать» нашу компанию, а также понять из вопросов соискателей, что для них важно.