Как сделать ваш код лаконичным и послушным одновременно?

Вам когда-нибудь встречалось приложение, которое вело себя очевидно странно? Ну, вы знаете, вы нажимаете на кнопку и ничего не происходит. Или экран вдруг чернеет. Или приложение впадает в «странное состояние» и вам приходится перезагружать его, чтобы все снова заработало.
Если у вас был подобный опыт, то вы вероятно стали жертвой определенной формы защитного программирования (defensive programming), которую я бы хотел назвать «параноидальное программирование». Защитник осторожный и рассудительный. Параноик испытывает страх и действует странно. В этой статье я предложу альтернативный подход: Offensive programming .
Бдительный читатель
Как может выглядеть параноидальное программирование? Вот типичный пример на Java
public String badlyImplementedGetData(String urlAsString) {
	// Convert the string URL into a real URL
	URL url = null;
	try {
		url = new URL(urlAsString);
	} catch (MalformedURLException e) {
		logger.error("Malformed URL", e);
	}
 
	// Open the connection to the server
	HttpURLConnection connection = null;
	try {
		connection = (HttpURLConnection) url.openConnection();
	} catch (IOException e) {
		logger.error("Could not connect to " + url, e);
	}
 
	// Read the data from the connection
	StringBuilder builder = new StringBuilder();
	try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
		String line;
		while ((line = reader.readLine()) != null) {
			builder.append(line);
		}
	} catch (Exception e) {
		logger.error("Failed to read data from " + url, e);
	}
	return builder.toString();
}
Этот код просто читает содержимое URL как строку. Неожиданное количество кода для выполнения очень простого задания, но такова Java.
Что не так с этим кодом? Код кажется справляется со всеми вероятными ошибками, которые могут возникнуть, но он делает это ужасным способом: просто игнорируя их и продолжая работу. Такая практика безоговорочно поддерживается Java’s checked exceptions ( абсолютно плохое изобретение), но и в других языках видно похожее поведение.
Что происходит если возникает ошибка:
- Если переданный URL невалидный (например “http//..” вместо “http://…”), то следующая строчка выдаст NullPointerException: connection = (HttpURLConnection) url.openConnection(); В данный момент несчастный разработчик, который получит сообщение об ошибке потерял весь контекст настоящей ошибки и мы даже не знаем какой URL вызвал проблему.
 
 
- Если рассматриваемый веб сайт не существует, то ситуация становится намного, намного хуже: метод вернет пустую строку. Почему? Результат StringBuilder builder = new StringBuilder(); по-прежнему будет возвращен из метода.
Некоторые разработчики утверждают, что подобный код хорош, так как наше приложение не упадет. Я бы поспорил, что существуют вещи похуже, которые могут случиться с нашим приложением помимо падения. В данном случае, ошибка просто вызовет неправильное поведение без какого-либо объяснения. Например экран может оставаться пустым, но приложение не выдает ошибки.
Давайте посмотрим на код, написанный по беззащитному методу.
public String getData(String url) throws IOException {
	HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
 
	// Read the data from the connection
	StringBuilder builder = new StringBuilder();
	try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
		String line;
		while ((line = reader.readLine()) != null) {
			builder.append(line);
		}
	}
	return builder.toString();
}
Оператор throws IOException (необходимый в Java, но не в других известных мне языках) показывает, что данный метод может не отработать и вызывающий метод должен быть готов разобраться с этим.
Этот код более лаконичный и если возникает ошибка, то пользователь и логгер (предположительно) получат правильное сообщение об ошибке.
Урок 1: Не обрабатывайте исключения локально.
Оградительный поток
Тогда как же должны обрабатываться ошибки подобного рода? Чтобы хорошо сделать обработку ошибок, нам необходимо учитывать всю архитектуру нашего приложения. Давайте предположим, что у нас есть приложение, которое периодически обновляет UI с содержимым какого-либо URL.
public static void startTimer() {
	Timer timer = new Timer();
	timer.scheduleAtFixedRate(timerTask(SERVER_URL), 0, 1000);
}
 
