Почему динамические языки программирования создают трудности при сопровождении больших объемов кода

5118

На одном из Q&A сайтов был задан этот вопрос, и лучший ответ на него дал пользователь Эрик Липперт.

Итак, его ответ.

Предупреждение: я не смотрел презентацию.

Я работал в проектных комитетах по JavaScript (очень динамический язык), C # (в основном статический язык), и Visual Basic (который как статический, так и динамический), у меня есть несколько мыслей по данному вопросу.

Начнем с того, что трудно поддерживать большую кодовую базу, и точка. Большой объем кода очень трудно написать, несмотря на то, какие инструменты есть в вашем распоряжении. Ваш вопрос не означает, что сопровождение большого кода в статически типизированных языках будет простым, а вопрос предполагает только то, что вы встретите больше проблем, если будете поддерживать код в динамическом языке, а не в статическом. Есть причины, почему усилия, затраченные для поддержки большой кодовой базы в динамическом языке несколько больше, чем затраченные усилия на статически типизированных языках. Я рассмотрю только некоторые из них.

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

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

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

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

Все эти характеристики создают препятствия на пути к пониманию кода, и, следовательно, создают препятствия для правильного изменения кода. Короче говоря: время = деньги и сделать правильные изменения в массивном коде дорого из-за характера этих проблем.

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

  • Модульность: код разделен на модули, где каждый модуль имеет четкую задачу. Логика кода может быть задокументирована и понятна без того, чтобы пользователю нужно было понимать подробности его реализации.
  • Инкапсуляция: в модулях проведены различия между их «внешней» поверхностью и их «внутренними» деталями реализации, таким образом, что последние могут быть улучшены без ущерба для правильности работы программы в целом.
  • Повторное использование: когда проблема решена правильно один раз, она решается так всегда. Результат может быть повторно использован в решении других проблем. Такие технологии, как создание библиотеки полезных функций, или создание функциональности базового класса, которые могут быть расширены с помощью производного класса, или архитектуры, в которой приветствуется содержимое, состоящее из методов, реализованных повторным использованием кода. И все это для того, чтобы снизить затраты.
  • Аннотации: аннотированный код описывает допустимые значения, которые могут принимать переменные.
  • Автоматическое обнаружение ошибок: команде, которая работает над большой программой, имеет смысл создать приспособление, которое сразу определит ошибку и расскажет о ней, чтобы она была решена быстро. Такие технологии, как написание набора тестов, использование статического анализатора попадают в эту категорию.

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

Таким образом, по одной только этой причине, в динамически типизированных языках труднее сопровождать объемную кодовую базу, потому что работу, которая выполняется компилятором “бесплатно”, вы должны делать в виде написания набора тестов. Если вы хотите комментировать свои переменные, то вы должны продумать четкую систему, и если новый член вашей команды случайно нарушит ее, то вы должны найти нарушение как можно раньше.

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

Давайте возьмем JavaScript в качестве примера. (Я работал на оригинальных версиях JScript в Microsoft с 1996 по 2001 год) цель проекта заключалась в том, чтобы обезьяна танцевала, при наведении на нее курсором. Скрипты зачастую состояли из одной строки. Мы рассматривали десятистрочные скрипты, и было довольно неплохо, скрипты из сотни строк уже считались большими, а скрипты из тысячи строк были неслыханно огромными. Язык абсолютно не предназначен для программирования объемных задач, и наши решения при реализации, контрольные задачи, и так далее, были основаны на этом предположении.
JavaScript был специально разработан для программ, которые велики настолько, чтобы один человек мог видеть все это на одной странице, так же JavaScript не только динамически типизирован, но так же в нем присутствует мало средств, которые помогают программировать большие кодовые базы:

  • Нет модульной системы, нет классов, интерфейсов, или даже пространств имён. Эти элементы есть в других языках, чтобы помогать организовывать объемные кодовые базы.
  • Система наследования, а так же наследование прототипов развиты довольно слабо. И не понятно, как правильно строить глубокие иерархии (вроде: капитан вид пирата, пират вид человека, человек вид чего-то там…) в out-of-the-box JavaScript.
  • Нет инкапсуляции: каждое свойство каждого объекта может изменяться по желанию какой-либо части программы.
  • Там нет никакого способа, чтобы обозначить какое-либо ограничение по памяти: любая переменная может содержать любое значение. Но это не просто недостаток тех самых возможностей, которые облегчают программирование. Есть также функции, которые делают его более трудным.
  • Система управления ошибок в JavaScript разработана с предположением, что скрипт выполняется на веб-странице, и что цена ошибки невелика, и что пользователь, который увидит неточности скорее всего не будет способен их исправить. Поэтому, как бы много ошибок ни было, программа попытается выполниться до конца. Это разумно, для данных целей языка, но она, безусловно, делает программирование больших объемов кода в разы сложнее, потому что это увеличивает сложность написания тестов. Зачастую искать ошибки в таких программах труднее, чем писать тесты, которые их выявят.
  • Код может изменить сам себя на основе данных с пользовательского ввода через объекты, такие как Eval или при помощи добавления новых блоков сценариев в DOM динамически. Любые инструменты статического анализа не могут даже знать, что код сделает с программой!
  • И так далее.

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

  • Они пишут тест-кейсы для каждого идентификатора использующегося в программе. В мире, где орфографических ошибки игнорируются, это необходимо. Это экономически выгодно.
  • Пишут код в типизированных языках, таких как TypeScript, и компилируют как JavaScript.
  • Они используют фреймворки, которые способствуют программированию в стиле, который лучше поддается анализу, более склонен к модульности и менее вероятно воспроизведет на свет наиболее распространенные ошибки.
  • У них хорошая дисциплина в именовании, в разделении обязанностей, и так далее. Опять же, это снижает стоимость, и все эти задачи будут выполнены компилятором в типичном статически типизированном языке.

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

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

Перевод статьи “Why do dynamic languages make it difficult to maintain large codebases?”

5118