Как сделать ваш код более лаконичным и в то же время вести себя хорошо
У вас когда-нибудь было приложение, которое просто вело себя странно? Вы знаете, вы нажимаете кнопку, и ничего не происходит. Или экран внезапно становится пустым. Или приложение попадает в «странное состояние», и вам нужно перезапустить его, чтобы все снова заработало.
Если вы испытали это, вы, вероятно, стали жертвой определенной формы защитного программирования, которую я хотел бы назвать «параноидальным программированием». Оборонительный человек охраняется и рассуждает. Параноидальный человек боится и действует странным образом. В этой статье я предложу альтернативный подход: «оскорбительное» программирование.
Осторожный читатель
Как может выглядеть такое параноидальное программирование? Вот типичный пример на 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 (глубоко неудачное изобретение), но другие языки видят подобное поведение.
Что произойдет, если что-то пойдет не так:
- Если URL , который проходил в это неверный 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: Не обрабатывать исключения локально.
Защитная нить
Так как же следует обрабатывать ошибки такого рода? Чтобы хорошо обрабатывать ошибки, мы должны рассмотреть всю архитектуру нашего приложения. Допустим, у нас есть приложение, которое периодически обновляет пользовательский интерфейс содержимым некоторого 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 в 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
есть задачи по ним. Каждое задание выполняет какой-то человек. Мы хотим собрать людей, которые участвуют в WorkOrder. Вы, возможно, сталкивались с таким кодом:
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; }
Вау! Куда делся весь код? Внезапно легко обдумать и снова понять код. Если есть проблема со структурой обрабатываемого нами рабочего заказа, наш код даст нам хороший сбой, чтобы сообщить нам!
Нулевая проверка — один из самых коварных источников параноидального программирования, и они очень быстро размножаются. Изображение, которое вы получили в отчете об ошибке — код только что потерпел крах с NullPointerException (NullReferenceException для вас на C #), в этом коде:
public String getCustomerName() { return customer.getName(); }
Люди в стрессе! Чем ты занимаешься? Конечно, вы добавляете еще одну нулевую проверку:
public String getCustomerName() { if (customer == null) return null; return customer.getName(); }
Вы компилируете код и отправляете его. Чуть позже вы получите еще один отчет: в следующем коде есть исключение нулевого указателя:
public String getOrderDescription() { return getOrderDate() + " " + getCustomerName().substring(0,10) + "..."; }
И так начинается, распространение нулевых проверок через код. Просто пресечь проблему в начале и покончить с этим: не принимать нули.
Кстати, если вам интересно, могли бы мы сделать так, чтобы код синтаксического анализа принимал пустые ссылки и все еще был простым, мы можем это сделать. Допустим, пример с рабочим заданием взят из файла 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. Нулевые проверки скрывают ошибки и порождают больше нулевых проверок.
Вывод
Пытаясь защититься, программисты часто оказываются параноиками, то есть отчаянно бьют по проблемам, в которых они их видят, вместо того, чтобы иметь дело с основной причиной. Наступательная стратегия, позволяющая вывести ваш код из строя и исправить его в исходном коде, сделает ваш код чище и менее подвержен ошибкам.
Сокрытие ошибок позволяет ошибкам размножаться. Взрыв приложения на вашем лице заставляет вас решить реальную проблему.