private static TimerTask timerTask(final String url) {
	return new TimerTask() {
		@Override
		public void run() {
			try {
				String data = getData(url);
				updateUi(data);
			} catch (Exception e) {
				logger.error("Failed to execute task", e);
			}
		}
	};
}
Это именно тот тип мышления который мы хотим! Из большинства непредвиденных ошибок нет возможности восстановиться, но мы не хотим же чтобы наш таймер от этого остановился, не так ли?
А что случится если мы этого хотим? Во-первых, существует известная практика оборачивания Java’s checked exceptions в RuntimeExceptions
public static String getData(String urlAsString) {
	try {
		URL url = new URL(urlAsString);
		HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 
		// Read the data from the connection
		StringBuilder builder = new StringBuilder();
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
			String line;
			while ((line = reader.readLine()) != null) {
				builder.append(line);
			}
		}
		return builder.toString();
	} catch (IOException e) {
		throw new RuntimeException(e.getMessage(), e);
	}
}
На самом деле, библиотеки были написаны с немного большим смыслом, чем сокрытие этой уродливой фичи языка Java.
Теперь мы можем упростить наш таймер:
public static void startTimer() {
	Timer timer = new Timer();
	timer.scheduleAtFixedRate(timerTask(SERVER_URL), 0, 1000);
}
 
private static TimerTask timerTask(final String url) {
	return new TimerTask() {
		@Override
		public void run() {
			updateUi(getData(url));
		}
	};
}
Если мы запустим этот код с ошибочным URL ( или сервер не доступен), все станет достаточно плохо: Мы получим сообщение об ошибке в стандартный поток вывода ошибок и наш таймер умрет.
В этот момент времени одна вещь должна быть очевидна: Этот код повторяет действия вне зависимости от того баг ли это, который вызывает NullPointerException, или же просто сервер сейчас не доступен.
В то время как вторая ситуация нас устраивает, первая может быть не очень: баг, который заставляет наш код падать каждый раз теперь будет мусорить в наш лог ошибок. Может нам будет лучше просто убить таймер?
public static void startTimer() // ...
 
public static String getData(String urlAsString) // ...
 
