Войти
ФлеймФорумПрограммирование

Мейнстримная система эксепшенов ущербна. Но её можно улучшить.

Страницы: 1 2 311 12 Следующая »
#0
12:28, 23 окт. 2017

Мейнстримная система эксепшенов ущербна. Но её можно улучшить.

Система эксепшенов в современных мейнстримных языках ущербна. Хотя это не очевидно на первый (и даже на второй) взгляд, но чем больше программируешь, тем сильнее ощущаешь - что-то не то. Например:

Email ParseEmail(String email) throws ParseError
{
...
ThirdPartyHelper tph ();
tph.ApiFunction(...);
...
}
Как культурные разработчики апи, мы желаем, чтобы в случае штатных ошибок наша функция кидала только ParseError (и держим в уме прежде всего ошибку формата). Но не всё так просто: ThirdPartyApiFunction написана макаками и может кинуть абсолютно всё, что угодно (от системной ошибки до ошибок ихней библиотеки, которых числом 100500).
Какие у нас варианты?
Вариант 1:
Email ParseEmail(String email) throws AnyException
// потому что мы используем функции, написанные макаками, и проще при любой ошибке включать режим "всё плохо",
// чем что-то гарантировать

Вариант 2:

Email ParseEmail(String email) throws ParseError
try {
ThirdPartyHelper tph ();
tph.ApiFunction(...);
} catch (AnyException e) // ибо не писать же 100500 кейсов
{
 throw ParseEmail("Third party fail"){ cause: e } ; // конвертируем ихнюю ошибку в нашу
 // ихнюю на всякий случай сохраняем, чтоб уж для очистки совести
}
И тут нам в e вдруг прилетает какой-нибудь OutOfDiskException. Говнокод от макак сам по себе ничего плохого не сделал, но он, оказывается, использовал библиотеку дебажного логгинга (написанную ещё какими-то макаками) и точно так же не ожидал от неё подвоха, как мы не ожидаем от него. А ей взяло и не хватило места для записи лога.
Что функция, которая хотела всего лишь распарсить мыло, может сделать по поводу внезапного OutOfDiskException? Да ничего: ошибка явно не её компетенции. Однако OutOfDiskException до неё не долетит - она получит ParseError и попытается как-то там адекватно отреагировать. Показать ругательный попап, или включить автокорректор... Ну а что, казалось бы, ещё может случиться при парсинге мыла? А между тем, уже пора тушить воду и сливать парашют.

Итак, ущербность мейнстримной системы эксепшенов в том, что ошибка кидается "в пустое пространство", без оглядки на контекст ошибки и компетенцию того, кто может её поймать. А поймать может совсем не тот, кому следовало.
Можно спроектировать иерархию классов ошибок, которая вроде как решит проблему (FatalException для совсем плохих ошибок, TransientException для ошибок, которые можно устранить и попытаться запустить какой-то участок ещё раз...) - но, во-первых, решит ли? А во-вторых - ну вот надо нам написать функцию ParseEmail, как определить, ошибка формата - это ошибка уже фатальная, или ещё поправимая?

Что же делать?
А делать вот что: нужно заменить ловлю эксепшона по классу на ловлю по конкретному инстансу.

void FunctionThatCanThrow(Exception &expectedException) // например
{
 if (rand ()&1) throw expectedException;
}

Exception e;
try {
FunctionThatCanThrow (e); // ловим конкретно инстанс e, и никакой не другой
} catch (e) {
 printf ("The expected fuckup happened\n");
}
А конкретный инстанс, в отличие от класса, можно привязать к какому-нибудь контексту или зоне ответственности. И тут нам становятся доступными разные осмысленные варианты.

Разработка нашего примера могла бы, например, выглядеть так.
1) Макака 1 пишет библиотеку логгера:

class Logger
{
 public /*thread local*/ Exception e; // через него будут кидаться ошибки в процессе логгинга, если вдруг что
 // под "если вдруг что" макака 1 понимает "нечто совсем уж непоправимое" и отражает это в документации
 public log (String s)
 {
  ...
  if (someProblem) throw e;
 }
};

Logger globalLogger ();

2) Макака 2 пишет объект ThirdPartyHelper, используя Logger.

class ThirdPartyHelper
{
 public /*thread local*/ Exception e;
 // вводя этот член, макака подразумевает, что через него будут кидаться ошибки, созданные именно
 // кодом данного инстанса ThirdPartyHelper и к логике его предметной области,
 // и отражает это подразумение в документации

 void ThirdPartyApiFunction(...)
 {
  // макака 2 не ждёт от логгера подлянки, основываясь на идее, что в случае "нечто совсем уж непоправимое"
  // из обычного логгера ловить всё равно ловить уже нечего
  // (хотя вообще можно было бы ловить catch (globalLogger.e)).
  globalLogger.log("ThirdPartyApiFunction called ok");

  // а вот, например, за возникновение ошибки о невалидных параметрах данные класс отвечает лично,
  // поэтому её кидать вполне логично
  if (InvalidParameters(...)) { e.SetErrorDetails("invalid parameters"); throw e; }
 }
 ...

3) Мы используем ThirdPartyHelper:

