...

среда, 9 ноября 2016 г.

[Перевод] Offensive programming: параноидальное, наступательное, атакующее или беззащитное программирование

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


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

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

Комментарии (0)

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий