Выполнение рекомендаций из предыдущих разделов обеспечивает хорошее разделение бизнес-логики и кода обработки ошибок. Основной код программы начинает выглядеть как простой алгоритм, не отягощенный посторонними вставками. Однако в результате код обнаружения ошибок смещается на периферию вашей программы. Вы создаете обертки для внешних API, чтобы иметь возможность инициировать собственные исключения, и определяете обработчик, который находится над основным кодом и позволяет справиться с любым прерыванием вычислений. Обычно такое решение отлично работает, но в некоторых ситуациях прерывание нежелательно.
Рассмотрим конкретный пример. В следующем, довольно неуклюжем фрагменте суммируются командировочные расходы на питание:
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
Если работник предъявил счет по затратам на питание, то сумма включается в общий итог. Если счет отсутствует, то работнику за этот день начисляется определенная сумма. Исключение загромождает логику программы. А если бы удалось обойтись без обработки особого случая? Это позволило бы заметно упростить код:
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
Можно ли упростить код до такой формы? Оказывается, можно. Мы можем изменить класс ExpenseReportDAO, чтобы он всегда возвращал объект MealExpense. При отсутствии предъявленного счета возвращается объект MealExpense, у которого в качестве затрат указана стандартная сумма, начисляемая за день:
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// Вернуть стандартные ежедневные затраты на питание
}
}
Такое решение представляет собой реализацию паттерна ОСОБЫЙ СЛУЧАЙ [Fowler]. Программист создает класс или настраивает объект так, чтобы он обрабатывал особый случай за него. Это позволяет избежать обработки исключительного поведения в клиентском коде. Все необходимое поведение инкапсулируется в объекте особого случая.
Не возвращайте null
На мой взгляд, при любых обсуждениях обработки ошибок необходимо упомянуть о неправильных действиях программистов, провоцирующих ошибки. На первом месте в этом списке стоит возвращение null. Я видел бесчисленное множество приложений, в которых едва ли не каждая строка начиналась с проверки null. Характерный пример:
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = persistentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
Если ваша кодовая база содержит подобный код, возможно, вы не видите в нем ничего плохого, но это не так! Возвращая null, мы фактически создаем для себя лишнюю работу, а для вызывающей стороны — лишние проблемы. Стоит пропустить всего одну проверку null, и приложение «уходит в штопор».
А вы заметили, что во второй строке вложенной команды if проверка null отсутствует? Что произойдет во время выполнения, если значение persistentStore окажется равным null? Произойдет исключение NullPointerException; либо кто-то перехватит его на верхнем уровне, либо не перехватит. В обоих случаях все будет плохо. Как реагировать на исключение NullPointerException, возникшее где-то в глубинах вашего приложения?
Легко сказать, что проблемы в приведенном коде возникли из-за пропущенной проверки null. В действительности причина в другом: этих проверок
Довольно часто объекты особых случаев легко решают проблему. Допустим, у вас имеется код следующего вида:
List
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
Сейчас метод getEmployees может возвращать null, но так ли это необходимо? Если изменить getEmployee так, чтобы метод возвращал пустой список, код станет чище: