Чистые, сухие спагетти: почему чистый код и следование принципам DRY и SOLID это не всегда хорошо

2
894
views
Спагетти-код

Перевод статьи «Clean, DRY, SOLID Spaghetti».

Ого, ваш проект уже готов к поставке! Вы гордитесь тем, что ваш код соответствует стандартам стиля, а при его написании вы четко придерживались принципов DRY (англ. «сухой») и SOLID (англ. «цельный», «неразрывный»). Вы провели тесты, разделались с багами, отполировали пользовательскую и API-документацию. Ваш код так чист, что аж блестит!

У меня для вас плохие новости. Несмотря на все это, ваша кодовая база может быть в ужасном состоянии.

В чем же дело?

Скажу сразу, что я не имею ничего против тех вещей, которые назвал. Принципы TDD, DRY, SOLID (и всех других модных сокращений) заслуженно занимают свое почетное место. Документация, стандарты и стиль имеют большое значение. Если вам действительно удалось все это реализовать, вы заслуживаете, чтобы вас одобрительно похлопали по спине.

Но вместе с тем, бывает легко увлечься всеми этими аббревиатурами и статьями «Топ-10 способов написания чистого кода», забыв о том, зачем, собственно, мы все это делаем. И если вы не вникли в предназначение этих вещей, то все вышесказанное в конечном итоге отправится в null.

Независимо от своего предназначения, языка или методологии, все программное обеспечение должно соответствовать двум критериям:

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

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

Но чистые, сухие, плотно сбитые спагетти это все равно спагетти. («But clean, DRY, SOLID spaghetti is still spaghetti». Здесь обыгрывается название принципов и запутанного, «спагетти»-кода, – прим. перев.). Причем, это спагетти в своем наихудшем виде! Вы, вероятно, знаете, о чем я говорю: вы варите спагетти, сливаете воду и оставляете их в кастрюле на бог знает сколько времени. Они определенно чистые. И к тому времени, как вы о них вспомните, станут несъедобной сухой массой. Этот плотно сбитый блин из спагетти будет невозможно разорвать.

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

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

Пустыня тоже сухая

"Сухой" спагетти код

DRY: Don’t Repeat Yourself

«Не повторяйтесь».

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

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

Применение принципа инверсии зависимостей из SOLID несет с собой те же риски. Мы можем абстрагировать все до такой степени, когда почти каждая функция занимается вызовом других функций.

А это не выгодно никому и ничем. Это удар по производительности (слыхали о кэш-промахе?). Читаемость стремительно падает. Чтобы разобраться в коде, требуется гораздо больше времени.

Исправить такую ситуацию непросто. DRY это важный принцип, но применять его нужно с осторожностью. Прежде, чем что-то абстрагировать, следует обдумать стоимость этого действия. Затем следует тщательно выбрать способ абстрагирования этого функционала. Старайтесь сделать так, чтобы читателю было легко идти по следу.

Уборка на месте преступления

Слишком чистый код

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

Слишком мало комментариев

Возможно, самый худший вариант – удаление (или даже ненаписание) комментариев, раскрывающих ваши намерения. Многие люди приводят аргумент, что, мол, «со временем комментарии и код рассинхронизируются». Многие стандарты гордо заявляют, что комментарии должны быть редким явлением (если их вообще стоит делать). «Пишите самодокументирующийся код!» – призывают они.

Но проблема в том, что никакой код не сможет ответить на вопрос «почему?» человеку, не знакомому с этим кодом. Я верю в принципы комментирования, раскрывающего намерения (Commenting Showing Intent, CSI), и придерживаюсь этих принципов. Если кратко раскрыть их смысл, каждый логический блок должен иметь комментарий, описывающий, для чего предназначен этот блок, т. е., отвечающий на вопрос «почему?» для этого блока. Мы не можем полагаться на интуицию в плане определения намерений. Пока мы пишем код, нам все кажется очевидным. Но в действительности никто не может читать наши мысли.

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

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

Слишком много ужасно подобранных имен

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

Но что, если все заходит слишком далеко?

RtlWriteDecodedUcsDataIntoSmartLBlobUcsWritingContext();

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

Удерживайте баланс между описательными и читаемыми именами.

И кстати, если вы пользуетесь венгерской нотацией (Systems Hungarian) в вашем коде, прекращайте немедленно. Прежде, чем делать что-то еще, используйте «найти и заменить», чтобы избавиться от всех этих жутко бесполезных префиксов. Чарльз Симони, предложивший эту систему именования, имел в виду вовсе не это. (А вот Apps Hungarian, в отличие от Systems Hungarian, это самодокументирующиеся имена в действии. Обратите внимание!).

Слишком мало… всего

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

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

Слишком мало пустого пространства

Наконец, пожалуйста, не минимизируйте свою рабочую кодовую базу. Никогда. Точка. Это не чистота, это суицид в плане поддерживаемости. На самом деле, за исключением особых требований бизнеса (объективных, с доказанной необходимостью) в отношении поставляемого кода, это просто плохая идея, как на нее ни посмотри. Раньше или позже кому-нибудь понадобится прочесть этот код, чтобы разобраться с багом или скопировать функционал. Будьте добры к этому человеку – не минимизируйте свой код.

