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




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

Если у вас был подобный опыт, то вы вероятно стали жертвой определенной формы защитного программирования (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)


  1. DrBAXA
    09.11.2016 20:07
    +3

    Что значит

    Просто пресеките проблему в самом начале: не принимайте nulls

    Всё зависит от контекста, вот например
    job.getContact().getEmail();
    

    Обязан каждый контакт иметь имейл? А если только телефон. Здесь null вполне правильное значение, никак не исключительное соответственно не должно никак вызывать исключений.
    Поетому проверки на null нужны там где они нужны!
    В Java 8 появились Optional которые позволяют делать эти проверки более красивыми и читаемыми.


    1. kk86
      09.11.2016 23:31

      NullObject может решить проблему в ряде случаев.


  1. webkumo
    09.11.2016 20:46
    +5

    Простите, для кого эта статья? Для новичков? Тогда может по рукам пора дать?

    «Урок 1». Вам не нравятся checked exception? Это нормально — примерно половине разработчиков на java они не нравятся. Другой половине — нравятся. И вот что интересно — если их уметь готовить, то они очень даже удобны (речь не идёт о работе со стримами в java8, это единственная область, где они действительно неудобны почти всем). А вот ваш способ решения — давно проверенный антипаттерн. Конкретно эту ошибку нужно обработать здесь и сейчас. А вот если она ломает логику вашего метода — наверх выкинуть надо СВОЁ исключение. И нет, расплываться мыслью по древу не надо — верхний пример прекрасно схлопывается в один try-catch блок

    «Урок 2», спасибо КЭП!.. Только что же примеры такие слабые?

    «Урок 3», чувак, всё хорошо на своём месте. null — очень полезная фича. В некоторых языках их легко синтаксисом обернуть в Optional, в Java это приходится делать самому. Ну и проверки на null должны быть адекватны — обязательное поле оказалось null? ПАДАЕМ и логируем — это дело надо исправлять. Необязательное поле? — так и хрен же с ним, а вот защититься от падения здесь надо.

    PS в общем переработав статью можно свести к одной фразе «при программировании необходимо включать мозги».


    1. Lure_of_Chaos
      10.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)


      1. qw1
        10.11.2016 01:29
        +1

        И еще, нет ничего отвратительнее catch(Exception ex)
        С этим можно поспорить. Досталось в наследство консольное приложение (c#), которое забирало данные с сервера и рассылало на почту. Запускалось через Task Scheduler ежедневно. И там все ситуации, типа ошибка 404 или ошибка 500, были перехвачены отдельно.

        Но в определённый момент оно стало просто виснуть. В настройках расписания было указано, что если вчерашний запуск не закончился, новые экземпляры не запускаются. Оказалось, что появилось новое, не предвиденное автором исключение — ошибка ресолва хоста. Исключение не перехватывалось и программа висело со стандартным .net окном: «я упало, выберите Дебажить/Закрыть».

        Просто заменил кучу всяких catch-ей на один catch (Exception ex), который пишет ошибку в лог, пишет письмо админу и завершает программу и проблема решена.


        1. drakmail
          10.11.2016 01:56
          +1

          Имхо, это тоже самое (концептуально), что и работа без catch вообще.


          1. webkumo
            10.11.2016 02:03

            Если не идёт речи о работе в каком-либо контейнере (который, кстати, ловит Exception), то концептуально это может быть полезным кейсом (в большинстве случаев — аналогиным кейсам этих самых контейнеров). Вот Error ловить — совсем нельзя…


          1. MacIn
            10.11.2016 03:53
            +1

            Без catch вообще исключение может никем быть не поймано вовсе, а если и поймано, то неизвестно как обработано — может быть, тем же выводом окна «я упало».


        1. redmanmale
          10.11.2016 12:49
          +1

          А можно было просто изменить настройки винды, чтобы такое окно (я упало, выберите Дебажить/Закрыть) не появлялось. Это один флаг в реестре.


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


          1. qw1
            10.11.2016 18:37

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


        1. Lure_of_Chaos
          10.11.2016 13:53
          +1

          Для таких случаев можно использовать catch (Exception ex) на самом верхнем уровне, чтобы пользователю не показывалось 404 и 500, но это очень специфичное исключение. Например, эта же конструкция на нижних уровнях — уродлива.


          1. qw1
            10.11.2016 18:39

            Да, я и сделал на верхнем уровне. Я хотел сказать, на верхнем уровне (в Main) отлавливать исключения по списку бывает ненадёжно.

            Например, эта же конструкция на нижних уровнях — уродлива.
            А если так
            catch (Exception ex) { throw new MySpecificException(LocalOperationDetails, ex); }


      1. Igor_Sib
        10.11.2016 06:15

        // И еще, нет ничего отвратительнее catch(Exception ex)

        Почему? Поясните, пожалуйста, я вот иногда так делаю.


        1. Lure_of_Chaos
          10.11.2016 13:59
          +2

          Потому что рискуем зажевать неожиданное исключение и создать ложное впечатление правильности работы юнита (класса, слоя) и долго потом искать реальную причину неправильного поведения системы. Более правильный способ — ловить наиболее конкретизированные исключения, с индивидуальной обработкой (и индивидуальным логгированием), за, возможно, исключением случая, когда неожиданное исключение долетает до пользователя.


  1. svboobnov
    09.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();
    }
     

    т.е. здесь идея в том, что код сам по себе не падает, а переходит в ветку «МнеДалиПлохиеДанные-Бибикаю»


    1. Lure_of_Chaos
      10.11.2016 00:03
      +4

      Во-первых, не переходит в ветку «МнеДалиПлохиеДанные-Бибикаю», а падает выше с кодом

      public String getOrderDescription() {
         return getOrderDate() + " " + getCustomerName().substring(0,10) + "...";
      }
      

      Во-вторых, приложение получается «бибикающим», вместо работающего (помните программы-шутки из 90х годов, которые заставляли нажимать ОК 100 раз?)
      И в-третьих, GUI.alert по-хорошему не должен быть доступен отовсюду: изолированный код — хороший код (см. закон Деметры)


    1. Sing
      10.11.2016 12:54
      +1

      Вашей функции дали плохие данные. Вы сообщаете об этом факте — это хорошо.

      Но после сообщения вы возвращаете, опять-таки, плохие данные. Ваша функция — её контракт, если хотите — вернуть имя пользователя. null — это не имя пользователя ни в коей мере, вы проталкиваете плохие данные дальше, рискуя:

      1. Упасть где-то ещё — проблемы с нахождением первопричины ошибки
      2. Испортить пользовательские данные
      3. Где-то ещё плодить кучу костылей, героически справляясь со своим кривым дизайном


  1. MacIn
    09.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 увеличивается.


    1. Lure_of_Chaos
      10.11.2016 00:15

      assert вместо проверки — и есть защитный подход.

      по второму пункту: да, «короткий» код скрывает типы объектов, но по сути, тут просто неверный выбор имен. Код

      workOrder.getJobGroup().getJobList()
      
      куда понятнее.


  1. kk86
    09.11.2016 23:33

    > Как может выглядеть параноидальное программирование? Вот типичный пример на Java

    И дальше идет пример параноидального программирования. Это совсем не то же самое, что и защитное программирование.

    «защитное программирование» это не есть «стелить солому везде, где только можно»


  1. Lure_of_Chaos
    10.11.2016 00:09
    +1

    наш код радостно сломается и сообщит нам об этом.
    что он упал с NPE на 4ой строке. А почему? да хз, то ли наgetContact(), то ли .getEmail(), а может и на getText(). В страшном коде с кучей проверок мы хотя бы сможем узнать, что у нас null.


  1. EngineerSpock
    10.11.2016 10:23

    Урок 1: Не обрабатывайте исключения локально.

    Нет. Как раз урок должен быть: старайтесь обрабатывать ошибки локально. Чем ближе код к месту возникновения ошибки, тем больше он знает про контекст и тем вероятнее может компенсировать ошибку (прописать обработку). Иногда это невозможно, тогда уровень выше обязан сделать это. Через уровень — 99% никто ничто уже не обработает. Улавливаете?
    Урок 2: Восстановление не всегда хорошо. Вы должны принимать во внимание ошибки, вызванные средой, например проблемами с сетью

    Если вылетает SocketException в результате того, что я дёрнул проводок — ваше приложение падает? Серьёзно?
    Урок 3: Проверки на null прячут ошибки и порождают больше проверок на null.

    Это не урок. Это НИ О ЧЁМ. Про это целую статью накатать можно. Как можно давать такие размашистые советы, вообще не понимаю. Детский сад, ну, ей богу.


    1. Lure_of_Chaos
      10.11.2016 14:13

      тем вероятнее может компенсировать ошибку

      опять же вопрос — как именно он это делает. Есть опасность вернуть неверные данные. И опять же, появляется необходимость пробрасывания и обработки исключений выше — иногда это излишне.
      Если вылетает SocketException в результате того, что я дёрнул проводок — ваше приложение падает?

      Не намного хуже того, если приложение не падает, а назойливо жалуется, ничего не делает или вообще виснет, пока не вдёрнете проводок на место — вот что имелось ввиду, по-моему.
      Про это целую статью накатать можно

      Так вся статья о том, насколько и в каких случаях приложение должно быть параноидальным — городить ли проверки на каждую функцию, или «пусть падает» до более общей проверки?


      1. EngineerSpock
        10.11.2016 15:07

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

        Кому какой вопрос? Речь о том, что изначальный посыл — неверен. Абсолютно.

        Не намного хуже того, если приложение не падает, а назойливо жалуется, ничего не делает или вообще виснет, пока не вдёрнете проводок на место — вот что имелось ввиду, по-моему.

        Плачу кровавыми слезами. Приложение, падающее при выдёргивании провода — ДЕБИЛИЗМ. Просто вдумайтесь в это, неужели непонятно? Во-первых, зачастую приложения не ограничены в своих возможностях наличием связи. Может быть независимый функционал. Второе — возможно кэширование. Третье — на крайний случай — надо сказать, что данных нет, так как нет связи — не надо тупо падать и не надо делать назойливо, надо просто уведомить и всё. Откройте официальный клиент ВК без связи — он падает? Вот и подумайте ещё раз. Запомните: SocketException, IOException и иже с ними НИКОГДА не должны валить приложение.

        Так вся статья о том, насколько и в каких случаях приложение должно быть параноидальным — городить ли проверки на каждую функцию, или «пусть падает» до более общей проверки?

        В контексте использования null — утверждение в статье это пшик. Просто ни о чём. Хорошее приложение должно быть устойчивым, а средств достижения много. Об этом в статье ноль.


        1. grossws
          10.11.2016 19:47

          Запомните: SocketException, IOException и иже с ними НИКОГДА не должны валить приложение.

          It depends. Если приложение, например, не смогло прочитать необходимый для запуска конфиг, т. к. его нет на диске и нет каких-нибудь sensible defaults с которыми можно запуститься — вполне логично упасть с информированием пользователя об отсутствии этого конфига (но не с голым IOE, который ничего не говорит пользователю, ессно).


          1. EngineerSpock
            10.11.2016 23:22

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


  1. Akon32
    10.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);
        }


    1. webkumo
      10.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 сори, парсер съел отступы, а на использование корректной разметки нехватает кармы.


      1. Akon32
        10.11.2016 16:49

        Угловые скобки парсер тоже мог съесть.


        1. webkumo
          10.11.2016 18:15

          А он и съел, только не скобки, а xml-entities: &lt; (правую скобку эмулировать не надо — без левой она ничего не кушает)

          PS &lt; я тоже записал с помощью xml-entities, в оригинале &amp;lt;


  1. grossws
    10.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> — не суть важно).


    1. burjui
      10.11.2016 21:09
      +2

      Жаль, монады вообще и Optional в частности мало кто знает и использует. В народе бытует мнение, что функциональное программирование — игрушка для теоретиков, и несовместимо с императивщиной. Хотя в данном примере монада Either не только не помешает, но и сильно улучшит код: обработку ошибок нельзя будет пропустить, и программе не будет грозить падение из-за необработанного исключения.


      1. Akon32
        11.11.2016 10:35

        К сожалению, на scala Option (думаю, и на java тоже) может давать значительную дополнительную нагрузку на gc, если нужно иметь в памяти десятки или сотни миллионов optional значений.


        1. grossws
          11.11.2016 15:35

          Если это значение короткоживущее, то оно будет в TLAB, выделение в котором очень дешевое. Т. е. для короткоживущих Option/Either оверхед для gc будет небольшой. HotSpot, кажется, не имеет оптимизации по размещению таких объектов на стеке, к сожалению, а то могло бы быть ещё приятнее.


          Есть ещё пример rust'а, где Option для ссылок (которые всегда не null) и Box (объект в куче) не имеет оверхеда, т. к. представляет в памяти просто нулевой указатель для None и ненулевой для Some(...). Во многих других случаях он имеет оверхед размером в 4 байта на тэг как и остальные такие enum'ы.