Всем привет! Каждый, кто стремится к совершенству, формирует для себя ряд правил, которые помогают ему двигаться в выбранном направлении. В этой статье я хочу немного рассказать о простых правилах, которые использую на практике — минимальность строк кода и одинаковая по вложенности структура равнозначных операций. А также показать, каких результатов они позволяют достичь.
Ничего не ново под луной, и наверняка вы встречали эти правила в различных источниках, а, возможно, уже используете в своей работе. Что ж, тогда вы сможете сравнить наши подходы между собой. Итак, начнем исследовать код и попытаемся сделать его лучше.
Лучше меньше
Возьмем вот такой код из нашего открытого проекта на GitHub, который ребята из R&D Terrasoft развивают в свободное время. Задача этого фрагмента: получив на вход URL и сериализованные данные для запроса, выполнить HTTP-вызов сервиса, скрыв от клиента весь транспортный слой.
Код содержит всего 17 строк. Теперь давайте посмотрим на один из структурных критериев сложности функции — количество отступов. На строке 12 мы видим вложенность
Конечно, приведенный пример — не единственный возможный в данном случае, но и он показывает одну интересную особенность: без изменения логики работы кода теперь вместо 17 строк кода мы имеем 26.
Как же так получается, что мы используем практику упрощения кода, а в итоге получаем его структурное усложнение?
Давайте разберемся с этим явлением подробнее и вспомним правило KISS: в коде мы использовали конструкцию языка C# - Extensions, добавили отдельный класс и ввели не общеизвестную функцию в наш проект. Согласитесь, это явно немного сложнее, чем просто написать вызов нескольких framework native методов, и это можно считать не очень хорошим решением. Но с другой стороны, мы упростили чтение изначальной функции, уменьшив её вложенность, что можно засчитать в плюс.
Сравнивая такие плюсы и минусы, я неоднократно приходил к выводу, что в каждом конкретном примере объективные показатели вывести довольно сложно. Поэтому, как правило, я использую принцип минимальности финального кода для решения задачи.
Как получилось, что пример на 17 строк лучше примера на 26, несмотря на то, что он содержит «запахи»? Теперь давайте посмотрим не только на функцию ExecutePostRequest
, но и на дополняющую её функцию ExecuteGetRequest
. Её синтаксис такой:
Как видно, в ней такая же операция обработки ответа от сервера. А теперь давайте вспомним ещё одно правило — YAGNI, или «Выделяйте функцию тогда, когда она вам понадобилась второй раз», и ещё вспомним известную аббревиатуру DRY. Теперь, если мы выделим код обработки ответа в отдельную функцию, получим вот такой результат:
Я выделил одинаковыми цветами логически идентичные блоки до и после рефакторинга. Если внимательнее посмотреть на пример «после», мы увидим, что дублирование кода уменьшилось, синтаксис стал более коротким.
Также заметно, что код вспомогательной функции отличается от первого примера и все еще содержит тройной отступ. Я бы хотел акцентировать внимание на том, что при выборе того, какой именно код выделять отдельно, учитывайте, что итоговый вариант дает более стройную форму кода, с одной стороны. С другой же — не вносит значительной сложности в понимание кода, поскольку «проблемный» участок занимает всего 9 строк и локализован в рамках одной функции. Ну и, конечно, хочу отметить, что количество строк кода в ходе изменений уменьшилось.
Мы рассмотрели размер кода в строках, то есть по вертикали, а теперь давайте обратим внимание на вторую размерность нашего кода — количество символов в строке и количество отступов в её начале. Для примера я возьму ещё одну функцию нашего экспериментального класса и попробую её улучшить:
Если мы вспомним, что каждый уровень отступов начинает новый логический уровень реализуемого алгоритма, то в этой функции мы можем выделить первичных три блока:
- создание HTTP-запроса;
- добавление к запросу данных;
- получение ответа и запись его в файл.
Давайте теперь обратим внимание на то, что, описывая логику работы этого метода, мы повторили соотношение кода в каждом из выделенных фрагментов к количеству символов для объяснения его работы — от очень простого к самому сложному.
Сопоставимые по размеру логические блоки в рамках одной функции
Это второе правило, которое помогает оценивать структуру кода. Посмотрим, как достичь этого на нашем примере. Для начала посмотрим, добавляет ли исходная функция ценности между операциями 1 и 2. Ответ: нет, она просто вызывает системные конструкции без какой-либо трансформации или обработки данных.
Поэтому логику записи данных в запрос сразу вынесем в функцию создания запроса:
Теперь у нас в функции уменьшилось количество действий:
- создание HTTP-запроса с данными;
- получение ответа и запись его в файл.
Но у нас все еще осталась проблема: первая и вторая операция отражены разной структурой кода (вложенности). Вспомним, как мы решили проблему чтения строковых данных в первом примере и вынесем процедуру обработки request-a в тот же самый класс Extension. Теперь он выглядит вот так:
Я очень хочу обратить внимание на то, что задача этого класса — упростить код вызова системных функций в нашем проекте, и мы видим, что одинаковые по уровню абстракции функции класса имеют похожую внутреннюю структуру. Таким образом, мы можем применять правило сопоставимости структур кода не только внутри функции, но и на уровне классов.
Если вы вспомните всеми любимые шаблоны проектирования, то обратите внимание на то, что правила структурной схожести там видны уже на уровне связанных классов.
Однако давайте вернемся к нашему примеру и посмотрим, во что превратилась исходная функция:
Операций так и осталось две, и теперь они имеют одинаковую структурную сложность. Предыдущее предложение звучит очевидно, но посмотрим, какие изменения стали возможны после того, как мы поработали с этой функцией в других участках кода:
Если до объединения формирования запроса и записи в него данных мы видим структурную разницу методов Get и Post, то уже после мы видим одинаковую структурную сложность этих функций, и это хорошо. После этих операций общий объем файла уменьшился на 17 строк, чем подтверждается принцип «улучшения кода уменьшают его объем». После более детального рассмотрения полученного кода заметна алгоритмическая схожесть, что уже наталкивает на мысль о применении DRY для дальнейшего улучшения. Подробнее о том, как развивать это код, и о других критериях оценки кода мы поговорим в рамках следующей статьи из этого цикла.
Выводы
Рассмотренные критерии по минимальности строк общего кода и одинаковой сложности внутренних структур не являются «научными», с одной стороны. Но, с другой — они позволяют легко найти места для оптимизаций.
Вот несколько моментов, на которые стоит обратить внимание в процессе написания кода:
- Не делайте преждевременных абстракций. Вы не потратите много времени потом, но сэкономите много времени сейчас, не придумывая названия и структуру нового кода.
- Всегда учитывайте контекст и полную структуру написанного кода, а не его локального фрагмента. Решение проблемы не на том логическом уровне, на котором она возникла, приводит к появлению сложных решений, стабильность которых зависит от контекста вызова.
- Используйте критерии оценки кода, которые можно легко измерить и контролировать. Внедрение субъективных критериев увеличивает количество времени, которые вы тратите на согласование решений, экономьте его, используя соглашения и правила.
Конечно, описанные приемы не являются истиной в последней инстанции и единственными путями анализа кода или его улучшения. Мы видим, что улучшение показателей приводит к необходимости выполнять одни и те же действия с точки зрения техник рефакторинга. И я хочу отметить, что взгляд на код с разных сторон позволяет упростить процедуру выявления мест улучшения кода, особенно в тех местах, где вы уже не замечаете возможностей к улучшению ввиду привычки.
Если в процессе работы вы используете собственные правила измерения качества кода, пишите в комментариях, будет интересно обменяться опытом.