Email ParseEmail (String email,Exception parseException)
{
 ...
 ThirdPartyHelper tph ();
 try { tph.ApiFunction(); }
 catch (tph.e)
 {
  // если в tph возникла ошибка, является ли это ошибкой и нашей ParseEmail?
  // допустим, является
  parseException.SetErrorDetails("Third party fail");
  throw parseException;

  // Заметьте, что эксепшен, вылетевший из globalLogger и непойманный в tph, здесь не ловится,
  // и более того - чтобы его поймать, текущему коду нужно иметь доступ к globalLogger.
  // Если Logger используется в ThirdParty приватно, то нам здесь его вообще никак не поймать.
  // Что правильно.
 }
 ...
 if (!email.match(/[-.a-zA-Z_]+@[-.a-zA-Z_]+/))
 {
  parseException.SetErrorDetails("Invalid email");
  throw parseException;
 }
 ...
}

Казалось бы, можно так же сделать и через эксепшоны по классам:

class LoggerException;
class ThirdPartyException;
class ParseException;

class Logger
{
 public log (String s)
 {
  ...
  if (someProblem) throw LoggerException ();
 }
};

class ThirdPartyHelper
{
 void ThirdPartyApiFunction(...)
 {
  globalLogger.log("ThirdPartyApiFunction called ok");
  if (InvalidParameters(...)) throw ThirdPartyException("invalid parameters");
 }
 ...
}

Email ParseEmail (String email,Exception parseException)
{
 ...
 ThirdPartyHelper tph ();
 try { tph.ApiFunction(); }
 catch (ThirdPartyException e)
 {
  throw new ParseException() { cause: e };
 }
 ...
 if (!email.match(/[-.a-zA-Z_]+@[-.a-zA-Z_]+/)) throw new ParseException;
 ...
}

Но тут мы сразу видим недостатки:
1) В случае ловли по инстансу мы обошлись одним-единственным классом, а в случае ловли по классу их понадобилось целых 3.
2) Ловя эксепшен от tph.ApiFunction по инстансу, дизайн класса даёт гарантирует, что ошибка случилась с ведома объекта tph, а не что он вызвал изнутри себя какой-нибудь придурковатый калбакс или коллаборатора, которому почему-то тоже вздумалось кинуть ThirdPartyException. При ловле по классу дать такую гарантию средствами дизайна невозможно.
3) Имя класса глобально, поэтому у нашего ParseEmail в зоне видимости маячит LoggerException, который ему вообще-то совсем не нужен. И даже вреден, поскольку catch (LoggerException) в зоне его ответственности категорически не предполагается.

С другой стороны, у кидания и поимки эксепшена по инстансу (если кто ещё не понял по нашему примеру - по заранее существующему инстансу) появляется профит: снимается вопрос, в какой памяти аллоцировать и содержать инстанс. (Не слишком актуально для языков, где объекты создаются динамически, но для языка вроде Ц++ - очень даже.)

Таким образом, только переходом на кидание и поимку эксепшонов по инстансу, можно улучшить культуру обработки ошибок и приблизить ситуацию с нею к Светлому Будущему.


#1
12:51, 23 окт. 2017

Sbtrn. Devil
> Email ParseEmail(String email) throws AnyException
да, так и делать. Если вызывается функция которая может кинуть AnyException, то ее вызыватель тоже должен кидать AnyException, а не пытаться что-то там угадать.

> 1) В случае ловли по инстансу мы обошлись одним-единственным классом, а в
> случае ловли по классу их понадобилось целых 3.
Ничего плохого. Особенно если объявление класса ошибки состоит из одной строчки.

> 2) Ловя эксепшен от tph.ApiFunction по инстансу, дизайн класса даёт
> гарантирует, что ошибка случилась с ведома объекта tph, а не что он вызвал
> изнутри себя какой-нибудь придурковатый калбакс или коллаборатора, которому
> почему-то тоже вздумалось кинуть ThirdPartyException. При ловле по классу дать
> такую гарантию средствами дизайна невозможно.
Придурковатый калбактор мог кинуть tph.e. И в некоторых случаях это и нужно сделать. Поэтому ничего плохого не вижу. Если уж есть плохое и нужна гарантия - спрятать конструктор ThirdPartyException в дружественном классе или что там за язык рассматривается.

