[Перевод] Обеспечение качества кода в масштабных проектах

6b2c71a0bc4443c8be959b6fbed01721.jpgКогда осенью 2012 года я пришёл в Airbnb, то здесь мягко выражаясь, был некоторый разброд и шатание. Некоторое время назад компания начала расти и развиваться огромными темпами. В первую очередь это выражалось в объёмах трафика и транзакций. Чтобы справляться со всем этим, очень быстро увеличили и штат разработчиков. За год до моего прихода в группе было 16 человек, со мной было около 40, а сейчас уже свыше 130. И одной из главных проблем, вызванной всеми этими процессами, стало сохранение качества кода в стремительно увеличивающемся и усложняющемся проекте.Оглядываясь назад, мне кажется, что это был переломный момент в истории нашей компании. Взрывное развитие стало причиной возникновения ряда технических и культурных вызовов для Airbnb, которые в большинстве своём пересекались друг с другом. Нам и раньше приходилось решать многочисленные трудные задачи, но в целом они были связаны с:

• масштабированием цельных приложений на Ruby on Rails, которые создавались безо всякого расчёта на последующую масштабируемость, • и увеличением группы разработчиков для решения предыдущей задачи.

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

Сейчас я работаю в группе, занимающейся разработкой модуля для обработки платежей. Поэтому для нас качество кода, его устойчивость и удобство сопровождения имеют критически важное значение. Учитывая объём транзакций, недопустимы даже маленькие ошибки. За последний год мы проделали большую работу по выработке правил и методик, позволяющих писать код быстро и в то же время качественно. В первую очередь, речь идёт о об установлении и соблюдении при кодинге специфических стилей и условий, регулярном рассмотрении коллегами (peer review) и тестировании.

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

У себя в Airbnb мы улучшали согласованность кода в течение двух лет. Сначала мы разработали руководство по стилю (было несколько версий этого руководства). Самые детальные руководства по JavaScript и Ruby, также есть по RSpec, дизайну API, проектированию услуг и т.д. Несмотря на то, что эти руководства основаны на наших специфических требованиях и условиях, многие из них теперь используются как другими коллективами, так и одиночным разработчиками. Конечно, потенциальная польза этих руководств зависит от типа приложения, над которым вы работаете, но если вы разрабатываете на JavaScript или Ruby, то рекомендую в любом случае ознакомиться с ними. Хотя бы для вдохновения.

Некоторые правила приняты нами исключительно произвольно. Например, по отступам (табуляция и пробел), или на какой строке располагать открывающую фигурную скобку. Эти и прочие моменты — дело вкуса. Тут важно, чтобы правила соблюдались всеми причастными лицами. С другой стороны, в ряде случаев возможны варианты.

Возьмём длину строки. Мы предпочитаем делать строки короткими и наглядными. Короткие строки не только более читабельны, но и позволяют задавать операторы в более простой форме (особенно, когда они используются с описательными именами переменных). Методы, состоящие из серий коротких простых операторов, легче для восприятия и модифицирования. Даже диффы в результате точнее показывают, что поменялось между коммитами. А поскольку мы пишем тестопригодный код, содержащий небольшие, лаконичные методы, то это подталкивает к созданию очень «чистой», модульной и лёгкой для понимания кодовой базы.

Рассмотрим пример:

usernames = Users.where (: status => 1, : deleted => false).map{ |u| u.first_name.downcase }.reject{ |n| n == «john» }.map{ |n| n.titleize } Допустим, нам нужно поменять регистр. Дифф покажет нечто подобное:

 — usernames = Users.where (: status => 1, : deleted => false).map{ |u| u.first_name.downcase }.reject{ |n| n == «john» }.map{ |n| n.titleize }

+ usernames = Users.where (: status => 1, : deleted => false).map{ |u| u.first_name.upcase }.reject{ |n| n == «JOHN» }.map{ |n| n.titleize } Трудно сказать навскидку, что именно поменялось, не пропарсив строку у себя в голове. А если бы код был изначально написан так:

users = Users.where (: status => 1, : deleted => false) usernames = users. map{ |user| user.first_name.downcase }. reject{ |name| name == «john» }. map{ |name| name.titleize } То разница в диффе была бы куда нагляднее:

users = Users.where (: status => 1, : deleted => false) usernames = users.  — map{ |user| user.first_name.downcase }.  — reject{ |name| name == «john» }. + map{ |user| user.first_name.upcase }. + reject{ |name| name == «JOHN» }. map{ |name| name.titleize } Забавно, но изначально те из нас, кто продвигать такой подход, поначалу столкнулись с сопротивлением коллег. Пришлось проявить настойчивость, и в результате это вошло у всех нас в привычку — делать короткие строки. Совсем экстремальные примеры на эту тему можно увидеть в руководствах Joint Strike Fighter C++ и JPL C Coding. Очевидно, что эти стандарты избыточны для большинства потребительских веб-приложений, поэтому всегда соотносите уровень проекта и цели, которых хотите достичь соблюдением тех или иных правил. Важно соблюдать баланс.

