Умение разбираться в коде Чужих или как правильно делать рефакторинг

Illustration of a human character sitting at a computer desk looking frustrated with a speech bubble saying "D'f?" and an alien creature observing from behind. Illustration of a human character sitting at a computer desk looking frustrated with a speech bubble saying "D'f?" and an alien creature observing from behind.

Вступление: Для кого мы пишем код?

“Нет ничего более постоянного, чем временное”.

— Альберт Джей Нок

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

Как вы считаете, что объединяет все программы в мире, что у них есть общего? Если отбросить такие очевидные вещи, что они написаны людьми на каком-либо языке программирования и состоят из символов, используемых в этом языке, то останется еще одно важное свойство. Что, кроме языка, отличает текст программы от текста книги? Да, книга нужна для того, чтобы ее читать, а не выполнять, но есть еще одно важное отличие. Любая программа, чтобы продолжать существование, вынуждена изменяться! И касается это абсолютно всего, начиная с модных мобильных приложений и вплоть до корневых компонентов операционных систем. Можно сказать, что код, создаваемый программистом, как бы адресован другим программистам (или самому себе в будущем). Вы думаете я что-то помню о коде проектов над которыми работал пять лет назад? Да я ничего не помню даже о том, что писал полгода назад!

Реализация программы должна быть проста и понятна не только вам в момент написания, но и каждому, кто впервые с ней встретится

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


Часть 1: Именование

“Мир был таким первозданным, что многие вещи не имели названия и на них просто тыкали пальцем”.

— Габриель Гарсия Маркес

Однажды мой коллега-программист рассказал историю, которая весьма меня позабавила. Речь шла о том, что в некоторых компаниях с очень высоким уровнем безопасности от разработчиков скрывается смысл бизнес-логики, которую они реализуют. Разработчики не должны понимать, что делает их программа. Точнее, они должны этого НЕ понимать. Но как можно написать программу, не понимая, что она делает? Для того, чтобы это осуществить, нанимают специальных аналитиков, уровень доступа безопасности у которых выше, чем у штатных программистов. Эти аналитики все-таки могут понимать, о чем будет программа, но сами они не программируют. У них есть специальные таблички, в которых хранятся соответствия реальных имен с кодовыми. Они просматривают их и дают задания разработчикам. К примеру: “Напиши функцию func_352, которая должна принимать число DEP20, умножать его на коэффициент CF1850 и записывать результат в базу, в ячейку FIELD255”. А на самом деле функция func_352 принимает сумму вклада, умножает на ежегодный процент и записывает в баланс пользователя, но разработчик, создавая функцию, об этом не знает.

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

В литературе по программированию вы можете найти много рекомендаций по именованию различных программных объектов. Основная же идея состоит в том, чтобы имена были осмысленными.

Имя функции должно отражать то, что она делает. Имя переменной должно отражать то, что в ней будет храниться

Достаточно легко подобрать имя для такой простой функции, как в примере с расчетом баланса вклада пользователя. Но как быть, если функция делает куда больше операций? Об этом будет следующая часть.


Часть 2: Принцип Разделения Ответственности

“Простое лучше, чем сложное.

Сложное лучше, чем запутанное”.

— Zen of Python

Представьте себе несчастного сотрудника компании, которому сегодня нужно составить акты приема-передачи товаров, сесть за руль автомобиля и доехать до склада. Забрать товар со склада и подписать документы. Приехать в магазин и разложить товар по витринам. Заказать товар у оптового поставщика и произвести его оплату.

Многие живут в подобном графике — ничего плохого тут нет. Но если бизнес расширяется, и у вас уже не один, а десять магазинов, то не будете же вы нанимать десять подобных сотрудников? Куда логичнее нанять одного бухгалтера, одного водителя, а выкладку товара доверить администратору или продавцу.

Что-то подобное происходит и в разработке программы. До тех пор, пока программа небольшая, подобный сценарный стиль кода показывает себя неплохо, ведь на его планирование тратится минимум времени. Но как только программа разрастается, а любая программа растет, то сценарии ее становятся все сложнее и все запутаннее. Возникают вопросы: “А как бы назвать ту функцию из 200 строчек, чтобы название передавало суть? Может назовем ее “функция СДЕЛАТЬ ВСЕ”?

