Что я ищу, когда провожу code-review
Предисловие
Так случилось, что я работаю в конторе с достаточно высокой культурой разработки. И как раньше (когда я был новичком) старший разработчик проводил код-ревью моего кода, так теперь в нашем отделе мы проводим перекрёстный код-ревью.
Я стараюсь никогда не упустить возможность “поревьюить”, потому что это даёт уникальный опыт. Посмотреть на код со стороны, вникнуть в него и подумать “почему я бы сделал по-другому” в конечном итоге улучшает и мой собственный код.
Я решил выписать основные пункты, по которым провожу код-ревью. Для себя будущего и для всех остальных настоящих. Особенно если когда-нибудь я буду смотреть в ваш код.
Буду очень сильно давить в сторону ruby/rails из-за того, что этими инструментами я пользуюсь постоянно.
Заранее должен предупредить, что никаких книг на тему я не читал, так что смело можно это ставить мне в вину. Всё “на живом опыте”. Ну разве что когда-то давно читал “Чистый код” дядюшки Боба.
По теме “поддерживаемого кода” рекомендую всем посмотреть доклад “Ментальное программирование”. Очень многие доводы в этом посте взяты из этого доклада (и личного опыта).
Цель код-ревью
Основной целью код-ревью я считаю поддерживаемость кода. Но не его работоспособность, которую должен обеспечивать сам разработчик и отдел QA/QC (кстати, кто-то понимает разницу?).
Задачи код-ревью
Соответственно, задачами код-ревью являются:
- Обеспечение читаемости кода
- Обеспечение модифицируемости кода
Что я ищу во время код-ревью
Корректное и полное именование
Когда-то фразу “работа программиста — придумывать названия переменным” считал шуткой. Но давно уже перестал. А всякие “как корабль назовёшь — так он и поплывёт”… Не буду продолжать.
А суть-то проблемы в следующем:
- Не надо стесняться длинных названий. Предел длины строки от 80 до 120 символов (в зависимости от соглашений), из которых можно где-то треть (30-40 символов) потратить на название переменной. И это нормально! Надо в названии переменной написать целое предложение — пишите. Лучше написать больше, чем меньше. Прямо сейчас. Не потом. Даже не через пять минут.
- Не надо бояться тратить время на придумывание названий. Это тоже работа программиста. Я бы даже сказал, что называть вещи своими именами — важнейший навык, который нужно применять везде и всегда.
- Нет нормальных однобуквенных названий. И не может быть. Даже счётчики
i
— это отстой. Их надо называть нормально.
Приведу два примера.
Пример про наименование переменной
Вот за такое я не очень люблю императивный подход:
@users = User.all
@users = @users.map(:name)
Я сторонник того, что нужно стараться работать с переменными так, будто они иммутабельны. И придумывать новые названия для новых значений, а не переписывать переменную по 10 раз. Нет, конечно ruby — не clojure. Но никто же не запрещает писать в иммутабельном стиле хоть на каком-то уровне?
Например, так:
@users = User.all
@user_names = @users.map(:name)
Почему это проблема именования? Потому что причина переназначения переменной чаще всего — лень и нежелание придумывать новые названия.
Пример про наименование теста
test 'should not allowed' do
Все поняли, о чём это? Вот и я не понял. А подразумевали на самом деле вот это:
test 'should not let User with role "guest" create and update post' do
Тесты
Несмотря на то, что я писал в посте про TDD, тесты всё-таки должны проверять корректность программы.
Однако жизнь показывает, что проверить покрытие сценариев тестами во время ревью сложно. Просто потому, что держать в голове весь код невозможно.
Поэтому я предпочитаю проверить наличие тестов (хоть каких-нибудь), и их содержимое.
Основных критерия проверки конкретного теста два: хрупкость и осмысленность.
Что я считаю хрупкими тестами? Ну, например, проверка вёрстки при рендере. И вообще, всё, что может поменяться не сломав при этом логику приложения. Вроде http-заголовков (кроме некоторых случаев).
Осмысленность? Например тестирование стабов или mock-объектов. Ну и, конечно, тестирование библиотечного кода. Регулярно находятся люди (включая меня когда-то), которые любят потестировать работу валидаций в рельсовых моделях.
Соблюдение модульности и уровней абстракции
Пропустим любителей написания всего кода приложения или всех тестов в одном файле.
А лучше напишу вот про это: каждый модуль должен иметь ограниченную но полную ответственность и не лезть в работу нижних уровней.
Например, сырые sql-запросы в контроллере — сомнительная практика. Как и http-запросы в рельсовой модели.
Многословность
Пока лень описывать этот пункт. Хотя он — важнейший из списка (после, конечно, наименований) и при этом самый неочевидный. Потому и объяснять сложно.
Отсутствие сомнительных практик
Мета-программирование а-ля ruby
Есть в ruby такие вещи как method_missing
, определение методов на ходу и даже классов на ходу, которые наследуются от классов, созданных на ходу, и инклюдят модули, сгенерированные на ходу. Ну и в ту же степь переопределение классов, методов и прочего.
Не считая того, что это всё будет выдавать непонятные ошибки прям как в Rails, так ещё и читается (и модифицируется) это всё тяжело. Так что когда я вижу подобные конструкции, использованные без особой необходимости, я говорю “нинада так”.
Макросы
Как-то я пробовал ковырять гниющий труп common-lisp’а. И решил я посмотреть содержимое библиотеки одной.
Зачем-то мне нужно было понять, как конкретная функция работает. Так вот на разбор макроса в три строки (вместе с названием) у меня ушло полтора часа. Я понимаю, что это можно было сделать быстрее при наличии интеллекта. Но зачем? Гораздо проще было бы отказаться от макросов совсем. А если это невозможно, то делать их максимально топорными.
Та же история у меня приключилась, когда я полез смотреть кишки Rails эликсировского Phoenix’а. Я был очень огорчён объёмными макросами, которые ссылались друг на друга.
Императивщина
some_variable = 1
if some_condition
some_variable = 2
else
some_variable = 3
end
return some_variable
Без комментариев.
ООП
ООП? В 2k17? Серьёзно?
Во-первых, ООП сосёт. Во-вторых ООП тоже сосёт. А в третьих…
Особенно ООП на классах (инкапсулируй, наследуй, полиморфи). Не зря группировка четырёх особо опасных личностей написала целую книгу по тому, как заставить ООП не сосать. Не то чтобы у них ничего не вышло, но выходом из ситуации я бы это не назвал…
Так что обычно я стараюсь остановить любые попытки запиливать лютое ООП в проекте.
Конечно, иногда ООП (на классах, чтоб их) — действительно неплохой выход из ситуации. Но чаще всего это только делает вполне понятные и очевидные идеи совершенно нечитаемыми, не говоря уже про модифицируемость.
Злоупотребление ФП
Ну здравствуйте, дороие мои любители анонимных функций. Как вам такое, а?
maybe_iteration(some_variable, anonymous_function1(params1) {
something(anonymous_function2(params2) {
something_else(anonymous_function3(params3) {
get_some_result(params1, params2, params3);
});
});
});
Ну и примерный аналог для рубистов:
some_variable.something do |param1|
param1.something_else {|param2| param2.what_is_this?}
end.another_fancy_method {|param3| param3.is_this_a_good_code?}
Понравилось? Вот и мне не очень.
Неявные code-conventions
Не все соглашения можно проверить линтерами, увы. Так что приходится искать самому.
Отсутствие дублирования
Немного холиварная тема. Но всё же.
Есть два уровня дублирования, которые можно опустить: дублирование на уровне кода и дублирование на уровне логики.
Дублирование на уровне кода
Ну, тут всё просто. Видим два использования одного и того же участка кода, константы, чего-угодно — выселяем в отдельную переменную/функцию. Не буду приводить примеры, ибо всё очевидно.
Конечно, некоторые считают погоню за дублированием “предварительной оптимизацией”. Я сторонник “лучше сейчас вынести, чем потом где-то пропустить”.
Дублирование на уровне логики
Вот тут всё не так очевидно. Хотя проблему видел несколько раз.
Простой (утрированный) пример на уровне Rails:
# app/models/post.rb
class Post < ApplicationRecord
validates :title, presence: true
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def create
@post = Post.new(params)
if @post.title.present? && @post.save
redirect_to post_path(@post)
else
render :new
end
end
Явное дублирование логики. Ведь мы уже поставили валидацию в модели. Зачем ещё и в контроллере проверять?
Да, пример высосан из пальца, но аналогии провести не составит труда.
Отсутствие ненужных велосипедов
Достаточно специфичная для рубей тема, ибо обилие гемов. Но, думаю, в конечном итоге для всех языков применимо.
А суть проста: если есть готовое решение (библиотека или даже готовое приложение), которое будет дешевле и быстрее, чем велосипед, то зачем пилить велосипед?
Не, ну я понимаю, это весело. Но совершенно бессмысленно. Тем более, что поддерживать это изделие нужно будет своими руками, а не руками бесплатного опенсорца.
Нужно ли ревьюить собственный код?
Да. Определённо.
Конечно, “сам себе ревьюер” — не решение проблемы собственного говнокода. Но это даёт возможность неплохо пошевелить шестерёнками в голове. Ну и всё-таки отсеивает часть говнокода.
Методика определения корректности кода
Когда я провожу код-ревью, я выполняю одновременно три действия:
- Проверяю код на соответствие всем пунктам (см. выше)
- “Компилирую” его в голове. Как бы мне ни хотелось, чтобы код был безошибочным, иногда избавиться от обидных ошибок можно только так.
- Пытаюсь понять, почему код был написан именно так, если что-то зацепило мой взгляд. Ведь, возможно, для конкретного костыля были причины. Особенно сложно выполнять этот пункт во время ревью своего кода.