Перевод статьи Майкла О. Черча — What is spaghetti code?
Самый простой способ для эпитета утратить свое первоначальное значение — это стать чрезмерно широким, начать означать чуть больше, нежели просто “мне это не нравится”. Речь идет о термине “спагетти-код”, который люди часто используют как синоним к понятию “плохой код”. Проблема в том, что не всякий плохой код является спагетти-кодом. Спагетти-код — это особенно опасный и специфический вид плохого кода, и его особое зло заключается в самом способе разработки нами программного обеспечения. Почему? Потому что отдельно взятые люди редко пишут спагетти-код самостоятельно. Скорее, определенный стиль в разработке делает его все более распространенным со временем. Для того, чтобы понять это, нужно рассмотреть первоначальный контекст, в котором было определено понятие “спагетти-код” — ужасное (и в основном архаичное) использование оператора goto.
Оператор goto — это простой и мощный механизм управления потоком выполнения программы: переход к другой точке кода. Это то, что на самом деле делает скомпилированная программа на ассемблере для того, чтобы передать управление, даже если исходный код написан с использованием более современных структур, таких как циклы и функции. Используя goto, каждый может реализовать любой контроль потока выполнения, который только понадобится. Сложно также не согласится, что на данный момент goto является неуместным для исходного кода более современных программ. Исключения для этого правила существуют, но они крайне малы. Более современные языки даже не имеют этого оператора.
Оператор goto делает код трудночитаемым, потому что, если управление программой может перескакивать с одного места в другое, то нельзя с уверенностью сказать в каком состоянии находится программа при выполнении конкретного участка кода. Основанные на goto программы не могут быть легко разбиты на составные части, потому что любая точка в коде может быть кротовой норой к любой другой части кода. В итоге такой код превращается во “все везде” и для понимания даже отдельной части кода, нужно уже разбираются во всей этой путанице, и в последствии это становится невозможным для больших программ. Это можно легко сравнить с миской спагетти, где извлечение даже одной макаронины включает в себя навигацию через большой клубок макарон. Вы не можете просто посмотреть в тарелку и понять какая макаронина с какой спутана, вместо этого приходится кропотливо распутывать весь клубок.
Спагетти-код — это код, где “все везде”, и ответить на такие вопросы, как (а) где реализуется определенная часть функционала, (б) где создается экземпляр объекта и как это происходит и (в) как определить критическую секцию для исправления. Просто назвав пару простых вопросов, уже хочется взглянуть на код, что потребует понимания всей программы. То есть, необходима постоянная диагностика исходного кода, чтобы иметь возможность ответить на простейшие вопросы. Это код, который останется загадкой для тех, у кого не хватит дисциплины проследовать по каждой макаронине от начала и до конца. Это и есть спагетти-код.
Что делает спагетти-код опасным, так это то, что в отличии от других видов плохого кода, этот стал общим побочным продуктом энтропии программного обеспечения. Если код имеет правильную модульную структуру, но некоторые модули низкого качества, люди исправляют плохой код, если это важно для них. Плохой, ошибочный или медленный код, может быть исправлен без изменения интерфейсов. Честно говоря, намного проще определить ошибку в небольших, независимых функциях, нежели в гигантском комке кода, который спроектирован для решения слишком многих задач. Спагетти-код является злом, потому что (а) это очень распространенный подвид плохого кода, (б) его практически невозможно исправить не изменяя функционал, что будет равносильно краху, если есть люди, зависящие от старой модели поведения программы и (г) по причинам, к которым я доберусь немного позже, появление такого кода не может быть предотвращено путем типичных процессов проверки кода.
Причина, по которой я считаю важным отделять понятие “спагетти-код” от более широкого понятия “плохой код”, состоит в том, что многое из того, что делает код плохим, является слишком субъективным. Много конфликтов и грубостей в программном обеспечении совместной работы (или при его отсутствии) являются результатом преимущественно мужской тенденции к высмеиванию неквалифицированной креативности (или ее восприятии, и касательно кода это очень часто предвзятое восприятие): бить претендента по альфа статусу до тех пор, пока он не перестанет приставать к нам со своими некомпетентными идеями. Проблема этой модели поведения заключается в том, что она бесполезна и редко делает человека лучше в том, чем он пытается заниматься. Также есть много отвратительных людей, которые определяют хороших и плохих программистов на основе визуальной составляющей, так что их определение “хорошего кода” сводится к “код, который выглядит так, как и написанный мной”. Я чувствую, что проблема спагетти-кода лучше определена по масштабам, нежели большая, но слишком субъективная проблема “плохого кода”. Мы никогда не достигнем консенсуса в вопросе “пробелы или табуляция”, но все мы сходимся на мысли, что спагетти-код непонятный и бесполезный. Более того, так как спагетти-код является наиболее распространенной и разрушительной разновидностью плохого кода, то большинство причин и предостережений, касающихся этого подтипа, могут быть распространены на другие категории плохого кода.
Люди обычно используют понятие “плохой код” подразумевая “отвратительный код”, но если есть возможность определить почему они считают кусок кода плохим и отвратительным и выяснить возможные пути его исправления, то это уже намного лучше от бОльшей части спагетти-кода. Спагетти-код непонятный и часто совсем неисправимый. Если Вы знаете, почему ненавидите определенный кусок кода, то этот код уже выше по качеству, нежели спагетти-код, так как последний является лишь безликим бредом.
Какие причины появления спагетти-кода? Некоторое время основной причиной спагетти-кода являлся оператор goto, но он потерял популярность и до сих пор пребывает в забвении, поскольку перестал быть значимым. Сейчас причиной служит совсем другое, а именно современная быдлоизация объектно-ориентированного программирования. Особенную роль здесь играет наследование, и, как результат, не продуманные абстракции: использование параметризации, характерной для определенного класса, подразумевая единственный сценарий использования, или добавление ненужных параметров. Я признаю, что утверждение, что ООП, в том виде, в каком оно сейчас практикуется — это спагетти-код, не является неоспоримой точкой зрения. Так же, как в свое время, неоспоримой не считалась вредность использования оператора goto.
Одна из самых больших проблем программного обеспечения для проведения сравнений (будь то сравнение подходов, техник, языков или платформ) является то, что большинство сравнений сосредотачивается на простых примерах. На двадцати строках кода не возможно выявить ничего зловещего, если только эти строчки не написаны с преднамеренным подлым умыслом. Программа в двадцать строк написанная с использованием goto, как правило, вполне приемлема, и может оказаться даже проще, чем написанная без его использования. На двадцати строках набор пошаговых инструкций с некоторой явной передачей управления является очень естественным способом представления программы. Для статических программ (например, платонической формы, которая никогда не будет изменена и не получит технического обслуживания), которые можно прочитать в один присест, такая структура может быть просто отличной. Но уже на двадцати тысячах строк программы с goto становятся более чем непонятными. На двадцати тысячах строк программы с goto поддаются такому количеству хаков, расширений и оптимизаций, что первоначальное видение построения вещей попросту теряется. И тот факт, что программа может оказаться в любой части кода “из откуда угодно” означает, что для безопасного изменения кода требуется уверенное знания числа этих “из откуда угодно”. Все везде. Это не только делает код трудным для понимания, но и означает, что каждая модификация кода, скорее всего, сделает его еще хуже, в связи с непредвиденными последствиями. С течением времени, программное обеспечение становится “биологическим”. Под этим термином я подразумеваю, что оно развивает модель поведения, в которой все компоненты независимы, но некоторые модули могут иметь скрытую связь.
Оператор goto не состоялся в качестве конструкции языка программирования, потому что порождал массу проблем связанных с постоянной диагностикой программы, написанной с его использованием. Большую благосклонность получили менее мощные, но более узко специализированные структуры, такие как процедуры, функции и четко определенные структуры данных. Для единственного случая, где люди нуждались в глобальном управлении потоком (обработка ошибок), были разработаны исключения. Это был переход от крайней универсальности и абстракции программ, написанных с использованием goto, к конкретности и специфичности частей (таких, как процедуры) решения конкретных проблем. В неструктурном программировании, Вы можете написать Большую Программу, которая делает множество вещей: добавляет новые возможности на любой вкус и изменяет ход вещей так, как вам нужно. Она не должна решать какую-то “проблему” (ведь это так скучно...), но может быть мета-фреймворком с встроенным интерпретатором. Структурное программирование призывает людей разбивать свои программы на специфические части, которые занимаются решением одной проблемы, и, по возможности, делать эти части пригодными для повторного использования. Этот принцип стал базой для философии Unix систем (сделай одну вещь, и сделай ее хорошо) и функционального программирования (добейся простоты определения точной математической семантики, избегая глобальных состояний).
Другая вещь, которую я хочу сказать про оператор goto, это то, что он редко необходим, как примитив уровня языка. Можно добиться того же эффекта, используя цикл while — переменная-счетчик, объявленная вне цикла и используемая конструкцией switch-case, либо увеличивается (шаг), продолжая цикл, либо обнуляется (goto). Это может, если пожелаете, быть расширено в одну гигантскую программу, которая будет работать как один такой цикл, но такой код никогда не пишется. То, что это почти никогда не делается, указывает на тот факт, что использование goto требуется редко. Тем самым структурное программирование указывает на безумие, к которому некоторые опускаются при попытке управления сильно нелокальными потоками.
Тем не менее, было время, когда отказ от goto был крайне спорным вопросом, и все эти идеи структурного программирования выглядели ерундой. Возражение звучало приблизительно так: зачем использовать функции и процедуры, если оператор goto намного мощнее?
Аналогично, почему использовать ссылочно прозрачные функции и неизменяемые записи если объекты намного мощнее? Объект в конце концов может иметь метод run или call или apply, поэтому он может быть функцией. Также он может иметь статические или константные поля и являться записью. Но в то же время он может делать намного больше: объект может иметь инициализаторы и финализаторы и открытые рекурсии и пятьдесят методов, если кто-то примет такое решение. Так в чем же суета вокруг этого бессмысленного структурного программирования, которое подразумевает, что люди будут создавать свои программы из конструкций, которые намного менее мощные, таких как записи, чьи поля никогда не изменяются и чьи классы не содержат магии инициализации?
Ответ в том, что наличие мощности не всегда хорошо. Мощность в программировании — это преимущество для того, кто пишет код, а не для того, кто его потом читает, а обслуживание (например, необходимость понять код) начинается приблизительно с 2000 строк или с шести недель, а объективно на проекте более одного разработчика. На реальных проектах, никто не будет заниматься только написанием кода. Зачастую нам приходиться читать как собственный код, так и код других людей. Нечитабельный код просто неприемлем, и допускается только из-за того, что его очень много, а также потому, что “лучшие практики” ООП, принятые во многих софтверных компаниях, порождают его. Более “мощная” абстракция является более общей и, следовательно, менее конкретной, а это означает, что тем, кто читает такой код, трудно определить, для чего именно она используется. Но люди, которые единолично пишут код, зачастую остаются довольно прямолинейны — мощная абстракция может иметь 18 возможных способов использования, но только один из них на самом деле задействован. В этом случае имеет место своеобразное индивидуальное видение (хотя обычно не задокументированное), которое помогает избежать путаницы. Опасность возникает тогда, когда человек, не посвященный в это видение, начинает модифицировать код. Часто, эти модификации являются хаками, которые не явно подразумевают еще один, из оставшихся 17 способов использования. Это, как правило, приводит к противоречиям, а они в свою очередь к ошибкам. К сожалению, люди, ответственные за исправление этих багов, имеют еще меньшее представление об изначальном видении, которое скрывается за кодом, и их модификации добавляют еще больше хаков. Местами исправления могут иметь место, но общее качество кода снижается. Это процесс “спагеттификации” кода. Никто вот так просто не садится и не начинает писать для себя спагетти-код. Это происходит путем постепенного “растяжения” процесса и почти всегда за это ответственны несколько разработчиков. В программном обеспечении “крутые склоны” действительно реальны и падение может быть очень внезапным.
Объектно-ориентированное программирование, первоначально разработанное для предотвращения спагетти-кода, стало (из-за использование без полного понимания “паттернов проектирования”) одним из худших его источников. “Объект” может спокойно совмещать в себе код и данные, имея при этом любое количество интерфейсов, в то же время класс может свободно порождать подклассы по всей программе. Объектно-ориентированное программирование таит в себе большую мощь, и при дисциплинированном использовании, оно может быть очень эффективно. Но большинство программистов не могут с этим справиться, и со временем их код превращается в спагетти.
Одной из проблем спагетти-кода является то, что он формируется постепенно, что делает его трудно выявляемым в процессе проверки кода, потому что каждое изменения, которое приводит к “спагеттизации”, вне общей картины может выглядеть чисто позитивным. Плюс в том, что изменения в которых нуждался менеджер или клиент “еще вчера”, появляются в коде, и с другой стороны, все это выглядит как умеренное количество дополнительных трудностей. Даже в Темные Времена goto никто не садился и не говорил: “Я собираюсь написать совершенно непонятную программу с 40 операторами goto, указывающими в одну точку кода.” Беспорядок накапливался постепенно, в то время как разработка программы передавалось от одного человека к другому. То же самое справедливо и для объектно-ориентированного спагетти. Здесь нет конкретной точки перехода от чистого первоначального дизайна к непонятному спагетти-коду. Это происходит с течением времени, когда люди злоупотребляют мощью ООП, чтобы протолкнуть непонятные хаки, в каких бы не было необходимости, если бы все понимали как работают программы, которые они модифицируют, и если бы более ясные (хоть и менее мощные) абстракции были использованы. Все это означает, что вина за “спагеттизацию” лежит на всех и не на ком одновременно: любой отдельно взятый разработчик сможет с уверенностью заявить, что это не его изменения отправили код прямиком в ад. Вот почему крупные производители программного обеспечения (на противовес минималистической философии Unix-систем), как правило, придерживаются следующей политики: никто не знает, кто на самом деле виноват в чем то.
Дополнительные проверки кода отлично подходят для выявления очевидно плохих практик, таких как смешивание пробелов и табуляций, или слишком длинных строк. Поэтому более косметические аспекты “плохого кода” менее интересны (используя определение “интересные” как синоним к “тревожные”) нежели спагетти-код. Мы уже знаем, как бороться с ними с помощью дополнительных проверок кода. Мы даже можем настроить наши серверы непрерывной интеграции на то, чтобы они отклоняли этот код. Что касается спагетти-кода, у которого нет четкого определения, то это не так просто сделать, если вообще не невозможно. Для его определения предназначена полная проверка всего программного кода, но я видел очень мало компаний, готовых инвестировать время и ресурсы, необходимые для таких проверок. В долгосрочной перспектива (10 и более лет) я думаю, что это почти невозможно, за исключением команд, которые разрабатывают жизненно- или критически-важное программное обеспечение, что обеспечивает высокий уровень дисциплины на неограниченный срок.
Ответ, как мне кажется, в том, что Большой Код просто не работает. Динамическая типизация “падает” в больших программах, а статическая типизация дает сбои другим способом. Все это справедливо для объектно-ориентированного программирования, императивного программирования, и в меньшей, но все же заметной степени для функционального программирования (проявляется в увеличении параметров размещенных в потоках). Проблемы с “goto” не было, так как его природа позволяла коду становиться Большим Кодом очень быстро. Но жестокая реальность такова, что Большой Код не является “серебряной пулей”. Большие программы просто становятся непонятными. Сложность и большой размер не “иногда не желательны”, наоборот — они всегда опасны. Такие люди, как Стиве Йегге давно это поняли.
Вот почему я считаю философию Unix систем по своей сути правильной: программы не должны быть неясными болотистыми вещами, которые разрастаются в масштабах и никогда не становятся законченными. Программа должна решать одну проблему и делать это хорошо. Если она становится большой и громоздкой, ее нужно разбирать на отдельные части: библиотеки и скрипты, исполняемые файлы и данные. Амбициозные программные проекты не должны иметь структуру в стиле “все-или-ничего”, как отдельные программы, потому что каждая парадигма программирования или набор инструментов просто ломается на этом. Вместо этого, такие проекты должны быть структурированы как системы и этому нужно уделять большое внимание. Это значит, что нужно уделить внимание отказоустойчивости, взаимозаменяемости частей и протоколам связи. Это требует больше дисциплины, нежели разработка случайно разрастающейся большой программы, но оно того стоит. В дополнение к очевидным преимуществам чистого, более удобного кода, добавляется то, что люди действительно читают этот код, а не бездумно добавляют хаки, не понимая что он на самом деле делает. Это означает, что они совершенствуются как разработчики со временем и качество их кода становится лучше в долгосрочной перспективе.
По иронии судьбы, ООП первоначально было предназначено поощрять нечто похожее на минималистическую программную разработку. Изначальное видение ООП не подразумевало, что люди должны садиться и писать огромные, сложные объекты, но имелось ввиду, что они должны использовать ООП именно когда сложность неизбежна. Примером успешного использования в этой области являются базы данных. Люди нуждаются в реляционных базах данных с точки зрения целостности транзакций, долговечности, доступности, параллелизма и производительности, так что сложность просто необходима. Базы данных невероятно сложные, и я могу с уверенностью сказать, что компьютерному миру понадобились десятилетия, чтобы добиться их достойной реализации, не взирая на огромные финансовые стимулы для того, чтобы сделать это быстрее. Но в то же время, когда база данных может быть сложной (по необходимости), интерфейс для ее использования намного проще (SQL). Вы не указываете, какую поисковую стратегию должна использовать база данных, Вы просто пишете SELECT (описываете то, что именно пользователь хочет получить, а не как это получить) и пусть уже оптимизатор запросов заботится об этом.
Отмечу, что базы данных являются своего рода исключением в моей неприязни к Большому Коду. Их сложность — это хорошо понятная необходимость, и есть люди, готовые посвятить свою карьеру исключительно их изучению. Но люди не должны разменивать свою карьеру на понимание типичных бизнес-приложений. И они не будут. Они откажутся от этого, и передадут код в другие руки, ускорив тем самым его спагеттизацию.
Почему Большой Код? Почему он существует, не смотря на свои подводные камни? И почему программисты так поспешно начинают использовать инструменты ООП, не спрашивая, нужна ли на самом деле их мощь и сложность? Я думаю, что есть несколько причин. Одной из них является лень: люди отдадут предпочтение изучению одной большой абстракции общего назначения, нежели потратят время на освоение сильно специфических абстракций и ситуаций, при которых их нужно применять. Зачем кому-то изучать связанные списки и массивы или все эти непонятные структуры, такие как деревья, если у нас уже есть ArrayList? Зачем знать, как программа использует ссылочно прозрачные функции, если объекты могут делать ту же самую работу (и даже намного больше)? Зачем учится использовать командную строку, если современные IDE могут защитить вас от возможности хоть еще раз в жизни увидеть эту чертову штуку? Зачем осваивать новые языки программирования, если Java уже является полным по Тьюрингу? Большой Код возникает от преобладания следующей позиции: зачем разбивать большую программу на модули, если современные компиляторы с легкостью справляются с сотнями тысяч строк кода? Если компьютерам все равно, когда они сталкиваются с Большим Кодом, то почему мы должны об этом заботится?
Тем не менее, если ближе к сути, то я считаю, что все это есть не что иное, как высокомерие с небольшим количеством жадности. Большой Код появляется от убеждения, что программный проект будет настолько востребованным и успешным, что люди стерпят его сложность — идеи на манер того, что собственный предметно-ориентированный язык программирования (DSL) будет столь колоссальным как C или SQL. Он также появляется из-за отсутствия готовности признать проблему решенной, а программу законченной даже тогда, когда значимая часть работы уже завершена. Он также появляется из заблуждений о том, что на самом деле являет собой программирование. Вместо того, чтобы решить существующие четко определенные проблемы и сойти с дистанции, как делают программы, спроектированные за минималистической методологией, программы с Большим Кодом делают намного больше чем нужно. Такие проекты зачастую всеохватывающие и с нецелесообразным “видением”, которое подразумевает создание программного обеспечения ради программного обеспечения. Это привносит беспорядок, потому как “видение” в корпоративной среде, как правило, быстро становится политикой. Программы с Большим Кодом всегда являются отражением среды, которая их породила (закон Конвея), и они всегда больше похожи на сборник пародий и специфического юмора, нежели на универсальный язык математики и информатики.
Существует еще одна проблема в этой пьесе. Менеджеры просто обожают Большой Код, потому что, когда отношение программист-программа становится много-к-одному вместо один-ко-многим, усилия могут быть отслежены и “численность персонала” может быть определена. Минималистические программные методологии превосходны, но они требуют проявлять доверие к программистам в их возможности распределять свое время надлежащим образом для решения более чем одной задачи, и большинство управленцев-тираннозавров чувствуют себя не комфортно, делая это. Большой Код действительно не работает, но он дает менеджерам ощущение контроля над распределением технических усилий. Он также сопутствует смешиванию величины и успеха, чем менеджеры часто и занимаются (о чем свидетельствует вопрос на собеседовании для руководителей “Сколько подчиненных у Вас было?”). Долгосрочная спагеттификация, которая является результатом Большого Кода, редко становится проблемой для таких менеджеров. Они не видят, как это происходит, и зачастую уходят из проекта до того, как это становится проблемой.
Подводя итоги, можно с уверенностью сказать, что спагетти-код — это плохой код, но не весь плохой код является спагетти-кодом. Спагетти-код — это продукт промышленного программирования, которое зачастую (но не всегда) является результатом прохождения кода через большое количество рук, и неизбежным следствием методологии разработки больших программных продуктов и результатом быдлоизации объектно-ориентированного программирования, которая возникла из дефективных управленческих процессов. Противоядие против спагетти-кода — это агрессивный и активный рефакторинг и прикладывание усилий по сохранению программы компактной, эффективной, с чистым исходным кодом и, самое главное, последовательной.
От переводчика: Статья довольно обширная, поэтому буду благодарен, если сообщите в ЛС на неточности или ошибки.