Будет логично разбить эту функцию на более мелкие функции, каждая из которых будет решать конкретную задачу. Да, подобные советы звучат слишком очевидно, но, возможно вы мне не поверите, многие профессионалы-разработчики до сих пор совершают подобные ошибки. В чем причина? Лень перерабатывать свой код? Или здесь как с английским: мы годами учим его правила, но когда должны что-то сказать, все знания забываются? Неважно, пусть это остается на совести нерадивых программистов, а мы пойдем дальше.

Есть такое замечательное правило:

Если для функции сложно подобрать имя, то ее нужно переписать (разбить на подфункции)

И еще один хороший принцип:

Одна функция — одна задача


Часть 3: Уровни Абстракции

“Чем больше употребляет художник эти абстрагированные или абстрактные формы, тем более он будет дома в их стране и тем глубже будет он вступать в эту область”.

— Василий Кандинский

Любой IT продукт, будь то веб-сайт, мобильное приложение или наручные электронные часы, является наглядным примером многоуровневых абстракций. К примеру, кнопки, которые мы видим на веб-сайте, через интернет запускают функции в коде. Но это лишь начало. Затем код, написанный разработчиком сайта, будет использовать библиотеки языка программирования, на котором написан сайт, допустим, Python. Python в свою очередь будет вызывать библиотеки более низкоуровнего языка программирования, при помощи которого он работает. В данном случае, это язык C++. Дальше библиотеки C++ будут обращаться к языку Assembler, на котором написаны компоненты операционной системы, а они будут совершать системные вызовы, через которые запрос дойдет до процессора и где-то там преобразуется в нолики и единички, а дальше в электрические сигналы, которые и сделают нужное нам вычисление. После чего результат вычисления по той же цепочке вернется обратно.

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

Те же самые законы действуют, когда мы работаем с кодом внутри проекта. Нам необходимо разделять код по уровням абстракции, чтобы разработка не стала слишком сложной. Каждый компонент системы должен “думать” на том уровне, на котором он находится. Функция, записывающая e-mail пользователя в базу данных, не должна знать, откуда пришел этот e-mail: из формы на веб-сайте, из мобильного приложения или из интеграции с социальной сетью. За это отвечают более высокие уровни. Этот принцип работает и в другую сторону: функция для записи e-mail в базу не должна знать, в какую базу она запишет данные. За это отвечают более низкие уровни, адрес и вид базы должен быть обозначен в них. Такой подход с четким разделением уровней абстракции в коде дает огромное преимущество в разработке, позволяет сделать ее проще и быстрее.

Представьте, что коллега говорит: “Нам нужно отправить нотификации на мобильные приложения, но я уже все сделал. Просто вызови мою функцию, передай в нее текст сообщения и Id пользователя”. По-моему, звучит неплохо! Разработчик абстрагировал отправку нотификаций от другой логики приложения. Теперь в проекте есть функция, занимающаяся именно этим, и ее можно использовать. И представьте другую ситуацию. Ваш коллега говорит: “Нам нужно отправить нотификации на мобильные приложения. Я уже сделал это для сообщений об оплате для клиента по имени Билл. Теперь нам нужно сделать сообщение о пополнении счета для Джона - ты посмотри, как я написал функцию, напиши свою, примерно такую же, только поменяй текст и Id пользователя.” Звучит уже не так хорошо. В этом случае придется изучить код коллеги, копировать части кода, изменять их и адаптировать под свои задачи. Это займет намного больше времени и обязательно приведет к дублированию в коде.

Не мешайте конкретику с абстракциями

Работая с кодом в одиночку также необходимо задумываться об уровнях абстракции. Таким образом мы помогаем самим себе в будущем. У программистов есть такое понятие: “закладывать в архитектуру", то есть заранее предусмотреть некую ситуацию, и, когда заказчик попросит что-то сделать, код уже будет максимально готов к таким изменениям. Разделение по уровням абстракции - это один из способов быть готовым к возможным новым требованиям. Нужно сделать нотификации для Айфона? Отлично, мы уже сделали их для Андроида. Теперь остается наладить соединение с новой платформой, а сами нотификации делать не придется - они уже у нас есть. Так это работает.


Часть 4: DRY (Don’t repeat yourself / Не повторяйся)

“Хотя бы свитер ему купи. Смотреть стыдно. Ведь он уже не маленький. Ему стыдно ходить раздетым. Хоть свитер ему купи. Смотреть стыдно. Ведь он уже не маленький. Ему совестно ходить раздетым”.

