Программирование: Exceptions vs Handlers vs Promises vs Null Object

» Раздел: Общее

Хотел бы рассказать и обсудить различные способы обработки ошибок и исключительных ситуаций в современной разработке в стиле статьи + комментарии.
Буду использовать Javascript как наиболее популярный. Для нешарящих, слово function может быть объявлением класса в случае создания её свойств.
Я помню всего шесть способов обработки ошибок в программировании, но хочу обратить внимание на последние четыре.

Возврат значения

Довольно бородатый и устаревший способ - просто возвращать null или любое значение, которое означает ошибку. Распространено из Linux API.
/** @return int -1 if negative, 0 if zero */
function doSomething(i) {
	if(i < 0) {
		return -1;
	} else if(i == 0) {
		return 0;
 	}
	return i*i + 5;
}

var result = doSomething(x);
switch(result) {
	case -1: // ... negative
	case 0: // ... zero
	default: // ... success
}
Преимущества: Очень быстро и очень легко написать
Недостатки: Ошибку нельзя проверить "потом", много условий, ошибки приходится описывать в документации, возвращаемые значения по диапазону.

GetLastError()

Тоже довольно бородатый, но всё ещё используемый способ обработки ошибок - обращение к менеджеру объектов. Распространено из Windows API.
Допустим, есть менеджер manager и объект obj.
function Manager() {
	var me = this;
	me.lastError = '';

	me.getLastError = function() { return me.lastError; }

	me.doSomething1 = function(whichObj) {
		me.lastError = '';
		if(! whichObj) {
			me.lastError = 'object is null';
		}
		// ... do something and return something
	}

	me.doSomething2 = function(whichObj) {
		me.lastError = '';
		if(! whichObj) {
			me.lastError = 'object is null';
		}
		// ... do something 2 and return something 2
	}
}

var manager = new Manager();
var obj = new Obj();
manager.doSomething1(obj);
manager.doSomething2(obj);
// ...
if(manager.getLastError() == 'object is null') {
	console.log("Объект "+obj.name+" пуст.");
}
Преимущества: Ошибку в некоторых случаях можно не проверять сразу, значения довольно интуитивные и не затрагивают возвращаемые значения.
Недостатки: В некоторых случаях всё ещё не понятно, почему функция не выполнилась, для каждого родительского класса свой менеджер (при правильном проектировании), что ведет к создании родительского менеджера-обработчика всех ошибок, много мета-информации.

Exceptions

Исключения. Почти все мы знакомы с ними... Исключение - сигнал, указывающий на возникновение какой-либо исключительной ситуации или ошибки.
function doSomething1(input) {
	// ...
		throw new Error('wrong input');
	// ...
}

function doSomething2(data) {
	// ...
		throw new Error('wrong data');
	// ...
}

try {
	doSomething2(doSomething1(myInput));
} catch(e) {
	console.log(e.message);
}
Преимущества: Контроль за всеми возможными ошибками почти в произвольном месте потока, интуитивные значения, кроме значений можно передавать другие параметры отладки (в том числе колл-стек), ни коим образом не мешают логике при не-исключительных ситуациях, могут быть использованы не только для обработки ошибок, но и для других "скачков" по потоку (например, мгновенный выход и многоуровневого цикла).
Недостатки: Некоторые языки требуют обрабатывать все возможные исключения, проблемы с асинхронностью на разных языках, всё ещё не до конца решена проблема нулевых указателей (особенно в случае работы со сторонними библиотеками, ведь нулл - не всегда плохо).

Handlers

Относительно часто используемый механизм обработки ошибок - разделение потоков выполнения. Получил свою популярность с приходом понятия замыкание.
Суть обработчиков - обрывать текущий поток (в некоторых решениях - не обрывать в случае успеха) и выполнять один из двух новых - случай ошибки и случай успеха. Распространено в javascript библиотеках вроде jQuery, а так же в Nodejs
function doSomething(input, onSuccess, onFail) {
	// ...
	onSuccess && onSuccess(result);
	// ...
	onFail && onFail(error);
}

var input = 'my input';

doSomething(
	input,
	function(result) { 
		console.log('success: ' + result); 
	},
	function(error) { 
		console.log('error:' + error); 
	}
);
Преимущества: Контроль за всеми возможными ошибками, интуитивные значения, асинхронная работа (в конкретных случаях), передача любых параметров, могут быть использованы не только для обработки ошибок, но и для обработки вообще любых ситуаций.
Недостатки: В компилируемых языках приходится писать специальные механизмы для такого рода обработок, в больших проектах - трудно уследить за потоком выполнения, особенно в случае анонимных функций. Проблема "лесенок" или Callback Hell.

Promise

Объект Promise (обещание) используется для отложенных и асинхронных вычислений. Promise может находиться в трёх состояниях:
  • ожидание (pending): начальное состояние, не выполнено и не отклонено.
  • выполнено (fulfilled): операция завершена успешно.
  • отклонено (rejected): операция завершена с ошибкой.
