[Перевод] Тёмный путь


image


Предлагаю вашему вниманию перевод оригинальной статьи Роберта С. Мартина.


За последние несколько месяцев я попробовал два новых языка. Swift и Kotlin. У этих двух языков есть ряд общих особенностей. Действительно, сходство настолько сильное, что мне стало интересно, не является ли это новой тенденцией в нашей языкомешалке. Если это действительно так, то это тёмный путь.


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


Проблема в том, что оба языка сделали ставку на сильную статическую типизацию. Кажется, оба намерены заткнуть каждую дыру в своём родном языке. В случае со Swift — это странный гибрид C и Smalltalk, который называется Objective-C; поэтому, возможно, упор на типизацию понятен. Что касается Kotlin — его предком является уже довольно строго типизированная Java.


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


Дело не в том, что меня беспокоит, что Swift и Kotlin статически типизированы. Скорее меня беспокоит глубина статической типизации.


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


Swift и Kotlin, с другой стороны, становятся абсолютно непреклонными, когда дело доходит до их правил типов. Например, в Swift, если вы объявите функцию, которая бросает исключение, то каждый вызов этой функции, вплоть до начала древа вызовов, должен быть обёрнут в блок do-try, или try!, или try?. В этом языке нет способа тихо выбросить исключение вплоть до верхнего уровня, без прокидывания через все древо вызовов. (Вы можете посмотреть, как Джастин и я боремся с этим, в наших видеоматериалах Mobile Application Case Study)


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


А теперь вопрос. Кто должен разруливать все эти риски? Язык? Или это работа программиста?

В Kotlin, вы не можете наследоваться от класса, или переопределить функцию, пока вы не отметите этот класс или функцию ключевым словом open. Вы также не можете переопределить функцию, если она не отмечена ключевым словом override. Если вы не объявите класс как открытый для наследования, язык не позволит вам наследоваться от такого класса.


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


А теперь вопрос. Кто должен разруливать все эти риски? Язык? Или это работа программиста?

Оба языка, Swift и Kotlin, включают в себя концепцию обнуляемых типов (nullable). Тот факт, что переменная может содержать null, становится частью типа этой переменной. Переменная типа String не может содержать значение null, она может содержать только конкретную строку. С другой стороны, переменная типа String? имеет обнуляемый тип и может содержать null.


Правила языка настаивают на том, что когда вы используете переменную, допускающую значение null, вы должны сначала проверить эту переменную на null. Так что если s это String? тогда var l = s.length() не будет компилироваться. Вместо этого вам следует писать так: var l = s.length() ?: 0 или var l = if (s!=null) s.length() else 0.


Возможно, вы думаете, что это хорошо. Возможно, вы видели довольно много NPE в вашей жизни. Возможно, вы знаете, без тени сомнения, что непроверенные null`ы являются причиной сбоев программного обеспечения на миллиарды и миллиарды долларов. (Действительно, документация Kotlin называет NPE «Billion Dollar Bug»). И, конечно, вы правы. Очень рискованно иметь неконтролируемые null`ы повсюду.


А теперь вопрос. Кто должен разруливать все эти null`ы? Язык? Или это работа программиста?

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


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


Это неверный путь!

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


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


И что же программисты должны делать для предотвращения дефектов? Я загадаю вам загадку. Вот пара подсказок. Это глагол. Он начинается на букву «Т». Да. Вы поняли. ТЕСТИРОВАТЬ!


Вы пишете тесты, чтобы ваша система не возвращала неожиданные значения null. Вы пишете тесты, чтобы ваша система обрабатывала null во всех входных данных. Вы пишете тесты, чтобы каждое исключение, которое вы можете выбросить, было где-то обработано.


Почему эти языки используют все эти функции? Потому что программисты не покрывают тестами свой код. И поскольку программисты не тестируют свой код, у нас теперь есть языки, которые заставляют нас ставить слово open перед каждым классом, от которого мы хотим наследоваться. Теперь у нас есть языки, которые заставляют нас оборачивать каждую функцию, сквозь всё древо вызовов, в блок try!. Теперь у нас есть языки, которые настолько ограничены и настолько переобусловлены, что нужно проектировать всю систему заранее, прежде чем начать кодить.


Рассмотрим пример. Как узнать, открыт ли класс для наследования или нет? Как я узнаю, что где-то вниз по древу вызовов кто-то может выбросить исключение? Сколько кода мне придется изменить, когда я наконец узнаю, что кто-то действительно должен вернуть null в древе вызовов?


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


И из-за всего этого есть основания полагать, что они наказывают вас, когда вы неправы. Они заставляют вас вернуться назад и изменить огромное количество кода, добавив try! или ?: или open сквозь всё древо вызовов.


И как вы избегаете этого наказания? Есть два пути. Тот, который работает, и тот, который не работает. Тот, который не работает, заключается в том, чтобы проектировать всё перед написанием кода. А тот, который избегает наказания, должен переопределить все меры предосторожности.


И поэтому вы объявляете все свои классы и все свои функции открытыми для наследования. Вы никогда не используете исключения. И вы привыкаете к использованию большого количества символов ! для переопределения проверок на null и позволяете NPE расплодиться в своих системах.



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

Комментарии (10)

  • 16 марта 2017 в 16:29 (комментарий был изменён)

    +2

    > Потому что программисты не покрывают тестами свой код

    Тьюринг полнота намекает, что покрыть тестами ВЕСЬ свой код невозможно. А вот если взять некоторое ограниченное подмножество от языка, то для него можно показать корректность. Именно этим и занимается статическая типизация. Доказывает, что некоторые элементы программы, которые целиком укладываются в это подмножество — корректны. Конечно, у вас есть опасные связи между этими островками безопасности, которые нарушают гарантии, но вы знаете где именно вам надо пытаться искать ошибки тестами — на границе!

    > Они предполагают, что вы знаете, какие классы должны быть открыты для наследования, а какие нет.

    Начните с final. По мере необходимости можете final снимать. Но вообще final очень полезная штука для escape analysis.

    • 16 марта 2017 в 16:39

      –1

      Вы это Дяде Бобу объясняете?
  • 16 марта 2017 в 16:57

    0

    Пусть пробует D — хороший баланс фич сделает его шерсть мягкой и шелковистой.
  • 16 марта 2017 в 16:58

    +2

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

    image
    • 16 марта 2017 в 17:00

      +1

      Языки, в свою очередь, иногда этому способствуют.


      Псст, парень, не хочешь объехать этот мучительный участок на потайном велосипеде?
      • 16 марта 2017 в 17:24 (комментарий был изменён)

        +1

        А блин, я только заметил — это же мой любимый Роберт Мартин, гений тоталитаризма в софтверном менеджменте. До сих пор при одном упоминании о книге «The Clean Coder» хочется убивать, даже несмотря на то, что там по сути довольно здравые вещи. Но стиль изложения все перечеркивает — собственные шишки достижения преподносятся как неотъемлемая часть профессионального роста любого программиста, что позволяет с высоты своего, так сказать, опыта поплевывать на седины измучившихся и жаждущих ценного совета салаг. Ну прямо как на форуме рожениц — девочка, которая «уже», не минует возможности поучить тех, кто только ждет своей участи.
  • 16 марта 2017 в 16:59

    +1

    То есть тезисно получается, что язык программирования что-то должен разработчику/Бобу?

  • 16 марта 2017 в 17:18

    +1

    Если после текста заинтересовало мнение «другой стороны» — этот пост недавно как раз обсудили в интервью на Хабре Андрей Бреслав, возглавляющий Kotlin, и Антон Кекс, пишущий на Kotlin:

    «Антон: Андрей, а ты видел интереснейший пост Uncle Bob как раз на тему Kotlin и nullability? Он хорошо прошёлся по этой nullability, мне интересен твой комментарий.

    Андрей: Он написал в общей сложности три поста, посыл примерно такой: когда-то были языки без типов, потом языки с типами, потом снова стали популярны языки без типов, такой маятник качается туда-сюда, сейчас маятник качнулся в сторону типов и ушёл за точку баланса. У нас больше типов, чем надо.

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

    Во-вторых, там есть некоторые конкретные аргументы «как же, если я, например, сделал какой-нибудь тип в своей программе nullable, то мне ж теперь надо поменять всю остальную программу, чтобы эта программа скомпилировалась». Этот момент я не понимаю, потому что — да, конечно, нужно что-то поменять, это содержательно. Если какой-то тип стал nullable, значит, код, который этот тип использует, должен учесть этот факт, иначе он будет неправильно работать! И, конечно, этот код надо поменять. Ну, можно его запустить, получить исключение, и поменять потом, а можно просто сразу поменять. Вот в Kotlin надо сразу.

    Там ещё есть какие-то аргументы, аналогии с const в C++, ещё чем-то — эта аналогия не совсем корректная, по-моему.

    Антон: При всём уважении к Uncle Bob, мне тоже кажется, что он просто использовал возможность ещё раз сказать «вы всё равно должны писать тесты для своего кода, и компилятор вас не спасёт». В чём он, в принципе, прав, но мне лично очень нравится в Kotlin эта фича с nullability, может быть, для меня это даже одна из основных фич, ради которых хочу писать на Котлине. С другой стороны, бывает такое, когда Kotlin не позволяет мне легко описать то, что я хочу, компилятор говорит, что я должен где-то поставить либо ?, либо !… В последнее время, когда у меня всё больше опыта с языком, мне всё меньше приходится бороться с компилятором, но есть такие кейсы. Для этого есть ещё ключевое слово lateinit, которое иногда помогает. Так что есть и плюсы, и минусы, но мне кажется, что всё-таки уклон ушёл в правильную сторону, что от этого больше пользы, чем неудобства.

    Андрей: Безусловно, я согласен, что минусы есть, но за всё надо платить. Если мы хотим, чтобы компилятор что-то гарантировал, то требуется какое-то количество работы с нашей стороны. Здесь просто вопрос, что более оправданно. По-моему, опыт нас самих и всех остальных людей с Kotlin показывает, что введение nullable-типов вполне оправдано, получилось хорошо.»

  • 16 марта 2017 в 17:25

    0

    Я не первый раз слышу про проблемы Kotlin и ?: и даже местами понимаю автора. Здесь вот интересно, можно ли провести аналогию с Generic в Java — определенную избыточность на уровне синтаксиса, особенно до появления Diamond Operator? Просто я помню свои возмущения когда приходилось писать полностью Map someName = new HashMap(); Мне таки казалось эта конструкция невозможной многословностью — ведь я сам знаю, что пишу!
  • 16 марта 2017 в 17:26

    0

    Из статьи больше всего понравилась ссылка на языкомешалку. Это действительно проблема, поскольку, несмотря на то, что новые языки не добавляют почти ничего концептуально нового по сравнению с языками 70-х — 80-х годов, они всё-же разняться в множестве мелких деталей и требуют много времени на освоение. Лучше бы это время потратить на программирование или создание новых подходов к программированию, как это делается для того же JavaScript, даже для старой версии которого было написано немало оригинальных фреймворков.

© Habrahabr.ru