— Из к/ф “Чеховские Мотивы”, реж. Кира Муратова

DRY - золотое правило программирования, значение которого трудно переоценить. Множество различных приемов программирования, таких как абстракции, наследование, композиция, разделение ответственности, паттерны проектирования и сама парадигма ООП (объектно-ориентированное программирование) во многом были сделаны для того, чтобы избежать повторений в коде. Такая, на первый взгляд, тривиальная задача порой ставит разработчика в очень сложные ситуации. Давайте рассмотрим как это происходит и какие виды дублирования бывают.

Дублирующее значение

Есть такой “анти-паттерн проектирования», как Магическое число. Допустим, переменная price умножается на число 0.13 и записывается в переменную tax. В этом случае можно догадаться, что tax - это коэффициент налога (13% налог НДФЛ). Любой российский разработчик знает величину подоходного налога и присутствие такого числа рядом с переменной price никого не удивит. И все-таки это - Магическое число! Во-первых, разработчик, незнакомый с российскими законами, не поймет откуда оно взялось. Во-вторых, величина налога НДФЛ в любой год может измениться, и тогда придется переделывать все участки кода, где данное число дублировано.

Подобные магические числа, как правило, повторяются в коде неоднократно. Но даже если сейчас оно имеется в единственном экземпляре, то велика вероятность, что в будущем нам снова придется его использовать. Так почему бы сразу не создать из него переменную?!

Любое значение должно присутствовать в коде в одном экземпляре

Исключение можно сделать в том случае, если значение присутствует повторно, но с другим смыслом. К примеру, в одном месте кода 0.13 может быть коэффициентом налога, а в другом месте 0.13 является уже комиссией, увеличивающей стоимость товара. Тогда это должны быть разные переменные.

Дублирующие блоки кода

C данной проблемой нередко сталкиваются даже профессиональные разработчики. Представьте, что перед вами стоит задача создать процедуру, похожую на какую-либо уже имеющуюся. Действительно, легче и быстрее скопировать блок кода, чем выносить его в отдельную функцию и вызывать ее в нужном месте. Но сколько такой подход может создать проблем! Изменение этого блока необходимо применять сразу в двух, а часто в трех и более местах, куда был скопирован код. А если вынести этот код в отдельную функцию, то, кроме очевидной выгоды от отсутствия дублирования, мы приобретаем еще несколько менее очевидных, но не менее важных преимуществ.

Выделение блока кода в отдельную функцию соответствует принципу разделения ответственности. Так как данный блок имеет определенное назначение, то будет правильным сделать из него отдельную процедуру, не перемешивая ее с логикой функций, которые ее вызывают.

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

Не копируйте код! Выносите его в отдельную функцию

Дублирующее поведение

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

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

Здесь две процедуры не только имеют общую стратегию поведения, но также имеют один общий этап. Реализовав поведение этих процедур в абстрактном базовом классе, мы вполне можем переместить в него код их общих этапов, наподобие этапа “Оповещение” в табличке, что будет удобно для совместного использования этого кода. А выделение основных схожих этапов в базовом классе упростит создание новых процедур.

Если вы видите, что создаваемые вами процедуры имеют схожее поведение, то попробуйте определить его в родительском (базовом) классе данных процедур

Автор статьи может стать твоим ментором и научить тебя программировать. Нанять


Заключение: Рефакторинг

“God is in the details”.

— Людвиг Мис ван дер Роэ и другие

Если вы дочитали до этой строки, то уже знаете несколько основных моментов, на которые стоит обратить внимание, анализируя код. По сути, анализ и переработка кода - это и есть части рефакторинга. А одна из главных его целей — сделать код более понятным для чтения, а значит и более логичным, и более качественным.

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

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

Недавно я разговаривал с художником, великолепно написавшим портрет моей знакомой. Я выразил восхищение и спросил, как ему удалось добиться такого сходства? Ответ был прост: “Я переписывал ее лицо снова и снова: три, четыре, пять раз, пока результат меня не начал устраивать”.

Создавая код, стремитесь к простоте и выразительности

Используйте приемы, описанные в этой статье. Изобретайте свои. Помогайте другим разработчикам сделать их код понятнее, ведь часто виднее со стороны. Относитесь к своему коду как к произведению искусства, и тогда он будет работать по-настоящему хорошо!