1. Завдання програміста

Дуже часто програмісти-початківці уявляють собі суть роботи програміста зовсім не так, як досвідчені.

Від новачка часто можна почути «Програма працює, що вам ще потрібно?» А от досвідчений програміст знає, що «працює правильно» — це лише одна з вимог до програми, причому навіть не найголовніша!

Прочитність коду

Найголовніше — щоб код програми могли зрозуміти інші програмісти. Це важливіше, ніж програма, яка правильно працює. Набагато важливіше.

Якщо у вас є програма, що працює неправильно, ви можете її виправити, а якщо у вас є програма, код якої незрозумілий, ви нічого не можете з нею зробити.

Просто візьміть будь-яку скомпільовану програму, наприклад Notepad (блокнот), і змініть у ній колір фону на червоний. Працююча програма у вас є, зрозумілого коду програми — немає: у таку програму неможливо вносити зміни.

Хрестоматійний приклад — коли розробники Microsoft прибрали з Windows гру Pinball, оскільки не змогли перенести її на 64-розрядну архітектуру. Причому в них навіть були вихідні коди цієї гри. Просто вони не могли зрозуміти, як працює написаний код.

Урахування всіх сценаріїв використання

Друга за важливістю вимога до програми — це врахування всіх сценаріїв її роботи. Часто-густо все виявляється трохи складнішим, ніж здається.

Як програміст-початківець бачить надсилання SMS у своїй програмі:

Як це бачить-програміст-професіонал:

А «правильна робота» — це зазвичай лише один із безлічі сценаріїв. І саме тому багато новачків скаржаться на валідатор задач у JavaRush: один сценарій із 10 працює, і програміст-новачок думає, що цього досить.


2. Позаштатні ситуації

Позаштатні ситуації

Під час роботи будь-якої програми можуть виникати позаштатні ситуації.

Наприклад, ви вирішили зберегти файл, а на диску немає місця. Або програма намагається записати дані в пам'ять, а пам'яті замало. Або ви завантажуєте картинку з інтернету, а в процесі завантаження перервався зв'язок.

Програміст (автор програми) повинен кожну позаштатну ситуацію а) передбачити, б) вирішити, як саме програма має діяти в цій ситуації, в) запрограмувати рішення, максимально близьке до бажаного.

Тому доволі довго поведінка програм була дуже простою: якщо в програмі ставалася помилка, програма закривалася. І це був досить хороший підхід.

Припустімо, ви хочете зберегти документ на диску і в процесі збереження з'ясовується, що місця на ньому не вистачає. Який варіант поведінки програми вам би сподобався найбільше:

  • Програма закрилася
  • Програма продовжила працювати, але файл не зберегла.

Новоспеченому програмісту може здатися, що другий варіант кращий, адже програма працює. Але насправді це не так.

Уявіть, що ви 3 години набирали текст документа у Word, хоча ще на другій хвилині роботи стало зрозуміло, що програма не може зберегти документ на диску. Що краще — втратити дві хвилини роботи чи три години?

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


3. Передісторія виникнення винятків

Позаштатні ситуації трапляються не тільки з програмами, а й усередині програми — з методами. Наприклад:

  • Метод намагається записати файл на диск, а місця немає.
  • Метод намагається викликати функцію для змінної, а змінна == null.
  • У методі виникло ділення на 0.

Водночас у деяких випадках метод, звідки здійснюється виклик, міг би виправити ситуацію (виконати альтернативний сценарій), якби знав, яка саме проблема виникла під час роботи методу, що викликається.

Якщо ми намагаємося зберегти файл на диску і такий файл уже існує, можна просто попросити користувача підтвердити перезапис файлу. Якщо на диску немає місця, можна вивести повідомлення для користувача й запропонувати вибрати інший диск. А якщо закінчилася пам'ять — програма аварійно завершиться.

Колись давно програмісти думали над цим питанням і придумали таке рішення: усі методи/функції мають повертати код помилки як результат своєї роботи. Якщо функція відпрацювала відмінно, вона повертала 0, якщо ні — повертала код помилки (не нуль).

За такого підходу до помилок програмісту після виклику майже кожної функції потрібно було додавати перевірку, чи не завершилася функція з помилкою. Код програм став значно довшим і схожим на щось таке:

Код без обробки помилок Код з обробкою помилок
File file = new File("ca:\\note.txt");
file.writeLine("Текст");
file.close();
File file = new File("ca:\\note.txt");
int status = file.writeLine("Текст");
if (status == 1)
{
   ...
}
else if (status == 2)
{
   ...
}
status = file.close();
if (status == 3)
{
   ...
}

Крім того, дуже часто функція, яка «бачила», що сталася помилка, не знала, що з нею робити: помилку потрібно було повернути функції, звідки здійснюється виклик, а та повертала її далі, туди, звідки викликали її саму і т. д.

У великій програмі ланцюжок із десятків викликів функцій — це норма: іноді навіть можна стикнутися з глибиною виклику, яка дорівнює сотні функцій. І тоді потрібно передати код помилки із самого низу на самий верх. І якщо десь на цьому шляху якась функція не обробить код завершення, помилка загубиться.

Такий підхід має ще один мінус — якщо функції повертали код помилки, вони більше не могли повертати значення своєї роботи. Доводилося передавати результат обчислень через параметри-посилання. Отож код ставав ще більш громіздким, і кількість помилок збільшувалася.