> 3) Имя класса глобально, поэтому у нашего ParseEmail в зоне видимости маячит
> LoggerException, который ему вообще-то совсем не нужен. И даже вреден,
> поскольку catch (LoggerException) в зоне его ответственности категорически не
> предполагается.
неймспейсы же есть. Или в чем проблема? Хотя необходимости в LoggerException я не вижу - если что-то пошло не так со стороны ОС, то и исключение должно быть OSException.

#2
12:53, 23 окт. 2017

1) Что-то кода не сильно меньше стало.
2) А что если одна библиотека бросает 3 вида своих ошибок, все их переменными объявлять? Или этот механизм как-то должен быть автоматически встроен, чтоб вручную не объявлять?

Кто вообще разрешил людишкам свои ошибки писать? Либеральничать удумали тут? От этого и беды все что порядка не хватает. И ведь каждый мнит что у него какие-то свои ошибки, требующие своих особых обработок.  Я предлагаю все не стандартные ошибки запретить и ни в какие кэтч-блоки их не пущать.
#3
15:20, 23 окт. 2017

На самом деле существует всего два варианта возможных исключительных ситуаций:

0) В текущей операции что-то пошло не так из-за кривости входных данных, несовпадения фаз Луны и т. д. Операция возвращает код ошибки.
1) Произошла какая-то катастрофа - кончилась память, стек переролнился, помер жёсткий диск. В таких ситуациях всё приложение вообще должно грохаться без возврата.

Выходит, сами исключения вместе с их разветвлённой иерархией, аннотациями о том, кто чего кидает ущербны и не нужны.
Достаточно лишь иметь коды возврата для случая 0 в качестве возвращаемых значений.

#4
15:30, 23 окт. 2017

Panzerschrek[CN]
> 1) Произошла какая-то катастрофа - кончилась память, стек переролнился, помер
> жёсткий диск. В таких ситуациях всё приложение вообще должно грохаться без
> возврата.
Но желательно потушить реактор перед гроханьем.

#5
15:36, 23 окт. 2017

1 frag / 2 deaths
> Но желательно потушить реактор перед гроханьем.

Тушение реактора, закрытие файлов и сетевых соединений и т. д. надо вешать на atexit.

#6
15:41, 23 окт. 2017

Когда уже ТС-а пригласят в комитет стандартизации ЦЭКРЕСТКРЕСТ.

#7
15:43, 23 окт. 2017

Неосиляторы исключений

#8
15:43, 23 окт. 2017

Panzerschrek[CN]
>1) Произошла какая-то катастрофа - кончилась память, стек переролнился, помер жёсткий диск. В таких ситуациях всё приложение вообще должно грохаться без возврата.
В случае с нехваткой памяти можно попытаться ее освободить и далее повторить операцию.

#9
15:49, 23 окт. 2017

1 frag / 2 deaths
> Но желательно потушить реактор перед гроханьем.
нет. не желательно.
приложение именно, что должно умереть.
быстро, молча, и без каких либо телодвижений.

тушить реактор системой,
которая уже находится в неконсистентном состоянии - слишком опасно.

реактор будет потушен резервной системой.

в отказоустойчивых системах действует принцип:
столбим все ресурсы на старте, либо вообще не взлетаем.

а пока летим - держим наготове резервные системы.

#10
15:50, 23 окт. 2017

nes
> В случае с нехваткой памяти можно попытаться ее освободить и далее повторить
> операцию.

если вас так парит отказоустойчивость,
значит всю необходимую память захапаете ещё на старте.
и в процессе работы выделять больше не будете.

если не парит - нах не нужны никаких головняки.
пускай падает.

#11
15:53, 23 окт. 2017

Kartonagnick
В некоторых случаях ты просто обязан что-то придпринять, например в Айосах есть даже специальное событие, которое оповещает тебя,
что ты просрал все свои ресурсы и требует, чтоб ты придпринял какие-нибудь меры по ее освобождению.

#12
15:59, 23 окт. 2017

nes
> В случае с нехваткой памяти можно попытаться ее освободить и далее повторить
> операцию.

А как ты собрался это через исключения делать? Если new кинул исключение, то ты уже ничего не сделаешь.
По-нормальному, надо у менеджера памяти регистрировать обратный вызов на такой случай, и пытаться освобождать память в нём.

#13
16:12, 23 окт. 2017

nes
> В некоторых случаях ты просто обязан что-то придпринять, например в Айосах есть
> даже специальное событие, которое оповещает тебя,
> что ты просрал все свои ресурсы и требует, чтоб ты придпринял какие-нибудь меры
> по ее освобождению.
>

нет никаких "некоторых случаев".

есть случаи когда:
"говно не должно случится никогда. независимо от ситуации"

и случаи когда:
"да и насрать"

#14
22:03, 23 окт. 2017

Kartonagnick
Ты для начала хоть что нибудь кроме студенческих теоретических лаб научись кодить.

Страницы: 1 2 311 12 Следующая »
ФлеймФорумПрограммирование

Тема в архиве.