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//.." замість «...»), то наступний рядок видасть NullPointerException: connection = (HttpURLConnection) url.openConnection(); В даний момент нещасний розробник, який отримає повідомлення про помилку втратив весь контекст цієї помилки і ми навіть не знаємо, який URL викликав проблему.

  • Якщо розглянутий веб сайт не існує, то ситуація стає набагато, набагато гірше: метод поверне пустий рядок. Чому? Результат StringBuilder builder = new StringBuilder(); як і раніше буде повернутий з методу.
Деякі розробники стверджують, що подібний код хороший, так як наш додаток не впаде. Я б посперечався, що існують речі гірші, які можуть трапитися з нашим додатком крім падіння. В даному випадку, просто помилка викличе неправильна поведінка без будь-якого пояснення. Наприклад екран може залишатися пустим, але додаток не видає помилки.

Давайте подивимося на код, написаний по беззахисній методом.

public String getData(String url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new 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 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.