Cложный код или история одного интервью
Хотелось бы поделиться своей историей неудачи, если понимать под этим негативный фидбек от компании после ревью технического задания. Конечно же, каждый подобный опыт — это всегда прекрасная возможность пересмотреть стратегию, сделать определенные выводы из своих или чужих ошибок. И в данном случае, у меня есть отличный повод порефлексировать над пережитым опытом интервью, даже несмотря на то, что он не закончился оффером. Хочу заранее выразить свою благодарность за время, которое вы потратите на чтение этой статьи с кодом и усилия, приложенные на их понимание. Хочу выразить двойную благодарность, если при этом читатель поделится своим мнением.
Весь исходный код доступен в публичном репозитории на GitHub по этой ссылке. Он не претерпевал никаких изменений с момента публикации, несмотря на то что в нём есть ряд моментов, которые я бы довел до ума, но намеренно не стал этого делать. Всегда есть некая договоренность с самим собой относительно времени, которое мы тратим на задание. Пытаемся расставить приоритеты, исходя из личных предпочтений, если иное не было подчеркнуто в требованиях к заданию: кто-то предпочтёт написать документацию на свои API, а кто-то решит увеличить покрытие тестами. В моём случае, как вы можете заметить, нет e2e тестов и unit тестов UI компонентов, также CSS накидан на скорую руку и не следует никакой методологии. Вполне вероятно, вы можете продолжить этот список и другими недочетами.
Будучи под впечатлением от профиля одной компании, подкрепив это отзывом одной бывшей коллеги, работавшей там когда-то, я решил отправить туда своё резюме и начать процесс собеседования. Через неделю-две со мной связался рекрутер и предложил пообщаться с лидом одной из команд. Разговор с лидом больше сводился к общим вопросам: что вам нравится, какие технологии используете, чем гордитесь, какие проблемы решали, также было оставлено место и для моих вопросов, — совершенно обычный разговор, вполне типичный для IT компании. Спустя некоторое время этого же дня мне сбросили задание. Ниже я напишу требования этого задания. Текст задания сохранен в оригинале:
Вам предлагается написать сервис для совместной игры в крестики-нолики.
Игра содержит несколько игровых полей, каждый игрок может либо присоединиться к игре на существующем поле, либо создать новое поле.
Начальный экран содержит список игровых полей и их краткое описание (сколько игроков сейчас играет, когда был последний ход). Данные обновляются в реальном времени.
Игровое поле имеет фиксированный размер 10 x 10. На каждом поле могут играть несколько игроков. Каждый игрок может сделать ход в любой момент, но не может сделать несколько ходов подряд.
Игровой сервер может хранить состояние игрового процесса в памяти.
Решение вы можете реализовать на любых удобных вам технологиях, но будет здорово если вы сделаете их на [обозначены технологии проекта]
Я решил делать на удобных мне технологиях. В качестве языков были выбраны: Java и Javascript. В качестве фреймворков: Spring и React. Хранение состояния осуществляется в памяти как и позволено в задании.
Буквально за неделю до этого, я делал техническое задание для другой компании, где так же фигурировала игра, но с другой логикой. Игра называлась Калах. Архитектура, которую я заложил для неё, и принципы, которым следовал, показались мне вполне уместными и здесь. И вообще, кажутся уместными для реализации простых игр в целом. Какие основные приоритеты я ставил для себя при решении задачи:
- Наличие документации для API. Сниппеты с семантикой Rest API будут генерироваться интеграционными тестами — убиваем двух зайцев. И здесь на помощь к нам приходит Spring Rest Docs.
- Использование websockets для построения интерактивного дашборда и самой игровой сессии. Long polling — уже давно не модно. Берем stomp и sockjs, рекомендуемые нам инфраструктурой spring. Как дополнительное преимущество — если сервер не поддерживает websockets, библиотека делает fallback на long polling незаметно для клиента. Почему бы и нет?
- Инкапсуляция правил игры в отдельных классах. Так, для Калах, например, у меня было три правила: правило хода, правило захвата и правило конца игры. Такой подход мне показался очень уместным, т.к. каждое правило можно тестировать отдельно, можно убрать из игры или можно усложнить игру, добавив новый класс с реализацией другого правила. В крестиках и ноликах у меня таких правил оказалось два — правило хода (TurnRule), которое так же взяло на себя ответственность за валидацию (всё же, валидацию я бы вынес на уровень выше) и правило конца игры (EndGameRule), задача которого сводилась к определению победителя или ничьи. Следует также отметить, что порядок правил я определяю с помощью аннотации Order спринга, которая указывает контейнеру в каком порядке инжектить бины. Это решение может показаться спорным, но наличие теста, в котором мы проверяем что задан именно такой порядок, делает его вполне жизнеспособным.
- Алгоритм нахождения победителя я решил реализовать, сделав ставку на композицию, при этом думая об эффективности, не забывая что нельзя выходить за пределы линейной сложности. Я написал некоторый набор кастомных итераторов, каждый из которых занят идентификацией выигрыша — по горизонтали, по вертикали, основная и побочная диагонали. Как мне кажется читать такой алгоритм проще и в любой момент можно включить/выключить конкретный итератор, что делает архитектуру достаточно гибкой.
- Фронтенд будет реализован тройкой react/redux/saga — это тот набор, который мы используем в своих проектах, и показавший свою эффективность на длительной дистанции. Я не уверен что имеет смысл комментировать зачем redux и почему мы берём его на вооружение. Но, на счёт redux-saga, часто слышу критику и, зачастую, эта критика исходит от людей, не достаточно хорошо ознакомившихся с предметом этой критики. Скажу лишь то, что я тоже изначально настороженно относился к этой библиотеке, но когда изучил её получше, осознал что с таким подходом работать очень удобно, как и с точки зрения тестирования, так и с точки зрения чтения кода.
— это и есть основные принципы, которые получили своё отражение в реализации.
Из особенностей серверного кода, отмечу, что для защиты от race conditions используется мапа ReentrantLock’ов — на игровую сессию свой ReentrantLock. Реализацию синхронизации с клиентским временем я решил не делать, а просто генерировать время хода на сервере.
После первого фидбека относительно UX с предложением поправить и добавить некоторые вещи, я также решил переписать фронтенд на функциональных компонентах с хуками. Давно хотелось прощупать этот подход и он мне понравился. Авторы reactjs в документации подчеркивают, что этот подход минимизирует кол-во ошибок совершаемых программистами, связанных с жизненным циклом компонента при использовании стиля, основанного на классах и callback'ах — и, мне кажется, это важный момент. И если вы ещё не используете новый подход в своей работе — я рекомендую вам его попробовать.
Нижеприведенный фидбек я получаю неделю спустя:
Мы с коллегами внимательно посмотрели новый вариант задания.
По результатам я решил дальше не продолжать процесс.
Ниже фидбек по заданию от нас:
— код запутан, понимать сложно, гораздо сложнее, чем могло бы быть
- оверинжиниринг в части frontend, цитирую нашего frontend разработчика
…
Видимо задание сделано на тех технологиях, которые знал кандидат, хотя мы никакого redux и redux-saga не просили при этом CSS, который руками написан – там просто заоверрайжены стили из библиотеки это плохой способ писать CSS
…
С учетом сложности codebase в нашем продукте нам важно, чтобы простые вещи кандидат делал просто.
Спасибо Вам за время и усилия!
Конечно, хотелось бы иметь концептуальную дискуссию, иметь возможность защитить те или иные решения. Я принимаю комментарий относительно CSS, но совершенно не понимаю остальных. Оверинжиниринг ли это? Или я столкнулся с технической незрелостью интервьюеров? В принципе, здесь любая оценка будет очень субъективна и по-своему имеет место на существование.
Задумываясь над смыслом вопроса — Что такое сложный код? Мне кажется, что ответ на этот вопрос очень простой: это код который сложно читать и сложно менять. Я, как и многие, предпочитаю думать, что мы пишем код для людей, а не для машин. И стараюсь оставить место для мыслительного процесса о композиции кода перед тем как садиться за его написание. Действительно ли проделанная мною композиция усложняет чтение этого кода и претендует на клеймо — оверинжиниринг? Мнение по этому вопросу я бы с интересом увидел в ваших комментариях.