Как сделать ваш код лаконичным и послушным одновременно?
Вам когда-нибудь встречалось приложение которое вело себя очевидно странно? Ну вы знаете, вы нажимаете на кнопку и ничего не происходит. Или экран вдруг чернеет. Или приложение впадает в «странное состояние» и вам приходится перезагружать его чтобы все снова заработало.
Если у вас был подобный опыт, то вы вероятно стали жертвой определенной формы защитного программирования (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.
Заключение
Пытаясь писать защитный код, программисты часто в конечном итоге становятся параноиками — отчаянно накидываясь на все проблемы, которые они видят, вместо того чтобы разобраться с первопричиной. Беззащитная стратегия разрешения коду упасть и исправления источника проблемы сделает ваш код чище и менее подверженным ошибкам.
Сокрытие ошибок ведет к размножению багов. Взрыв приложения, прямо перед вашим носом заставляет вас решать реальную проблему.
ссылка на оригинал статьи https://habrahabr.ru/post/314550/
Добавить комментарий