var promise = new Promise(function(resolve, reject) {
  // здесь вытворяй что угодно, если хочешь асинхронно, потом…
  
  if (/* ..если всё закончилось успехом */) {
    resolve("Работает!");
  }
  else {
    reject(Error("Сломалось"));
  }
});

promise.then(function(result) {
  console.log(result); // "Обрабатываем результат!"
}, function(err) {
  console.log(err); // Ошибка: "Сломалось"
});
Преимущества: Контроль за всеми возможными ошибками, интуитивные значения, асинхронная работа (в конкретных случаях), передача любых параметров, могут быть использованы не только для обработки ошибок, но и для обработки вообще любых ситуаций, проверка над множествами случаев или некоторыми случаями в одном месте.
Недостатки: Реализовано далеко не во всех языках, Не подходит для повторяющихся событий, Не подходит для streams, Текущая реализация в браузерах не позволяет следит за progress

Null Object

Null Object - это объект с определенным нейтральным («null») поведением. Началось всё с книг, так же я лично замечал это в MFC с состояниями isEmpty у многих объектов, кроме того, это распространено в C++ iostream.
Лично я вижу это приблизительно так:
function NullObject() {
	var me = this;
	
	me._error = '';
	me.isBad = function() { return !!me._errror; }
	me.isGood = function() { return !me._error; }
	me.getError = function() { return me._error; }
	me.setError = function(e) { me._error = e; }
}

function MyClass() { // extends NullObject
	var me = this;
	angular.extend(me, new NullObject()); // или любая функция наследования

	// fields
	// ...
	
	// methods

	me.doSomething1 = function() {
		if(me.isBad()) { return me; }
		// ...
		me.setError('wrong vodka');
		return me;
	}

	me.doSomething2 = function() {
		if(me.isBad()) { return me; }
		// ...
		me.setError('wrong beer');
		return me;
	}
}

var obj = new MyClass();
obj.doSomething1().doSomething2().doSomething1();
if(obj.isBad()) {
	console.log(obj.getError());
}
Преимущества: Контроль за всеми возможными ошибками, интуитивные значения, передача любых параметров (если задано в родителе), могут быть использованы не только для обработки ошибок, но и для обработки вообще любых ситуаций, реализуемо в любых ООП языках, последовательное выполнение методов без остановки потока.
Недостатки: Синхронность, нужно писать некоторые проверки в требуемых методах, нет работы с множеством случаев, требуется перестраивать дерево наследования, дополнительные данные в объектах.

Заключение

Какие ещё вы знаете способы обработки ошибок и где они могут применяться?

Просмотров: 4 979

» Лучшие комментарии


Doc #1 - 5 лет назад 0
Последнее это скорее твой выдуманный велосипед, про который ты мне загонял еще года два назад. В норм функциональщине это по дефолту есть, см. haskell maybe
Hanabishi #2 - 5 лет назад (отредактировано ) 6
Вообще исключения смотрят на все остальные способы как на говно, хотя бы потому что можно создать множество вложенных в друг друга обработчиков.
Какие ещё вы знаете способы обработки ошибок и где они могут применяться?
Если извращаться, то можно ещё так:
//пример на шарпе
void A(){
	//создаем и запускаем контрольный поток
	Thread t = new Thread(B);
	t.Start();
	t.Join();
	//будет ждать до завершения контрольного потока
	
	if(t.Name=="pass"){
		//контрольный поток отработал без ошибок
	} else {
		//что-то сломалось
	}
}

void B(){
	doSomething();
	
	Thread.CurrentThread.Name = "pass";
}
DioD #3 - 5 лет назад 0
При грамотном подходе можно полностью избежать необходимости обрабатывать исключения проверяя параметры всех методом на входе в процедуру.
Механизм исключений явы и шарпов который имеет особую процедуру призван решить проблему волшебных числе в возврате, например мы ожидаем что если метод вернул -1 то он отработал с ошибкой, но что если этот выход на самом деле допустимый?
Именно это и решают исключения, которые передают те самые -1 но особым образом.
для додиеза имеется достаточно забавная методика борьбы с ошибками методом множественного возврата на основе передачи параметров по указателю:
public boolean likelytofailhard (object 0,ref resultstore)
операции над resultstore внутри метода приведут к изменению этого значения вне метода, так как модифицируют память по указателю, а не переданное значение.
при этом обычный возврат метода можно использовать для иных целей
checksum = 10;
if likelytofailhard(new Car(),checksum)
display(error)
display(checksum)
выглядит такая "лесенка" не очень интуитивна но при грамотном подходе просто идеальный метод обработки ошибок связанных с вводом пользователя.
alexprey #4 - 5 лет назад 0
DioD, второй пример классический во многих API, например DirectX 11. В OpenGl используется GetLastError, в Win сокетах, тоже GetLastError, да и в WinAPI тоже. На самом деле это самый ущербный способ хендлить ошибки.
Согласен, что второй пример очень удачен с точки зрения хендлинга ошибок пользовательского ввода.
Теперь что касается исключений, на самом деле это весьма мощная штука в плане отладки, как локально так и на продакшенах всяких и альфах, если использовать сериализаторы или использовать специальный сервис для аггрегации исключений. В чем соль исключений?
  • Сохранение нужных данных, которые потом можно обработать.
  • Благодаря иерархический структуре исключений, можно обрабатывать исключения там, где это нужно. При должной организации приложения все складывается очень хорошо.
