Как сделать ваш код более лаконичным и в то же время вести себя хорошо
У вас когда-нибудь было приложение, которое просто вело себя странно? Вы знаете, вы нажимаете кнопку, и ничего не происходит. Или экран внезапно становится пустым. Или приложение попадает в «странное состояние», и вам нужно перезапустить его, чтобы все снова заработало.
Если вы испытали это, вы, вероятно, стали жертвой определенной формы защитного программирования, которую я хотел бы назвать «параноидальным программированием». Оборонительный человек охраняется и рассуждает. Параноидальный человек боится и действует странным образом. В этой статье я предложу альтернативный подход: «оскорбительное» программирование.
Осторожный читатель
Как может выглядеть такое параноидальное программирование? Вот типичный пример на 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. Нулевые проверки скрывают ошибки и порождают больше нулевых проверок.
Вывод
Пытаясь защититься, программисты часто оказываются параноиками, то есть отчаянно бьют по проблемам, в которых они их видят, вместо того, чтобы иметь дело с основной причиной. Наступательная стратегия, позволяющая вывести ваш код из строя и исправить его в исходном коде, сделает ваш код чище и менее подвержен ошибкам.
Сокрытие ошибок позволяет ошибкам размножаться. Взрыв приложения на вашем лице заставляет вас решить реальную проблему.