Как я упоминал, мы создали руководства также для разработки API и сервисов. Несмотря на то, что заранее не всегда удаётся однозначно прописывать подобные вещи, консолидация общих знаний команды в единый документ оказала неоценимую пользу.

Рассмотрение коллегами Регулярный аудит кода своих коллег стал вторым краеугольным камнем повышения согласованности кода. Самое главное преимущество этого процесса в том, что он позволяет быстро и эффективно обнаруживать всевозможные баги до того, как они попадут в очередной релиз. Но есть в перекрёстном аудите и множество других мелких положительных моментов. В течение последнего года мы проводим аудит для каждого внесённого в код изменения.Мы стараемся вовлекать в аудит всех, кто так или иначе причастен к данной части кода или функционала. В результате по наиболее нетривиальным pull request возникает как минимум одно-два независимых обсуждения. Иногда процесс разбора заходит так далеко, что некоторым приходится изучать ранее неизвестные для себя вещи (например, процессинг кредитных карт, внутреннюю организацию баз данных, криптографию и т.д.). Если изменение в коде затрагивает какой-то большой важный вопрос, то мы создаём соответствующие рекомендации и документацию. Хочу подчеркнуть, что при рецензировании у нас не допускается апеллирование к своему статусу или должности. Ценится вклад любого члена команды, и в бой выкатываются наилучшие решения. Всё это является и хорошей школой для новичков, к слову говоря.

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

Вот пример ситуации, когда главную роль играет не стиль кодинга, а мудрый подход:

rus_users = User.active.select{ |u| «RUS» == u.country } puts «There are #{rus_users.length} RUS users today» и

rus_users = User.active.where (: country => «RUS») puts «There are #{rus_users.count} RUS users today» Первый вариант более ресурсоёмок, и при больших объёмах данных может привести к катастрофическому падению производительности. Добавление select к объекту User.active означает, что Rails обратится к MySQL, вызовет и инстанцирует всех активных пользователей, положит их в массив, итерирует его и выберет пользователей, чья страна соответствует RUS. И всё это лишь для того, чтобы посчитать их.

Во втором примере мы тоже начинаем с объекта User.active, но затем применяем фильтр where. Первая строка не инициирует какие-либо запросы к базе данных. Когда во второй строке, когда мы запрашиваем счётчик, Rails лишь делает запрос SELECT COUNT (*) и не заморачивается вызовом строк или инстанцированием моделей.

Вот ещё пример:

плохо:

amount = Payout. where (: successful => true). where («DATE (timestamp) = ?», Date.today). inject (0) { |sum, p| sum += p.amount } хорошо:

amount = Payout. where (: successful => true). where («DATE (timestamp) = ?», Date.today). sum (: amount) В первом примере мы ограничиваем область поиска одним днём, но даже в этом случае нужно вызвать и инстанцировать большое количество объектов всего лишь для суммирования по одному полю. Во втором примере суммированием во время поиска занимается MySQL, который потом просто возвращает нужное нам значение. В целом это не создаёт дополнительной нагрузки на MySQL, мы не генерируем ненужный сетевой трафик и избегаем потенциально огромных вычислений в Ruby.

Приведённые примеры демонстрируют, как мы улучшаем отзывчивость нашего сайта и даже предотвращаем его падения. Рецензирование хорошо ещё и тем, что, обсуждая какую-то конкретную часть кода, сотрудники периодически поднимают самые разные темы, вроде структуры баз данных или генерации финансовых отчётов. То есть вокруг кода постоянно возникают живые и продуктивные диалоги, в ходе которых мы учимся сами и обучаем других.

Тестирование Я не буду углубляться в этот вопрос, об этом очень хорошо написано в одном из постов нашего блога. Скажу лишь (и вряд ли это будет для вас чем-то новым), что тестирование невероятно важный процесс с точки зрения обеспечения высокого качества кода, удобного в сопровождении. Однако дело не в максимальном охвате кода тестами, главное заключается в создании культуры тестирования, чтобы это стало условным рефлексом у каждого члена команды. Когда в привычку входит написание тестов, то и создание тестопригодного кода также превращается в привычку.Заключение Подведу итоги всему сказанному.Все члены профессиональной команды разработчиков, какого бы размера она не была, должны писать код в одном, заранее утверждённом стиле. Это помогает широко использовать удачные подходы и методики, облегчает сопровождение и обслуживание кода другими людьми. Лучшим началом будет создание руководств по стилю кодинга. Даже обманчиво простые и очевидные вещи могут сильно влиять на качество кода, его устойчивость и простоту рефакторинга.

Необходимо активно и конструктивно рецензировать код друг друга. Как минимум один человек, помимо автора, должен просмотреть все диффы. Доброжелательное внесение предложений и их рассмотрение является залогом сплочения коллектива и роста профессионального уровня. Это помогает новичкам быстрее и полнее учиться, обретать полезные навыки и привычки. В конце концов, это может уберечь ваше приложение от серьёзного падения.

Важно развивать сильную культуру тестирования, не идти в этом на компромиссы или «срезать дорогу». Чем больше мы тестируем, тем больше это превращается в привычку. В результате можно даже полюбить этот процесс.

© Habrahabr.ru