Когда SOLID становится глупостью

Когда SOLID становится глупостью

Принципы SOLID в объектно-ориентированном программировании невероятно полезны. Тем не менее, не стоит вслепую следовать ни одному из них.

История, которую вы сейчас прочтете, случилась на самом деле. Имена изменены для защиты идиотов.

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

  • Single responsibility (Принцип единственной ответственности). В том коде каждый класс отвечал за что-то одно, причем что-то специфическое. И там было много классов. Собственно, тысячи.
  • Open-closed (Принцип открытости-закрытости). Каждая дополнительная фича добавлялась в кодовую базу в форме расширения, и всегда через наследование. Да, всегда наследование.
  • Liskov substitution (Принцип подстановки Барбары Лисков). Наследующий класс дополнял, а не заменял поведение родительского класса – и так по всей цепочке. Никаких исключений.
  • Interface segregation (Принцип разделения интерфейса). Вам следовало использовать только те функции, которые вам нужны. Все было основано на единственном наследовании, поэтому вам никогда не нужно было импортировать X только потому, что вы использовали Y.
  • Dependency inversion (Принцип инверсии зависимостей). Все было абстрагировано. Буквально все. Этот код был data-driven до предела.

Вау, 100% соответствие SOLID! И, тем не менее, это был неподдерживаемый кошмар. Каждый класс наследовался из одного семейного дерева, порой это наследование уходило на сотни слоев вглубь. Чтобы написать один класс, вам нужно было вычитать предыдущие 99 классов. И если выше по цепочке кто-то исправлял баг или проводил оптимизацию (причем никто никогда не утруждался внести это в документацию), ломалась вся последующая цепочка. Таким образом, код, который прекрасно работал в версии 3.2, мог оказаться совершенно сломанным в версии 3.3.

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

Неужели во всем этом были виноваты принципы SOLID? Конечно, нет! Я лишь хочу показать на примере, что SOLID это не волшебная палочка в том, что касается поддерживаемости. Эти принципы следует применять выборочно, руководствуясь здравым смыслом. Любой программист с опытом в ООП знает, что описанная мной структура наследования это плохое проектирование. Но большая open-source команда, отвечавшая за этот проект, именно так и работала. Их код полностью соответствовал SOLID и DRY, но я все равно считаю, что это была серьезная заявка на звание худшего кода в истории.

TDD: катастрофа через тестирование

Разработка через тестирование

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

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

Как и раньше, я не говорю, что TDD нужно вовсе отбросить. Я скорее хочу предупредить тех, кто его практикует (да и всех, кто пишет тесты). Чтобы предотвратить подобное туннельное зрение, я рекомендую следующее.

Пишите свои продакшен-тесты вслепую, ПОСЛЕ того, как ваш код написан и несомненно работает. Под «вслепую» подразумевается «не смотрите в свой код, когда пишете тесты». Разбейте на бумаге, что, как вы помните, должен делать ваш код, и чего он делать не должен. Сделайте список и напишите тест для каждого пункта. Убедитесь, что у каждого теста есть четкие условия прохождения и непрохождения. Будьте безжалостны.

Для меня такой подход приводит к следующему:

  • Зная, что мои продакшен-тесты еще не существуют, я фокусируюсь на написании хорошего кода, а не на попытках обойти своих маленьких, заранее известных вратарей.
  • Таким образом я вылавливаю множество багов и недостатков в дизайне. И, говоря «множество», я имею в виду именно это. Больше половины моих фатальных багов и edge cases проявились в ходе слепого тестирования.

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

Приоритет стиля над функцией

Стиль не должен преобладать над функцией

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

  • Не будет ли тяжелее понять вашу мысль из-за ограничения длины строки в 80 или 120 символов? (Да, так бывает, пускай и редко). Делая исключение из этого правила, можно повысить читаемость и/или поддерживаемость.
  • Любите строчные, бесскобочные условия? Ошибки тоже их любят. Будьте осторожны в использовании стильных сокращений. Порой скобки просто безопаснее.
  • Вам нужно сделать обходной маневр в своем коде, чтобы выдержать какие-то правила стиля? (Ага, и так бывает!) Остановитесь! Стиль сам по себе никогда не должен приводить к функциональным изменениям вашего кода.
  • Вы ведете крестовый поход против тройных условий? Или, наоборот, любите их, потому что благодаря им кажетесь альфа-хакером? Используйте те инструменты, которые обеспечивают наилучшее сочетание читаемости, поддерживаемости и функциональности. А свои пристрастия отставьте в сторону.

Здравый смысл никто не отменял

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

Спагетти в программировании это уже достаточно плохо. Но слипшиеся (SOLID), сухие (DRY) и чистые (clean) спагетти? Их распутать сложнее всего!

2 КОММЕНТАРИИ

  1. Вредная статья. Всё, что требует SOLID и TDD — всё поставлено с ног она голову. Автор статьи имеет низкую квалификацию и не понимает то, о чём пишет.

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here