Pull to refresh

Тёмный путь

Reading time 6 min
Views 19K
Original author: Robert C. Martin (Uncle Bob)

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 расплодиться в своих системах.




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

Tags:
Hubs:
+21
Comments 77
Comments Comments 77

Articles