private static TimerTask timerTask(final String url) {
	return new TimerTask() {
		@Override
		public void run() {
			try {
				String data = getData(url);
				updateUi(data);
			} catch (IOException e) {
				logger.error("Failed to execute task", e);
			}
		}
	};
}
Урок 2: Восстановление не всегда хорошо. Вы должны принимать во внимание ошибки, вызванные средой, например проблемами с сетью, и ошибки, вызванные багами, которые не исчезнут до тех пор, пока кто-то не исправит код.
Вы на самом деле там?
Давайте предположим, что у нас есть класс
WorkOrders с задачами. Каждая задача реализуется кем-то. Мы хотим собрать людей, которые вовлечены в WorkOder. Вы наверняка встречались с подобным кодом:public static Set findWorkers(WorkOrder workOrder) {
	Set people = new HashSet();
	
	Jobs jobs = workOrder.getJobs();
	if (jobs != null) {
		List jobList = jobs.getJobs();
		if (jobList != null) {
			for (Job job : jobList) {
				Contact contact = job.getContact();
				if (contact != null) {
					Email email = contact.getEmail();
					if (email != null) {
						people.add(email.getText());
					}
				}
			}
		}
	}
	return people;
}
В этом коде мы не особо доверяем тому что происходит, не так ли? Давайте предположим, что нам скормили плохие данные. В этом случае, код радостно проглотит данные и вернет пустой набор. Мы на самом деле не заметим, что данные не соответствуют нашим ожиданиям.
Давайте вычистим код:
public static Set findWorkers(WorkOrder workOrder) {
	Set people = new HashSet();
	for (Job job : workOrder.getJobs().getJobs()) {
		people.add(job.getContact().getEmail().getText());
	}
	return people;
}
Эй! Куда исчез весь код? Вдруг снова легко обсуждать и понимать код. И если есть проблема со структурой обрабатываемой задачи, наш код радостно сломается и сообщит нам об этом.
Проверка на null является одним из самых коварных источников параноидального программирования и их количество быстро увеличивается. Представьте что вы получили багрепорт с продакшна — код просто свалился с NullPointerException в этом коде.
public String getCustomerName() {
	return customer.getName();
}
Люди в стрессе! И что делать? Конечно, вы добавляется еще одну проверку на null.
public String getCustomerName() {
        if (customer == null) return null;
	return customer.getName();
}
Вы компилируете код и выгружаете его на сервер. Немного погодя, вы получаете другой отчет: null pointer exception в следующем коде.
public String getOrderDescription() {
   return getOrderDate() + " " + getCustomerName().substring(0,10) + "...";
}
И так это и начинается — распространение проверки на null во всем коде. Просто пресеките проблему в самом начале: не принимайте nulls.
И кстати, если вы интересуетесь можем ли мы заставить код парсера принимать nulls и оставаться простым, то да-мы можем. Предположим, что в примере с задачами мы получаем данные из XML файла. В этом случае, мой любимый способ решения этой задачи будет таким:
public static Set findWorkers(XmlElement workOrder) {
	Set people = new HashSet();
	for (XmlElement email : workOrder.findRequiredChildren("jobs", "job", "contact", "email")) {
		people.add(email.text());
	}
	return people;
}
Конечно, это требует более достойной библиотеки чем та что есть в Java сейчас.
Урок 3: Проверки на null прячут ошибки и порождают больше проверок на null.
Заключение
Пытаясь писать защитный код, программисты часто в конечном итоге становятся параноиками — отчаянно накидываясь на все проблемы, которые они видят, вместо того чтобы разобраться с первопричиной. Беззащитная стратегия разрешения коду упасть и исправления источника проблемы сделает ваш код чище и менее подверженным ошибкам.
Сокрытие ошибок ведет к размножению багов. Взрыв приложения, прямо перед вашим носом заставляет вас решать реальную проблему.
За перевод спасибо Ольге Чернопицкой и компании Edison, которая разрабатывает внутреннюю систему тестирования персонала и приложение для учета рабочего времени сотрудников.
Комментарии (34)
 - webkumo09.11.2016 20:46+5- Простите, для кого эта статья? Для новичков? Тогда может по рукам пора дать? 
 
 «Урок 1». Вам не нравятся checked exception? Это нормально — примерно половине разработчиков на java они не нравятся. Другой половине — нравятся. И вот что интересно — если их уметь готовить, то они очень даже удобны (речь не идёт о работе со стримами в java8, это единственная область, где они действительно неудобны почти всем). А вот ваш способ решения — давно проверенный антипаттерн. Конкретно эту ошибку нужно обработать здесь и сейчас. А вот если она ломает логику вашего метода — наверх выкинуть надо СВОЁ исключение. И нет, расплываться мыслью по древу не надо — верхний пример прекрасно схлопывается в один try-catch блок
 
 «Урок 2», спасибо КЭП!.. Только что же примеры такие слабые?
 
 «Урок 3», чувак, всё хорошо на своём месте. null — очень полезная фича. В некоторых языках их легко синтаксисом обернуть в Optional, в Java это приходится делать самому. Ну и проверки на null должны быть адекватны — обязательное поле оказалось null? ПАДАЕМ и логируем — это дело надо исправлять. Необязательное поле? — так и хрен же с ним, а вот защититься от падения здесь надо.
 
 PS в общем переработав статью можно свести к одной фразе «при программировании необходимо включать мозги». - Lure_of_Chaos10.11.2016 00:30+1- Урок 1. Не всякое исключение обязательно ловить и пробрасывать собственное — в некоторых случаях «системное» исключение будет не менее информативно, а кол-во кода и классов собственных исключений будет меньше. И еще, нет ничего отвратительнее catch(Exception ex) 
 
 Урок 2. Да, исключения бывают штатными и неожиданными, но тут разница только в том, что отсутствие обработки первых — позорно, а вот вторые нужно «почти» не обрабатывать (в гуёвом приложении можно выкинуть окошко «Извините, но что-то плохое случилось», в headless — как минимум, расписать подробно в лог, что мы пытались сделать)
 
 Урок 3. Все сводится к холивару «fail-fast vs defaults», и где лучше исключение, а где — пустые данные — вопрос вечно открытый. А на самом деле, в Java явно недостает конструкции?.. чтобы вместо if(a!=null&&a.getB()!=null&&a.getB().getC()!=null) писать if(a?.getB()?.getC()!=null) - qw110.11.2016 01:29+1- И еще, нет ничего отвратительнее catch(Exception ex) С этим можно поспорить. Досталось в наследство консольное приложение (c#), которое забирало данные с сервера и рассылало на почту. Запускалось через Task Scheduler ежедневно. И там все ситуации, типа ошибка 404 или ошибка 500, были перехвачены отдельно.
 
 Но в определённый момент оно стало просто виснуть. В настройках расписания было указано, что если вчерашний запуск не закончился, новые экземпляры не запускаются. Оказалось, что появилось новое, не предвиденное автором исключение — ошибка ресолва хоста. Исключение не перехватывалось и программа висело со стандартным .net окном: «я упало, выберите Дебажить/Закрыть».
 
 Просто заменил кучу всяких catch-ей на один- catch (Exception ex), который пишет ошибку в лог, пишет письмо админу и завершает программу и проблема решена. - drakmail10.11.2016 01:56+1- Имхо, это тоже самое (концептуально), что и работа без catch вообще.  - webkumo10.11.2016 02:03- Если не идёт речи о работе в каком-либо контейнере (который, кстати, ловит Exception), то концептуально это может быть полезным кейсом (в большинстве случаев — аналогиным кейсам этих самых контейнеров). Вот Error ловить — совсем нельзя… 
  - MacIn10.11.2016 03:53+1- Без catch вообще исключение может никем быть не поймано вовсе, а если и поймано, то неизвестно как обработано — может быть, тем же выводом окна «я упало». 
 
  - redmanmale10.11.2016 12:49+1- А можно было просто изменить настройки винды, чтобы такое окно (я упало, выберите Дебажить/Закрыть) не появлялось. Это один флаг в реестре. 
 - А ещё лучше дописать немного кода, преобразовав приложение в сервис, и в его настройках выбрать требуемое поведение при остановке, например, повторный запуск.  - qw110.11.2016 18:37- А можно было просто изменить настройки винды И повлиять на все другие приложения. Нет, так не надо.
 - ещё лучше дописать немного кода, преобразовав приложение в сервис И круглосуточно занимать память и крутить цикл ожидания (подписываться на таймеры), чтобы сделать что-то 5 минут в день.
 
  - Lure_of_Chaos10.11.2016 13:53+1- Для таких случаев можно использовать catch (Exception ex) на самом верхнем уровне, чтобы пользователю не показывалось 404 и 500, но это очень специфичное исключение. Например, эта же конструкция на нижних уровнях — уродлива.  - qw110.11.2016 18:39- Да, я и сделал на верхнем уровне. Я хотел сказать, на верхнем уровне (в Main) отлавливать исключения по списку бывает ненадёжно. 
 - Например, эта же конструкция на нижних уровнях — уродлива. А если так
 - catch (Exception ex) { throw new MySpecificException(LocalOperationDetails, ex); }
 
 
 - Igor_Sib10.11.2016 06:15- // И еще, нет ничего отвратительнее catch(Exception ex) 
 
 Почему? Поясните, пожалуйста, я вот иногда так делаю. - Lure_of_Chaos10.11.2016 13:59+2- Потому что рискуем зажевать неожиданное исключение и создать ложное впечатление правильности работы юнита (класса, слоя) и долго потом искать реальную причину неправильного поведения системы. Более правильный способ — ловить наиболее конкретизированные исключения, с индивидуальной обработкой (и индивидуальным логгированием), за, возможно, исключением случая, когда неожиданное исключение долетает до пользователя. 
 
 
 
 - svboobnov09.11.2016 20:56-5- А почему не предлагается подход, распространённый в 1С? Примерно так: 
 - public String getCustomerName() { if ( ! isCustomerCorrect(customer) ) { logger.log("in getCustomerName() we have" + customer.toString() + " incorrect value."); /* если метод не гуёвый, то выбросим здесь exception и дело с концом */ GUI.alert("The value of '\customer\' field is incorrect! \n" + customer.toString()); return null; }; return customer.getName(); }
 т.е. здесь идея в том, что код сам по себе не падает, а переходит в ветку «МнеДалиПлохиеДанные-Бибикаю» - Lure_of_Chaos10.11.2016 00:03+4- Во-первых, не переходит в ветку «МнеДалиПлохиеДанные-Бибикаю», а падает выше с кодом - public String getOrderDescription() { return getOrderDate() + " " + getCustomerName().substring(0,10) + "..."; }
 Во-вторых, приложение получается «бибикающим», вместо работающего (помните программы-шутки из 90х годов, которые заставляли нажимать ОК 100 раз?)
 И в-третьих, GUI.alert по-хорошему не должен быть доступен отовсюду: изолированный код — хороший код (см. закон Деметры)
  - Sing10.11.2016 12:54+1- Вашей функции дали плохие данные. Вы сообщаете об этом факте — это хорошо. 
 
 Но после сообщения вы возвращаете, опять-таки, плохие данные. Ваша функция — её контракт, если хотите — вернуть имя пользователя. null — это не имя пользователя ни в коей мере, вы проталкиваете плохие данные дальше, рискуя:
 
 1. Упасть где-то ещё — проблемы с нахождением первопричины ошибки
 2. Испортить пользовательские данные
 3. Где-то ещё плодить кучу костылей, героически справляясь со своим кривым дизайном
 
 - MacIn09.11.2016 21:29- Урок 3: Проверки на null прячут ошибки и порождают больше проверок на null. 
 Проверки на null не прячут ошибки. Обертки вида
 if (abc != null) {
 
 }
 
 прячут. Тут поможет assert вместо проверки.
 
 - Давайте вычистим код: 
 - for (Job job : workOrder.getJobs().getJobs()) {
 
 Сразу возникает вопрос — сфигаль мы, получив какие-то джобс из них получаем какие-то другие джобс?
 В длинном коде:
 - Jobs jobs = workOrder.getJobs(); if (jobs != null) { List jobList = jobs.getJobs();
 
 хотя бы понятно, что мы из какого-то супер-объекта джобс получаем список. А в короткой версии это вообще неочевидно — wtf/min увеличивается. - Lure_of_Chaos10.11.2016 00:15- assert вместо проверки — и есть защитный подход. 
 
 по второму пункту: да, «короткий» код скрывает типы объектов, но по сути, тут просто неверный выбор имен. Код
 куда понятнее.- workOrder.getJobGroup().getJobList()
 
 - kk8609.11.2016 23:33- > Как может выглядеть параноидальное программирование? Вот типичный пример на Java 
 
 И дальше идет пример параноидального программирования. Это совсем не то же самое, что и защитное программирование.
 
 «защитное программирование» это не есть «стелить солому везде, где только можно»
 - Lure_of_Chaos10.11.2016 00:09+1- наш код радостно сломается и сообщит нам об этом. что он упал с NPE на 4ой строке. А почему? да хз, то ли наgetContact(), то ли .getEmail(), а может и на getText(). В страшном коде с кучей проверок мы хотя бы сможем узнать, что у нас null.
 
 - EngineerSpock10.11.2016 10:23- Урок 1: Не обрабатывайте исключения локально. 
 Нет. Как раз урок должен быть: старайтесь обрабатывать ошибки локально. Чем ближе код к месту возникновения ошибки, тем больше он знает про контекст и тем вероятнее может компенсировать ошибку (прописать обработку). Иногда это невозможно, тогда уровень выше обязан сделать это. Через уровень — 99% никто ничто уже не обработает. Улавливаете?
 - Урок 2: Восстановление не всегда хорошо. Вы должны принимать во внимание ошибки, вызванные средой, например проблемами с сетью 
 Если вылетает SocketException в результате того, что я дёрнул проводок — ваше приложение падает? Серьёзно?
 - Урок 3: Проверки на null прячут ошибки и порождают больше проверок на null. 
 Это не урок. Это НИ О ЧЁМ. Про это целую статью накатать можно. Как можно давать такие размашистые советы, вообще не понимаю. Детский сад, ну, ей богу. - Lure_of_Chaos10.11.2016 14:13- тем вероятнее может компенсировать ошибку 
 опять же вопрос — как именно он это делает. Есть опасность вернуть неверные данные. И опять же, появляется необходимость пробрасывания и обработки исключений выше — иногда это излишне.
 - Если вылетает SocketException в результате того, что я дёрнул проводок — ваше приложение падает? 
 Не намного хуже того, если приложение не падает, а назойливо жалуется, ничего не делает или вообще виснет, пока не вдёрнете проводок на место — вот что имелось ввиду, по-моему.
 - Про это целую статью накатать можно 
 Так вся статья о том, насколько и в каких случаях приложение должно быть параноидальным — городить ли проверки на каждую функцию, или «пусть падает» до более общей проверки? - EngineerSpock10.11.2016 15:07- опять же вопрос — как именно он это делает. Есть опасность вернуть неверные данные. И опять же, появляется необходимость пробрасывания и обработки исключений выше — иногда это излишне. 
 Кому какой вопрос? Речь о том, что изначальный посыл — неверен. Абсолютно.
 
 - Не намного хуже того, если приложение не падает, а назойливо жалуется, ничего не делает или вообще виснет, пока не вдёрнете проводок на место — вот что имелось ввиду, по-моему. 
 Плачу кровавыми слезами. Приложение, падающее при выдёргивании провода — ДЕБИЛИЗМ. Просто вдумайтесь в это, неужели непонятно? Во-первых, зачастую приложения не ограничены в своих возможностях наличием связи. Может быть независимый функционал. Второе — возможно кэширование. Третье — на крайний случай — надо сказать, что данных нет, так как нет связи — не надо тупо падать и не надо делать назойливо, надо просто уведомить и всё. Откройте официальный клиент ВК без связи — он падает? Вот и подумайте ещё раз. Запомните: SocketException, IOException и иже с ними НИКОГДА не должны валить приложение.
 
 - Так вся статья о том, насколько и в каких случаях приложение должно быть параноидальным — городить ли проверки на каждую функцию, или «пусть падает» до более общей проверки? 
 В контексте использования null — утверждение в статье это пшик. Просто ни о чём. Хорошее приложение должно быть устойчивым, а средств достижения много. Об этом в статье ноль. - grossws10.11.2016 19:47- Запомните: SocketException, IOException и иже с ними НИКОГДА не должны валить приложение. - It depends. Если приложение, например, не смогло прочитать необходимый для запуска конфиг, т. к. его нет на диске и нет каких-нибудь sensible defaults с которыми можно запуститься — вполне логично упасть с информированием пользователя об отсутствии этого конфига (но не с голым IOE, который ничего не говорит пользователю, ессно).  - EngineerSpock10.11.2016 23:22- Да, согласен. Если не способно запуститься приложение, то да, согласен. Интересно не единственный ли это кейс. 
 
 
 
 
 - Akon3210.11.2016 14:18- Если некто написал такой код 
 Скрытый текст- URL url = null; try { url = new URL(urlAsString); } catch (MalformedURLException e) { logger.error("Malformed URL", e); } // Open the connection to the server HttpURLConnection connection = null; try { connection = (HttpURLConnection) url.openConnection(); } catch (IOException e) { logger.error("Could not connect to " + url, e); } - webkumo10.11.2016 16:22-1- По проблеме второго цитированного куска кода — бить по рукам не за null нужно, а за избыточный функционал (и за использование коллекций-не-генериков) и хреновую архитектуру… хотя да, за null вместо коллекций можно и по кривым рукам хлопнуть. 
 
 Вот что такое класс Jobs? Очевидно workOrder должен содержать коллекцию <job>. (бьём по рукам за кривую архитектуру)
 Почему jobs.getJobs может вернуть null вместо пустой коллекции? (бьём по рукам за некорректное использование null)
 Какой на***н getContact в Job?! (бьём по рукам за кривую архитектуру)
 Почему Email это не подкласс Contact (вот это может быть и обоснованными… но архитектура-то получается кривой)
 Какого чёрта мы в Set<непонятно_чего> запихиваем email.getText()?!
 
 А как должно быть? А должно быть разделение обязанностей:
 Есть метод findWorkers? Пусть возвращает РАБОТНИКОВ! Хотим извлечь текстовые(!) значения их емейл адресов? Обработаем полученную коллекцию работников в другом месте!
 
 И код свёлся бы к
 
 private Set<Worker> findWorkers(WorkOrder workOrder) {
 Set<Worker> workers = new HashSet<>();
 
 for(Job job: workOrder.getJobs()) {
 workers.add(job.getWorker());
 }
 
 return workers;
 }
 
 private Set<String> getWorkerContacts(Set<Worker> workers) {
 return workers.stream()
 .map(Worker::getEmail)
 .filter(email -> email != null)
 .map(Email::getText)
 .collect(Collectors.toSet());
 }
 
 public Set<String> getWorkerContacts(WorkOrder workOrder) {
 Set<Worker> workers = getWorkers(workOrder);
 return getWorkersContacts(workers);
 }
 
 PS сори, парсер съел отступы, а на использование корректной разметки нехватает кармы.
  - grossws10.11.2016 19:39+1- Это не deffencive programming, это говнокод обыкновенный. При вызове, например - badlyImplementedGetData(null)будет простой тупой NPE из- connection = (HttpURLConnection) url.openConnection();.
 - public String badlyImplementedGetData(String urlAsString) { // Convert the string URL into a real URL URL url = null; try { url = new URL(urlAsString); } catch (MalformedURLException e) { logger.error("Malformed URL", e); } // Open the connection to the server HttpURLConnection connection = null; try { connection = (HttpURLConnection) url.openConnection(); } catch (IOException e) { logger.error("Could not connect to " + url, e); } }
 - Deffencive programming предполагает проверку всех веток. Например, проверка - urlAsStringна null, отсечение ветки- url == null(ранним возвратом, использование исключения или монадическими операциями на- j.u.Optional<T>— не суть важно). - burjui10.11.2016 21:09+2- Жаль, монады вообще и Optional в частности мало кто знает и использует. В народе бытует мнение, что функциональное программирование — игрушка для теоретиков, и несовместимо с императивщиной. Хотя в данном примере монада Either не только не помешает, но и сильно улучшит код: обработку ошибок нельзя будет пропустить, и программе не будет грозить падение из-за необработанного исключения.  - Akon3211.11.2016 10:35- К сожалению, на scala Option (думаю, и на java тоже) может давать значительную дополнительную нагрузку на gc, если нужно иметь в памяти десятки или сотни миллионов optional значений.  - grossws11.11.2016 15:35- Если это значение короткоживущее, то оно будет в TLAB, выделение в котором очень дешевое. Т. е. для короткоживущих - Option/- Eitherоверхед для gc будет небольшой. HotSpot, кажется, не имеет оптимизации по размещению таких объектов на стеке, к сожалению, а то могло бы быть ещё приятнее.
 - Есть ещё пример rust'а, где - Optionдля ссылок (которые всегда не- null) и- Box(объект в куче) не имеет оверхеда, т. к. представляет в памяти просто нулевой указатель для- Noneи ненулевой для- Some(...). Во многих других случаях он имеет оверхед размером в 4 байта на тэг как и остальные такие enum'ы.
 
 
 
 
 
           
 
DrBAXA
Что значит
Всё зависит от контекста, вот например
Обязан каждый контакт иметь имейл? А если только телефон. Здесь
nullвполне правильное значение, никак не исключительное соответственно не должно никак вызывать исключений.Поетому проверки на
nullнужны там где они нужны!В Java 8 появились Optional которые позволяют делать эти проверки более красивыми и читаемыми.
kk86
NullObject может решить проблему в ряде случаев.