1. Задачи программиста

Очень часто начинающие программисты представляют себе суть работы программиста совсем не так, как опытные программисты.

От новичка часто можно услышать «Программа работает, что вам еще нужно?». Опытный же программист знает, что «работает правильно» — это только одно из требований к программе, причем даже не самое главное!

Читабельность кода

Самое главное — это чтобы код программы был понятен другим программистам. Это важнее, чем правильно работающая программа. Гораздо важнее.

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

Просто возьмите любую скомпилированную программу, например, notepad (блокнот) и поменяйте у него цвет фона на красный. Работающая программа у вас есть, понятного кода программы у вас нет: в такую программу невозможно вносить изменения.

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

Учет всех сценариев использования

Второе по важности требование к программе — это учет всех сценариев ее работы. Зачастую все оказывается немного сложнее, чем кажется.

Как программист-новичок видит отправку СМС в своей программе:

Правильная работа программы

Как это видит-программист-профессионал:

Правильная работа программы

А «работа правильно» — это обычно только один из множества сценариев. И именно поэтому многие новички жалуются на валидатор задач в 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)
{
   ...
}

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

В большой программе цепочка из десятков вызовов функций — это норма: иногда даже можно встретить глубину вызова в сотни функций. И вот нужно передать код ошибки с самого низа на самый верх. И если где-то на этом пути какая-то функция не обработает код завершения, ошибка потеряется.

Еще один минус такого подхода в том, что если функции возвращали код ошибки, они больше не могли возвращать значения своей работы. Приходилось передавать результат вычислений через параметры-ссылки. Это делало код еще более громоздким и еще больше увеличивало количество ошибок.