Image source: PIRO4D pixabay.com
Наверное вам уже приходилось иметь дело с объектами, которые меняют свою структуру по законам своего внутреннего развития или в зависимости от внешних условий. На практике это означает, что части объекта для внешнего мира могут быть иногда доступны, а иногда – нет.
Какие это могут быть объекты? Многие сервисные компании, торговые предприятия и банки предоставляют различные виды услуг в зависимости от времени суток. Различные устройства могут выполнять или не выполнять те или иные действия в зависимости от своего состояния. Если вы читаете этот текст с экрана смартфона или ноутбука, он может перестать его показывать, если заряд аккумуляторов опуститься ниже определенной границы.
В этом и последующем посте мы рассмотрим один простенький объект такого рода – комбинацию кофе-автомата и электрочайника. Но пока порассуждаем о том, как на Java можно реализовать доступ к элементам с динамической структурой, например – нашего комби-автомата.
Следует сразу оговориться, что физическая структура прибора со временем не меняется. Говоря о динамике структуры, мы глядим на прибор глазами пользователя. Если по каким-то причинам прибор не может приготовить пользователю кофе, то этой функциональности для пользователя в нем на данный момент фактически нет.
Как можно это выразить с помощью Java?
Предположим, что мы получили задание написать блок управления таким прибором и нам необходима функция вроде
device.getCoffeePortion()
Какого типа элемент должен возвращать этот метод? Ведь иногда прибор может способен дать пользователю кофе, а иногда нет.
Существует не так уж много способов решения этой проблемы (по крайней мере известных мне).
Возвращаем либо объект либо null
Самым распространенным решением является возвращения некоторого обьекта в позитивном случае и null в отрицательном. Пользователь должен поэтому всегда проверять возвращаемое значение и использовать его только если оно не null.
Проблема заключается в том, что пользователь вашего объекта должен как-то знать или догадаться о возможности возвращения нулевого объекта. Программист, вызывающий ваш метод в своем коде, может без проверки передать его как параметр в цепочке вызовов других функций, записать в список и т. д. Это означает, что расплата в в виде NullPointerException может прийти существенно позже и в существенно удаленном от вызова функции месте.
NullPointerException заведомый чемпион среди ошибок Java программистов, и по количеству, и по затраченным на их поиски и устранение время и нервы.
Интересно отметить, что у этой катастрофы есть автор, назвавший её своей “billion-dollar mistake”. Null появился намного раньше Java в 1965 году при разработке языка ALGOL.
Спустя 44 года Tony Hoare, создатель этого чудовища (но и замечательный ученый) написал в своей статье “Null References: The Billion Dollar Mistake” (QCon, August 25, 2009), (https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare).
I call it my billion-dollar mistake … 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.
Как создатель объекта вы можете несколько смягчить ситуацию используя аннотированный возвращаемый результат, например:
@Nullabble CoffeePortion getCoffeePortion()
Если программист использует современное IDE, то оно в определённых ситуациях укажет ему на необходимость проверки на нулевой объект. Но это происходит далеко не всегда и особо рассчитывать на это не стоит.
Итак, возвращать null как результат операции – это плохо. Значит надо этого избегать. А как?
Сначала хорошо попросите
Одним из вариантов решения является снабжения каждого get… метода напарником has… Пользователь объекта должен быть как-то предупрежден, что перед запросом get.. он должен спросить, а имеется ли значение, которое надо возвращать. В нашем случае пара выглядела бы так:
boolean hasCofeePortion() @Nullabble CoffeePortion getCoffeePortion()
Если первая функция возвращает true, она дает тем самым добро на вызов функции -напарника. Пользователь теоретически может попробовать не вызывать первую has..функцию. Если же значение на момент вызова будет недоступно, функция get… вернет не null, как было в первом варианте, а выбросит Exception.
Таким образом проблема передачи “опасного” объекта по цепочке вызова исключается. В этом преимущество подхода. Но у подхода есть два существенных недостатка. Во-первых, умножается количество функций. Во-вторых, нет гарантии, что на момент вызова has… объект был доступен, а к моменту get … перестал быть доступным. (В нашем простом примере это невозможно, но в сложных системах, где состояние объектов зависит от времени или внешних воздействий, а для подготовки вызова get… требуется провести другие операции, эта опасность реальна.
Итак, сначала спросить, а потом взять – не очень хорошая стратегия. А нельзя наоборот? Именно эту идею использует следующий подход.
Обьект всегда выдается, но не всегда живой
В этом подходе объект всегда возвращается, но содержит дополнительный признак. Это признак говорит нам, грубо говоря, есть он на самом деле или его нет. Заострив до крайности, можно этот подход сформулировать так: вы получаете либо живой объект, либо его труп. Но даже если это труп, он немного говорящий.
А именно, подход говорит, что метод типа isActive() или isPresent() вы всегда вы можете без опаски вызывать. Даже у “мертвого” объекта. И при положительном ответе работать с объектом дальше.
Очевидным недостатком этого подхода является навешивание на объект технического признака, никак не объяснимого с точки зрения его собственного функционирования (business logic). В определенных ситуациях этот признак может входить в противоречие с этой логикой. Например, представим, что некий объект связан с двумя другими объектами. С точки зрения одного объекта он может быть активен или “жив”, а с точки зрения другого “мертв”.
Однако, с технической точки зрения, у этого подхода могут быть и свои, иногда неожиданные, преимущества. Например, если нам надо сохранять или пересылать составной объект в виде JSON или XML, разумно пересылать его полностью, включая и его на данный момент деактивированные части. Ведь может оказаться так, что после восстановления объекта (считывания из файла или потока) деактивированные части когда-нибудь придется активировать.
Итак, этот подход хорош тем, что не приводит к NullPointerException, но “ненатурален”. Нельзя ли найти что-то похожее, но без описанных недостатков?
Возвращается лист, возможно пустой
Один из самых рекомендованных экспертами до появления в Java 8 Optional подходов был подход, описанный ниже.
Идея его крайне проста. Вместо объекта мы возвращаем массив или список объектов. Этот массив или список может быть либо пустым либо содержать точно один объект.
Фактически мы принуждаем тем самым пользователей нашего метода на месте, сразу же после вызова функции разобраться со списком. Вряд ли он будет этот список передавать дальше как параметр других методов.
Недостатком метода является его очевидная вычурность Уж очень сильно начинают различаться ситуации, когда get… метод возвращает элементарное значение, например int, заведомо существующий объект и условно существующий объект.
И чтобы преодолеть это противоречие, а также по другим причинам, разработчики языка Java и решили ввести Optional.
Но об этом – в следующем посте. В нем мы рассмотрим такие вопросы, как:
- Optional – что же это на самом деле?
- Использование Optional в нашем примере
- Использования в цепочках доступа
- Использование Optional в специальных ситуациях обработки объектов (Persistence, Validation)
- Так когда же его использовать
- Optional – это часть функционального программирования?
Следующая статья: Java 8 Optional и объекты с динамической структурой. Часть 2