ScorpioT1000 #5 - 5 лет назад (отредактировано ) 0
Doc, www.cplusplus.com/reference/ios/ios/bad
DioD, не особо удобный способ, я даже на крестах избавлялся от него, кстати, в пользу возврата массива или объекта с результатом и/или ошибкой
суть, что не вызовешь несколько методов по очереди, если гдето вернется нулл, например, с пустыми объектами и исключениями это решаемо
DioD #6 - 5 лет назад 0
Если допускать failfast поведение не разумно то можно получить сложновыявляемые ошибки которые проявляются на 1 случай из 100 но проявляются.
Код должен быть атомарным, или он выполняется целиком или никак.
Например метод который изменяет стек или регистры должен вносить реальные изменения в систему крайней линией, а не последовательно прерываясь линиями которые могут вызвать ошибку или прерывание.
Иначе можно столкнуться с ситуацией, когда метод завершился с ошибкой, выдал ошибку, вернул массив с пустыми объектами, но при этом "поправил" счётчики или изменил регистры.
Вопрос атомарности в статье не решен, однако "богомерзкие" майкрасофт со своим "ласт эррор" тем не менее реализовали многие методы атомарно, это необходимо отразить в статье.
ScorpioT1000 #7 - 5 лет назад 0
это скорее вопрос транзакций, очень широкая тема, почему те же сервер-сайд решения пытаются все сместить к базам данных, чтобы сервер "отрабатывал" короткую логику, остальное на базу данных, которая эти транзакции поддерживает
Mihahail #8 - 5 лет назад 0
Есть ещё эрланговское let it crash :)
Но это немного не по сабжу
alexprey #9 - 5 лет назад 0
чтобы сервер "отрабатывал" короткую логику, остальное на базу данных, которая эти транзакции поддерживает
Всм? У нас на проекте лично, наоборот стараемся минимизировать кол-во обращений к базам данных
Zahanc #10 - 5 лет назад 0
В Oracle DB есть возможность писать транзакции на SQL и сохранять в БД, чтобы минимизировать нагрузку на серверный код. Не нужно никаких исключений и прочего — просто вызываете SQL процедуру, а об остальном позаботится БД.
alexprey #11 - 5 лет назад 0
bladget, я знаю, но у нас наоборот разгружают сервер БД по возможности
ScorpioT1000 #12 - 5 лет назад 1
правильно, бородатая mysql база, что от неё ожидать)
prog #13 - 5 лет назад (отредактировано ) 0
В основном имею дело с исключениями. Удобно, гибко, а при добавлении должного количества синтаксического сахара еще и компактно.
Из интересного сталкивался с множественным возвратом в LUA, где некоторые API используют эту возможность для возврата не только результатов работы функции, но и дополнительной информации.
Еще краем уха зацепил аспектно-ориентированное программирование. Серьезных вещей с его использованием еще не писал, так что не скажу насколько оно офигенно на самом деле, но возможность вынести код обработки исключений и кучу другого вспомогательного кода отдельно от логики мне понравилась.
GeneralElConsul #14 - 5 лет назад (отредактировано ) 0
Windows поддерживает структурную обработку исключений: _try{} ; _except(выражение-фильтр); finally{}.
Их преимущество - ориентированы не только на обработку программных ошибок, но и аппаратных. И выражение-фильтр опять же.
Однако они дико не удобные. Мало того, что ловить, например, 2 ошибки надо так:
_try
{
_try
{
........
}
_except(выражение-фильтр)
{
.........
{
}
_except(выражение фильтр)
( второй try и первый except вложены в первый try )
так есть еще и масса других неприятных тонкостей. Например, если в выражение фильтр подставить функцию(возвращающее значение) с более чем двумя входящими аргументами то про возврат в выражений фильтр EXCEPTION_CONTINUE_EXECUTION, которая позволяет вернутся к тому месту кода, в котором возникла ошибка и опять выполнить его(при условии что мы исправили ситуацию в нашей функции, например, проинициализировали ранее нулевой указатель), можно забыть.
Будет бесконечно возникать и отлавливаться исключение, выполнятся в нашей функции, которая опять вернет в выражение-фильтр EXCEPTION_CONTINUE_EXECUTION -> возврат в код, где опять возникает исключение и так до бесконечности.
alexprey #15 - 5 лет назад 0
GeneralElConsul, эм... если это си, то там все как-то не так страшно. Или это что-то другое?
ScorpioT1000 #16 - 5 лет назад 0
это вроде windows api
GeneralElConsul #17 - 5 лет назад 0
это вроде windows api
Да, это оно.
XimikS #19 - 4 года назад 0
Doc:
Последнее это скорее твой выдуманный велосипед, про который ты мне загонял еще года два назад. В норм функциональщине это по дефолту есть, см. haskell maybe
Два чая этому господину, остальное шняга.