Как стать автором
Обновить
1052.16
OTUS
Цифровые навыки от ведущих экспертов

Темный путь

Время на прочтение6 мин
Количество просмотров2.5K
Автор оригинала: Robert C. Martin (Uncle Bob)

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

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

Для меня проблема заключается в том, что оба языка удвоили сильную статическую типизацию. Оба, кажется, намерены закрыть каждую брешь в типах своих родительских языков. У Swift родительским языком является причудливый безтиповой гибрид C и Smalltalk под названием Objective-C, поэтому, возможно, акцент на типизации понятен. В случае Kotlin родительским языком является уже достаточно сильно типизированная Java.

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

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

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

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

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

Вопрос в том, в чьи обязанности входит управление этим риском? Это работа языка? Или это работа программиста?

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

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

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

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

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

Вопрос в том, кто должен управлять null. Язык? Или программист?

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

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

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

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

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

И что же программисты должны делать, чтобы предотвратить дефекты? Попробую предположить. Вот несколько подсказок. Это глагол. Он начинается с буквы "Т". Да. Вы угадали. Тестировать!

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

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

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

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

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

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

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

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


Сегодня в 20:00 состоится открытое занятие по теме «Понятие "состояние" в шаблонах проектирования». На это уроке рассмотрим понятие «состояние». Посмотрим, как работать с диаграммой состояний и переходов. Проведем обзор конечных автоматов и поймем, как от простой реализации объектов перейти к интерфейсам. Регистрация здесь.

Теги:
Хабы:
Всего голосов 8: ↑4 и ↓40
Комментарии3

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS