Здравствуйте, Сергей Губанов, Вы писали:
СГ>Чтобы был возможен откат, базис примитивных операций должен быть другой, ибо операция "Начислить" — необратима.
Она, может, и необратима, но это не значит, что она всегда завершается успешно. Так вот в случае неудачи состояние обоих счетов (отправителя и получателя) должно остаться в точности таким же, как до начала операции.
Один из возможных сценариев (упрощенный, естественно).
1. Видимая владельцу счета-отправителя сумма уменьшается на величину перевода.
2. Сумма перевода заносится в специальный журнал, ей присваивается статус "ожидание".
3. Отправляется запрос получателю.
4. Принимается подтверждение от получателя о приеме.
5. Сумма списывается со счета.
6. Операция получает статус "выполнена".
На каждом из шагов может произойти что угодно. Например, кто-нибудь оборвал сетевой кабель на одном из серверов на маршруте. Функция, ожидающая ответа, вернет банальный time-out, ничего не говорящий о том, как завершилась операция.
Вот вариант обработки исключений в случае серьезного обрыва связи: подтверждение шлется по телефону/факсу и все обновления данных выполняются вручную на обоих концах линии.
Здравствуйте, Сергей Губанов, Вы писали:
СГ>Здравствуйте, mrozov, Вы писали:
M>>Но главное — да приведите же наконец хоть одну причину, по которой от исключений нужно отказаться.
СГ>Признаюсь, едва ли мне по силам найти хоть одну причину убедившую бы Вас. Впрочем, и Вам тоже едва ли по силам найти серьёзную причину по которой исключения должны быть встроены в язык, а не в библиотеку.
Да, это будет сложно. Но не нужно. Да пускай хоть в подсознание. Хоть тушкой, хоть чучелом — но ехать надо.
S>1. Все функции обязаны возвращать булевый признак успешности выполнения. S>2. Все функции обязаны передавать информацию о причине неудачи. S>3. Если нам надо что-то большее, чем текст, в качестве информации о причине неудачи, то мы встряли. Потому что сигнатуры всех функций, имевших несчастье попасть в стек вызовов между тем местом, где мы обрабатываем ситуацию, и тем местом, где она произошла, должны быть модифицированы для передачи этой информации. Что-то более-менее общее мы использовать не сможем.
Мне это напоминает паскаль или COM с его HRESULT ;(
...Ei incumbit probatio, qui dicit, non qui negat...
Создайте сокет и забиндите его на любой из портов в этом диапазоне (но некоторые порты уже могут быть заняты).
Трабл в том, что процедура socket.Bind(this.point) кидает исключение если порт уже занят,
хотя в миллион раз было бы логичнее, чтобы это была булевская функция.
namespace MeraSystems.Net
{
public sealed class StdSocketFactory: MeraSystems.Net.ISocketFactory
{
private readonly System.Net.IPAddress localIP;
private readonly int minPort, maxPort, incPort, cntPort;
private readonly System.Net.IPEndPoint point;
private static int CalculatePortsCount (int min, int max, int inc)
{
int count = 0;
for (int i = min; i <= max; i += inc)
{
count++;
}
return count;
}
public StdSocketFactory (System.Net.IPAddress ThisLocalIP, int ThisMinPort, int ThisMaxPort, int ThisIncPort)
{
ASSERT.PRE(ThisLocalIP != null, "{ThisLocalIP is null}");
if (ThisMinPort % 2 == 1) ThisMinPort++;
if (ThisMaxPort % 2 == 1) ThisMaxPort--;
if (ThisIncPort < 1) ThisIncPort = 1;
ASSERT.PRE(ThisMinPort <= ThisMaxPort, "{ThisMinPort > ThisMaxPort}");
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -this.localIP = ThisLocalIP;
this.minPort = ThisMinPort;
this.maxPort = ThisMaxPort;
this.incPort = ThisIncPort;
this.cntPort = CalculatePortsCount(ThisMinPort, ThisMaxPort, ThisIncPort);
this.point = new System.Net.IPEndPoint(ThisLocalIP, ThisMinPort);
}
private void SetNextPort ()
{
this.point.Port += this.incPort;
if (this.point.Port > this.maxPort) this.point.Port = this.minPort;
}
private bool TryBindNextPort (System.Net.Sockets.Socket socket)
{
bool result = false;
try
{
socket.Bind(this.point); result = true;
}
catch
{
}
this.SetNextPort();
return result;
}
public System.Net.Sockets.Socket NewSocket (System.Net.Sockets.SocketType socketType, System.Net.Sockets.ProtocolType protocolType)
{
lock(this)
{
System.Net.Sockets.Socket s = new System.Net.Sockets.Socket(this.point.AddressFamily, socketType, protocolType);
for (int n = 0; n < this.cntPort; n++)
{
if (this.TryBindNextPort(s)) return s;
}
return null;
}
}
}
}
2) Следующий ненадуманный пример.
Есть виндос-форма, в поля которой пользователь вводит числа.
Если пользователь ошибся и ввел неправильную строку, то надо ему об этом сообщить.
Но стандартная функция
int System.Int32.Parse (string str)
имеет ошибку в дизайне — она кидает исключенние, приходится либо писать свой парсер чисел (что влом)
либо писать так :
bool TryGetNumber (string str, out int number)
{
try
{
number = System.Int32.Parse(str);
return true;
}
catch
{
number = 0;
return false;
}
}
Сергей Губанов wrote:
>>> после свершившегося начисления, откат назад невозможен. > C>Это почему же? > Деньги *уже начисленные* на счёт в другом банке??????????
Ни одна серьезная система так не работает. Для таких переводов всегда
используется расперделенные транзакции (как они работают объяснять лень
— поищите по словам "distributed transactions" и "two-phase commit").
> Чтобы был возможен откат, базис примитивных операций должен быть > другой, ибо операция "Начислить" — необратима.
Операция "начислить" — необратима. Однако можно отменить совершенные ей
изменения.
Здравствуйте, Privalov, Вы писали:
P>Один из возможных сценариев (упрощенный, естественно).
P>1. Видимая владельцу счета-отправителя сумма уменьшается на величину перевода. P>2. Сумма перевода заносится в специальный журнал, ей присваивается статус "ожидание". P>3. Отправляется запрос получателю. P>4. Принимается подтверждение от получателя о приеме. P>5. Сумма списывается со счета. P>6. Операция получает статус "выполнена".
В том коде который я привёл именно этот сценарий и реализован (в упрощенном варианте конечно).
После этого меня спросили, а что если при выполнении пункта (6) произошла ошибка записи информации в локальный журнал работы — как я буду откатывать всё взад?
Я и говорю, что на момент выполнения пункта (6) деньги уже перечислены (от получателя уже пришло подтверждение о получении им денег — он их уже может быть даже успел истратить ). Так что на шаге (6) взад уже ничего не вернёшь.
Здравствуйте, Cyberax, Вы писали:
C>Ни одна серьезная система так не работает. Для таких переводов всегда C>используется расперделенные транзакции (как они работают объяснять лень C>- поищите по словам "distributed transactions" и "two-phase commit").
Я это прекрасно понимаю.
Однако тут речь не о том как это по настоящему работает,
а о реализации вымышленного механизма денежного перевода
основанного на единственном примитиве "Начислить"
причём без использования exceptions.
Меня просили так сделать — я так и сделал.
Здравствуйте, mrozov, Вы писали:
M>Простите. вы вообще имели когда-нибудь дело с платежными системами? Я вот да.
Отродясь не имел. Но мы тут спорим о платежах или об exceptions? Если не нравится пример с платежами, давайте рассмотрим какой-нибудь другой пример, я его берусь написать без использования exceptions.
Здравствуйте, Сергей Губанов, Вы писали:
СГ>Здравствуйте, Privalov, Вы писали:
P>>Один из возможных сценариев (упрощенный, естественно).
P>>1. Видимая владельцу счета-отправителя сумма уменьшается на величину перевода. P>>2. Сумма перевода заносится в специальный журнал, ей присваивается статус "ожидание". P>>3. Отправляется запрос получателю. P>>4. Принимается подтверждение от получателя о приеме. P>>5. Сумма списывается со счета. P>>6. Операция получает статус "выполнена".
СГ>В том коде который я привёл именно этот сценарий и реализован (в упрощенном варианте конечно). СГ>После этого меня спросили, а что если при выполнении пункта (6) произошла ошибка записи информации в локальный журнал работы — как я буду откатывать всё взад? СГ>Я и говорю, что на момент выполнения пункта (6) деньги уже перечислены (от получателя уже пришло подтверждение о получении им денег — он их уже может быть даже успел истратить ). Так что на шаге (6) взад уже ничего не вернёшь.
Ошибаетесь.
До выполнения п.6 опрерации перечисления не существует в природе. Ни для отправителя, ни тем более для получателя операция не существует — она существует только в воспалённом мозгу компьютера, в котором работают "какие-то" программы. И только после п.6 всем вдруг становится ясно, что "операция по переводу денег совершилась". В этом и суть транзакции.
Здравствуйте, Сергей Губанов, Вы писали:
СГ>В том коде который я привёл именно этот сценарий и реализован (в упрощенном варианте конечно). СГ>После этого меня спросили, а что если при выполнении пункта (6) произошла ошибка записи информации в локальный журнал работы — как я буду откатывать всё взад?
Этот вопрос разве не был Вами проигнорирован?
СГ>Я и говорю, что на момент выполнения пункта (6) деньги уже перечислены (от получателя уже пришло подтверждение о получении им денег — он их уже может быть даже успел истратить ). Так что на шаге (6) взад уже ничего не вернёшь.
Вернешь, вернешь. До завершения шага 6 переводимая сумма заморожена на обоих счетах. Фактическое начисление/списание происходит только после получения подтверждения об успешном переводе. До этого получатель сумму не увидит, не говоря уже об истратить.
Здравствуйте, Сергей Губанов, Вы писали:
СГ>Однако тут речь не о том как это по настоящему работает, СГ>а о реализации вымышленного механизма денежного перевода СГ>основанного на единственном примитиве "Начислить" СГ>причём без использования exceptions. СГ>Меня просили так сделать — я так и сделал.
Здравствуйте, Privalov, Вы писали:
P>Вернешь, вернешь. До завершения шага 6 переводимая сумма заморожена на обоих счетах. Фактическое начисление/списание происходит только после получения подтверждения об успешном переводе. До этого получатель сумму не увидит, не говоря уже об истратить.
Значит Вы пользуетесь другим базисом примитивных операций. А вот в базисе состоящем всего из одной примитивной операции "Начислить" — откат после начисления невозможен. О чём я в сотый раз уже повторил. И exceptions тут совсем не при чём. Ёмоё сколько можно, всё надоело, хватит....
M>>Исключения — это стандартный и универсальный механизм, с помощью которого код может делегировать принятие решений коду вызывающему данный. Неважно, о чем мы говорим — о модуле, функции или трех строчках кода — это все равно так.
E>Причем не важно, сигнализирует ли исключение об ошибке, исключительной ситуации или же исключения используются для реализации логики (бывают и такие ситуации, когда исключения удобнее чем if/elseif/else или switch/case).
Яркий пример — разбор ХМЛя многими библиотеками. Обычно достаточно знать, что ХМЛ невалидный и сразу завершить работу:
В процессе эксплуатации оказалось, что ХМЛ в 99% случаев приходит валидный (так как я сам его и отсылаю ). Вопрос. Нафига мне там столько if-ов? Когда документ начинает расти, код читать становится неприятно.
int displayReservationCommand::execute()
{
QDomDocument xmlDoc;
xmlDoc.setContent(_params["data"]);
try{
QDomElement el = xmlDoc.documentElement();
el = el.firstChild().toElement();
QString str;
QTextStream ts(str, IO_WriteOnly);
el.save(ts, 0);
QString attr = el.attribute("guid", "-1");
iqAdrReservationApplicationForm::displayReservation(attr, str);
}catch(... /*на самом деле здесь надо что-нить типа XMLTagError, но мне все равно.*/){
return false;
}
return true;
}
Сама функция возвращает application-specific error-code.
Здравствуйте, Сергей Губанов, Вы писали:
M>>Но главное — да приведите же наконец хоть одну причину, по которой от исключений нужно отказаться.
СГ>Признаюсь, едва ли мне по силам найти хоть одну причину убедившую бы Вас. Впрочем, и Вам тоже едва ли по силам найти серьёзную причину по которой исключения должны быть встроены в язык, а не в библиотеку.
Исключения, встроенные в библиотеку (по крайней мере, основывающиеся на раскрутке стэка) — это очень, очень плохо. Посмотрите на OS Symbian и ее C++ API — там как раз исключения поддерживаются на уровне библиотеки (API), а не на уровне языка. Во что это выливается на практике? В кромешный ад — не забыть запихнуть объект в CleanupStack, чтобы по возникновении исключения он был корректно уничтожен, не забыть положить туда же объект, созданный на стэке, причем cleanup операцией для него будет являться не деструктор, а некий виртуальный метод Close, что автоматически означает необходимость вызова этой же Close при нормальном завершении времени жизни объекта (вручную! ). Это также означает необходимость two phase construction — сначала для объекта вызывается его конструктор (который ничего практически не делает!) и только затем (после заталкивания его в CleanupStack) — функция ConstructL, которая и выполняет фактические конструирующие действия, небезопасные с точки зрения исключений. Тем самым очень сложно становится полагаться на RAII.
Но самое интересное во всем этом — это то, что ничего больше и нельзя было сделать на уровне библиотеки. Да, это криво, но это — максимум, что можно выжать из библиотечного кода, не трогая компилятор. Т.к. только компилятор владеет списком автоматических объектов. Отсюда вывод — исключения, реализованные на уровне библиотеки, на порядок увеличивают объем работы программиста, не привнося вместе с тем никаких преимуществ.
Вот такой вот практический опыт работы с исключениями, реализованными на уровне библиотеки.
Сергей Губанов wrote:
> M>Простите. вы вообще имели когда-нибудь дело с платежными системами? > Я вот да. > Отродясь не имел. Но мы тут спорим о платежах или об exceptions? Если > не нравится пример с платежами, давайте рассмотрим какой-нибудь другой > пример, я его берусь написать без использования exceptions.
На самом деле, пример с платежами — очень хороший. Он заодно помогает
проиллюстрировать понятие транзакций и их симбиоз с исключениями.
СГ>2) Следующий ненадуманный пример. СГ>Есть виндос-форма, в поля которой пользователь вводит числа. СГ>Если пользователь ошибся и ввел неправильную строку, то надо ему об этом сообщить. СГ>Но стандартная функция СГ>
СГ>int System.Int32.Parse (string str)
СГ>
СГ>имеет ошибку в дизайне — она кидает исключенние, приходится либо писать свой парсер чисел (что влом) СГ>либо писать так :
Как насчет использования bool System.Int32.TryParse(string s, out int result) в тех случаях, когда исключение не желательно?
Здравствуйте, Сергей Губанов, Вы писали:
S>>Вот в приведенном примере — что делать, если журнал не смог записать успешную запись в файл?
СГ>Что делать кому? Объекту "ЛокальныйСчёт" по этому поводу точно делать ни чего не надо, он свою задачу выполнил на 100%, а уж как там внутри объекта "Журнал" чего-то стряслось — не его дело.
Что делать программе? Нам не удалось записать операцию в журнал. Это может иметь катастрофические последствия для бизнеса — грубо говоря, мы вообще не знаем про эту операцию. Сегодня у нас никакой катастрофы нет, а завтра логика потребует изменения. Что, перепишем всю программу сверху донизу?
СГ>Ничего подобного. СГ>1) Не все процедуры обязаны возвращать булевый признак успешности. Например, операция рапорта о чём-то произошедшем, ничего не должна возвращать — это никому не интересно кроме неё самой.
Сегодня не интересно. Процедура, вообще говоря, не должна принимать такие решения. Для улучшения повторного использования она должна дать возможность вызывающему коду гибко контролировать весь процесс. СГ>2) Не все процедуры обязаны возвращать информацию о причине неудачи. Ну не получилось, что-то сделать — иногда интересно узнать почему, а иногда — причина по барабану.
То же самое. Заранее знать, по барабану или нет, нельзя. Это принципиальный момент. Без понимания этого вам не удастся двигаться дальше. Архитектура, которая требует массовых изменений по всему коду для внесения небольших изменений — обречена. СГ>3) С чего Вы взяли, что информация о неудаче есть обязательно текст? Можно возвращать текст, можно число, можно указатель на абстрактный объект, и т.д.
Я этого ни с чего не взял. В данном примере потребовалось всегда возвращать текст — поскольку реакция на любую проблему должна отправлять текстовое сообщение. Я упомянул о том, что в более сложной ситуации придется придумывать более сложные структуры. СГ>Вот, пример полиморфной информации об ошибке:
Совершенно верно. Это объектно-ориентированный вариант "кода результата". Поздравляю, вы успешно его изобрели. Он намного лучше, чем HRESULT из COM.
Он позволяет передать практически любую релевантную информацию.
Это почти что механизм исключений. Можно даже написать аналог набора блоков Catch — в обероне же вроде был свитч по фактическому типу, не так ли? Вместо корявого набора if, который вы привели.
Осталась только одна проблема — транспорт. Т.е. для обеспечения повторного использования, абсолютно весь код обязан возвращать именно такой результат. В шарпе это было бы эквивалентно принуждению использовать out параметры для настоящих результатов, а возвращать всегда Exception (который и есть полиморфный тип информации об ошибке). Но тогда код типичного метода был бы устроен так:
public Exception ToString(out string result)
{
int a1;
Exception r = SomeMethod(out a1);
if (r != null)
return r;
string s1;
r = SomeOtherMethod(out s);
if (r != null)
return r;
result = a1.ToString()+s;
return null;
}
То есть каждый-каждый вызов приходится оборудовать проверкой результата и передачей выше по стеку.
Теперь нельзя каскадировать вызовы и писать выражения:
int a = GetX()*GetY(); // ай-яй-яй!
вместо этого мы будем писать так:
int x, y, a;
Exception r;
r = GetX(out x);
if (r != null)
return r;
r = GetY(out y);
if (r != null)
return r;
r = Multiply(x, y, out a);
if (r != null)
return r;
Здорово, правда? Только не надо рассказывать про то, что мы можем себе позволить игнорировать ошибки в GetX, GetY, или что при умножении не может ее возникнуть — еще как может! IntegerOverflow, пожалте-ка. А приведенный фрагмент пишется в библиотечном коде, который не имеет никакого представления о том, как правильно реагировать на проблемы. Ну, может на некоторые конкретные он и знает, как реагировать (тогда вместо сравнения r c null будет какой-то более сложный код), но все остальные (а в общем случае он не знает, какие они будут), обязан отдать вызывающему коду. В данном случае мы имеем десятикратное раздувание кода.
После взгляда на это безобразие становится понятно, что наличие механизма доставки информации об ошибке есть великое благо. К тому же, как и любой механизм, он не требует использования. Если хочется — можно писать ровно в приведенном стиле и возвращать коды результата.
Вопрос встраивания подобной логики в библиотеку мне кажется несколько надуманным. Я не могу себе представить библиотеку, способную предоставить все необходимые свойства без модификации языка. В лучшем случае это будут корявые костыли, которые лишь слегка замажут все эти наслоения.
1.1.4 stable rev. 510
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Joker6413, Вы писали:
J>Это будет круче множественного наследования! Имхо повысит энтропию системы не в разы, а на порядки!
Ага, если обработчик:
1. Меняет состояние объектов, участвовавших в выполнении сбойнувшего фрагмента
2. Подшаманивает стек, чтобы все выглядело так, как будто все и было нормально с самого начала
3. Подправляет код методов в стеке, чтобы обеспечить более корректное исполнение.
4. Передает управление временно сгенерированному thunk, который модифицирует сам первоначальный обработчик, заканчивает очистку и продолжает выполнение в нормальном режиме.
1.1.4 stable rev. 510
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Zero-Overhead Exception Handling Using Metaprogramming
Markus Hof, Hanspeter Mössenböck, Peter Pirkelbauer
We present a novel approach to exception handling which is based on metaprogramming.
Our mechanism does not require language support, imposes no run time overhead to errorfree
programs, and is easy to implement. Exception handlers are implemented as ordinary
procedures. When an exception occurs, the corresponding handler is searched dynamically
using the type of the exception as a search criterion. Our implementation was done in the
Oberon System but it could be ported to most other systems that support metaprogramming.