Статьи

Оскорбительное программирование

Как сделать ваш код более лаконичным и в то же время вести себя хорошо

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

Если вы испытали это, вы, вероятно, стали жертвой определенной формы защитного программирования, которую я хотел бы назвать «параноидальным программированием». Оборонительный человек охраняется и рассуждает. Параноидальный человек боится и действует странным образом. В этой статье я предложу альтернативный подход: «оскорбительное» программирование.

Осторожный читатель

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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-адрес является недействительным (например, «http // ..» вместо «http: //…»), следующая строка встречается с NullPointerException: connection = (HttpURLConnection) url.openConnection(); , В данный момент плохой разработчик, который получает отчет об ошибке, потерял весь контекст исходной ошибки, и мы даже не знаем, какой URL вызвал проблему.
  • Если рассматриваемый веб-сайт не существует, ситуация намного, намного хуже: метод вернет пустую строку. Почему? Результат StringBuilder builder = new StringBuilder(); все равно будет возвращен из метода.

Некоторые разработчики утверждают, что подобный код хорош, потому что наше приложение не будет зависать. Я бы сказал, что могут случиться и худшие вещи, чем сбой нашего приложения. В этом случае ошибка просто приведет к неправильному поведению без объяснения причин. Например, экран может быть пустым, но приложение не сообщает об ошибке.

Давайте посмотрим на код, переписанный оскорбительным образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
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.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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.

Теперь мы можем упростить наш таймер:

01
02
03
04
05
06
07
08
09
10
11
12
13
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, или же сервер в данный момент не работает.

Хотя вторая ситуация хороша, первая может и не быть: ошибка, которая приводит к сбою нашего кода каждый раз, теперь будет выводить сообщения об ошибках в нашем журнале. Возможно, нам лучше просто убить таймер?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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. Вы, возможно, сталкивались с таким кодом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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;
}

В этом коде мы не очень доверяем тому, что происходит, не так ли? Допустим, нам дали какие-то гнилые данные. В этом случае код успешно обработает данные и вернет пустой набор. Мы бы на самом деле не обнаружили, что данные не соответствуют нашим ожиданиям.

Давайте очистим это:

1
2
3
4
5
6
7
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 #), в этом коде:

1
2
3
public String getCustomerName() {
    return customer.getName();
}

Люди в стрессе! Чем ты занимаешься? Конечно, вы добавляете еще одну нулевую проверку:

1
2
3
4
public String getCustomerName() {
        if (customer == null) return null;
    return customer.getName();
}

Вы компилируете код и отправляете его. Чуть позже вы получите еще один отчет: в следующем коде есть исключение нулевого указателя:

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

И так начинается, распространение нулевых проверок через код. Просто пресечь проблему в начале и покончить с этим: не принимать нули.

Кстати, если вам интересно, могли бы мы сделать так, чтобы код синтаксического анализа принимал нулевые ссылки и все еще был простым, мы можем это сделать. Допустим, пример с рабочим заданием взят из файла XML. В этом случае мой любимый способ решения проблемы будет примерно таким:

1
2
3
4
5
6
7
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. Нулевые проверки скрывают ошибки и генерируют больше нулевых проверок.

Вывод

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

Сокрытие ошибок позволяет ошибкам размножаться. Взрыв приложения на вашем лице заставляет вас решить реальную проблему.

Ссылка: оскорбительное программирование от нашего партнера по JCG Йоханнеса Бродвола из блога Thinking Inside a Bigger Box .