Как стать автором
Обновить

Вы часто используете null? А он у нас в спецификации

Время на прочтение5 мин
Количество просмотров6.6K

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

Младшие разработчики даже если не понимают, то обычно следуют "чистому коду" (прочитав книжку Роберта Мартина). Поэтому код с возможностью возникновения NPE стал встречаться реже, хотя, конечно, они есть.

Не хочу сказать, что любое использование null - это плохо, скорее тут можно сказать "семь раз отмерь, один раз отрежь".

Тем не менее, я долгое время не жаловался насчет NPE, даже когда разработчики с языков у которых строгий контроль на эту тему хвастались своим null-safety. Но из-за одного бага я понял, что просто использование null это не так страшно, даже если вы возвращаете или передаете его. Разумеется, это очень плохо, но есть вещи хуже - null в спецификациях.

Было бы не особо интересно, если рассказ был об одной компании, которая сделала плохую спецификацию. Давайте вместо этого поговорим о более известной спецификации из Java EE - Java Servlet Specification, а конкретно возьмем класс HttpServletRequest и заглянем в метод getCookies()

getCookies

Cookie[] getCookies()

Returns an array containing all of the Cookie objects the client sent with this request. This method returns null if no cookies were sent.

  • Returns:

    an array of all the Cookies included with this request, or null if the request has no cookies

Больше всего следует обратить внимание на это:

This method returns null if no cookies were sent.

То есть, если здесь нет куков, то нужно вернуть null. Посмотрим на это со стороны разработчика:

  • Нам нужно вернуть массив из куки, а если их нет, то что-то, что означало бы, что куков в запросе не было.

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

null vs empty array

В спецификации разработчики предпочли использовать null, что привело к некоторым последствиям, которые можно было бы избежать:

  • Усложнение API, которое заставляет пользователя каждый раз делать null-check

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

  • Усложнение имплементации этого метода. Если контейнер пустой, то нужно вернуть null (далее рассмотрим пример)

Но, конечно, не стоит считать что разработчики приняли такое решение напившись в баре. Использование null можно было оправдать, например, ради перформанса (избегаем аллоцирование пустого контейнера).

Тем не менее, это утверждение тоже можно поставить под сомнение.

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

  • Во-вторых, аллоцирования легко можно избежать просто создав пустой массив и возвращать его (например, какое-нибудь статическое неизменяемое поле)

Давайте посмотрим пару примеров, как использование null может испортить код

for (Cookie cookie : httpServletRequest.getCookies()) { 
  // NPE!    // …
}
int cookiesSize = httpServletRequest.getCookies().length    // NPE!

Добавляем null-check:

if (httpServletRequest.getCookies() != null)
for (Cookie cookie : httpServletRequest.getCookies()) {    
  // …
}
Cookie[] cookies = httpServletRequest.getCookies();
int cookiesSize = cookies == null ? 0 : cookies.length

Как и говорилось ранее, самое обидное, что этот NPE может и не появиться сразу. И разумеется, может быть сложным случайно не забыть сделать эту проверку.

Но усложнения касаются не только API, но и его имплементации. Рассмотрим как пример Jetty

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

Коммит, который исправил ошибку

До:

return cookies == null?null:cookies.getCookies();

После:

if (cookies == null || cookies.getCookies().length == 0)    
	return null;
return _cookies.getCookies();

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

Справедливости ради стоит сказать, что реализация была неправильной как с точки зрения спецификации, так и со стороны нормального кода. Так как null все равно мог вернуться и просто не было проверки на длину массива, которая если он пустой, то должен вернуть null. Тем не менее, он хорошо показывает усложнение реализации.

Мы против!

Хотя люди, конечно же, возмущались новым изменениям, но не идти же против спецификации. Или же есть смельчаки, которые сделали это? Разумеется есть, хотя я и не знаю нарочно ли они это или по нутру хорошего кода изменили спецификацию?

Например, проект classpathx

The GNU Classpath Extensions project, aka classpathx builds free versions of Oracle's Java extension libraries, the packages in the javax namespace. It is a companion project of the GNU Classpath project.

У них есть скажем так "своя спецификация"

Cookie[] getCookies()

Gets all the Cookies present in the request.

  • Returns:

    an array containing all the Cookies or an empty array if there are no cookies

  • Since:

    2.0

Статические анализаторы

Не обойдем стороной и статические анализаторы. Они также считают что возвращать null не лучшее решение. Например, тот же Sonar, SEI CERT Oracle Coding Standart for Java

Заключение

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

В этом случае как разработчикам как и клиентам пришлось усложнять свой код.

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

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

Все что мы можем сейчас сделать - это вынести уроки из предыдущих ошибок (и своих и чужих):

  • Проблемы каких-то решений могут существовать и до того, как вы их осознаете

  • То что сейчас является обычной практикой в будущем может оказаться плохим кодом

  • Нужно делиться своими знаниями и опытом. Даже не ради кого-то другого, а ради себя, ведь возможно именно вашу статью прочитает будущий разработчик самого популярного фреймворка.

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


Speaking at a software conference in 2009, Tony Hoare apologized for inventing the null reference:[25]

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years

Теги:
Хабы:
Всего голосов 19: ↑15 и ↓4+11
Комментарии12

Публикации