совет по вложенным бутербродам с кодом попытки / наконец-то на Java

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


Использование идиомы «Code Sandwich» - обычное дело для управления ресурсами. Привыкнув к идиоме RAII в C ++, я переключился на Java и обнаружил, что мое безопасное для исключений управление ресурсами приводит к глубоко вложенному коду, в котором мне очень трудно справиться с обычным потоком управления.

Очевидно (доступ к данным Java: это хороший стиль кода доступа к данным Java, или это слишком много попыток, наконец?, Уродливый блок попыток в Java io и многое другое) Я не одинок.

Я пробовал разные решения, чтобы справиться с этим:

  1. явно поддерживать состояние программы: resource1aquired, _2 _... и условно очищать: _3 _... Но я избегаю дублирования состояния программы в явных переменных - среда выполнения знает состояние, и я не хочу заботиться о нем.

  2. обернуть каждый вложенный блок в функции - это еще больше усложнит отслеживание потока управления и приведет к действительно неудобным именам функций: runResource1Acquired( r1 ), runFileOpened( r1, file ), ...

И, наконец, я пришел к идиоме (концептуально), подкрепленной некоторой исследовательской статьей по коду. бутерброды:


Вместо этого:

// (pseudocode)
try {
   connection = DBusConnection.SessionBus(); // may throw, needs cleanup
   try {
        exported = false;
        connection.export("/MyObject", myObject ); // may throw, needs cleanup
        exported = true;
            //... more try{}finally{} nested blocks
    } finally {
        if( exported ) connection.unExport( "/MyObject" );
    }   
} finally {
   if (connection != null ) connection.disconnect();
}

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

class Compensation { 
    public void compensate(){};
}
compensations = new Stack<Compensation>();

И вложенный код становится линейным:

try {
    connection = DBusConnection.SessionBus(); // may throw, needs cleanup
    compensations.push( new Compensation(){ public void compensate() {
        connection.disconnect();
    });

    connection.export("/MyObject", myObject ); // may throw, needs cleanup
    compensations.push( new Compensation(){ public void compensate() {
        connection.unExport( "/MyObject" );
    });   

    // unfolded try{}finally{} code

} finally {
    while( !compensations.empty() )
        compensations.pop().compensate();
}

Я был в восторге: независимо от того, сколько исключительных путей, поток управления остается линейным, а код очистки визуально находится рядом с исходным кодом. Кроме того, ему не нужен искусственно ограниченный метод closeQuietly, что делает его более гибким (то есть не только Closeable объектов, но также Disconnectable, Rollbackable и других).

Но...

Я нигде не нашел упоминания об этой технике. Итак, вот вопрос:


Этот метод действителен? Какие ошибки вы в нем видите?

Большое спасибо.


person xtofl    schedule 31.08.2011    source источник
comment
Похоже, вы не обрабатываете случай, когда исключение может быть выброшено из метода компенсировать (). Это предотвратит запуск последующих компенсаций.   -  person KevinS    schedule 31.08.2011
comment
@Kevin: действительно, здесь слишком много идиомы C ++: деструкторы не должны выбрасывать, и здесь я придерживаюсь этой идиомы. Обработка исключений зависит от compensate реализаций.   -  person xtofl    schedule 31.08.2011
comment
@xtofl - это вообще не похоже на то, что вы используете try-finally - вы могли бы просто принять эту идиому с самим кодом и не беспокоиться о блоке finally. Кроме того, многие Compensators естественно генерируют исключения (например, SQLException для закрытия ресурса БД); каждая реализация должна перехватывать и обрабатывать все исключения по отдельности, а не ваш finally блок, обрабатывающий их последовательно в одном месте. Для них это не обязательно (исключение во время выполнения нарушит их спецификацию, и это может произойти в любое время), поэтому вы просто поощряете копипасту.   -  person Andrzej Doyle    schedule 31.08.2011
comment
вот почему мне нравится scope(exit) оператор D   -  person ratchet freak    schedule 31.08.2011
comment
@ratchet freak: который также упоминался в упомянутом исследовании.   -  person xtofl    schedule 31.08.2011


Ответы (6)


Это мило.

Никаких серьезных жалоб, мелочи не у меня в голове:

  • небольшая нагрузка на производительность
  • вам нужно сделать некоторые вещи final, чтобы Компенсация их увидела. Возможно, это предотвращает некоторые варианты использования
  • вы должны перехватывать исключения во время компенсации и продолжать выполнение компенсаций, несмотря ни на что.
  • (немного надумано) вы можете случайно очистить очередь компенсации во время выполнения из-за ошибки программирования. Ошибки программирования OTOH могут произойти в любом случае. А с вашей очередью компенсации вы получаете «условные окончательные блоки».
  • не заходите слишком далеко. В рамках одного метода это кажется нормальным (но тогда вам, вероятно, все равно не понадобится слишком много блоков try / finally), но не передавайте очереди компенсации вверх и вниз по стеку вызовов.
  • это откладывает компенсацию до "окончательного внешнего", что может быть проблемой для вещей, которые необходимо очистить на ранней стадии
  • это имеет смысл только тогда, когда вам нужен блок try только для блока finally. Если у вас все равно есть блок catch, вы можете просто добавить здесь finally.
person Thilo    schedule 31.08.2011
comment
Спасибо. Что вы подразумеваете под условными блоками finally? - person xtofl; 31.08.2011
comment
Ну, можно сказать if(something) componensation.push(extraCompensation). - person Thilo; 31.08.2011

Мне нравится этот подход, но я вижу несколько ограничений.

Во-первых, в оригинале бросок в ранний блок finally не влияет на последующие блоки. В вашей демонстрации бросок неэкспортного действия остановит компенсацию разъединения.

Во-вторых, язык усложнен уродством анонимных классов Java, включая необходимость введения кучи «конечных» переменных, чтобы их могли видеть компенсаторы. Это не твоя вина, но мне интересно, не хуже ли лекарство, чем болезнь.

Но в целом подход мне нравится, и он довольно милый.

person Burleigh Bear    schedule 31.08.2011

На мой взгляд, вам нужны транзакции. Ваши компенсации - это транзакции, реализованные несколько иначе. Я предполагаю, что вы не работаете с ресурсами JPA или любыми другими ресурсами, которые поддерживают транзакции и откаты, потому что тогда было бы довольно просто использовать JTA (API транзакций Java). Кроме того, я предполагаю, что ваши ресурсы разрабатываются не вами, потому что, опять же, вы можете просто заставить их реализовать правильные интерфейсы из JTA и использовать с ними транзакции.

Итак, мне нравится ваш подход, но я бы скрыл от клиента сложность извлечения и компенсации. Кроме того, вы можете прозрачно передавать транзакции.

Таким образом (осторожно, впереди уродливый код):

public class Transaction {
   private Stack<Compensation> compensations = new Stack<Compensation>();

   public Transaction addCompensation(Compensation compensation) {
      this.compensations.add(compensation);
   }

   public void rollback() {
      while(!compensations.empty())
         compensations.pop().compensate();
   }
}
person LeChe    schedule 31.08.2011
comment
... уродливый код? Я могу сделать и попроще :) Это, по крайней мере, хороший способ обернуть эту технику в многоразовый класс. - person xtofl; 31.08.2011
comment
единственное, что мне здесь не хватает, это упаковка try-catch для compensations.pop().compensate(); для обработки исключений, которые могут там возникнуть, как указано в нескольких предыдущих ответах - person gnat; 31.08.2011
comment
Ну, я также скучаю по геттерам и сеттерам, правильному конструктору, проверкам нулевых параметров и т. Д. - довольно уродливо на мой вкус. :) Но да, комар, код для отката должен быть более сложным, вроде проверки на недействительные или нефункциональные компенсации и тому подобное. Другое дело, что вы хотите делать, если компенсация не срабатывает: продолжать или нет? Зависимы ли компенсации друг от друга? Вызвать исключение (мое любимое, поскольку в таком случае состояние вашего приложения не определено)? Ваши требования, вероятно, говорят вам, что делать, так что выбейте себя из колеи! :) - person LeChe; 31.08.2011
comment
Чтобы быть ясным, мое замечание об упаковке было чисто для того, чтобы сохранить то же поведение, что и во вложенных попытках. Я не хотел делать что-то лучше исходного кода, только чтобы читать лучше, чем он. Кстати, мне больше всего нравится ваш подход к удобочитаемости из того, что я видел в ответах до сих пор - person gnat; 01.09.2011

Деструктор, подобный устройству для Java, который будет вызываться в конце лексической области видимости, является интересной темой; его лучше всего решать на языковом уровне, однако языковые цари не находят его очень привлекательным.

Обсуждалось определение действия по разрушению сразу после строительства (ничего нового под солнцем). Примером может служить http://projectlombok.org/features/Cleanup.html.

Другой пример из частного обсуждения:

{
   FileReader reader = new FileReader(source);
   finally: reader.close(); // any statement

   reader.read();
}

Это работает путем преобразования

{
   A
   finally:
      F
   B
}

в

{
   A
   try
   {
      B
   }
   finally
   {
      F
   }
}

Если в Java 8 добавлено закрытие, мы могли бы кратко реализовать эту функцию в закрытии:

auto_scope
#{
    A;
    on_exit #{ F; }
    B;
}

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

File.open(fileName) #{

    read...

}; // auto close
person irreputable    schedule 31.08.2011
comment
ломбок ... похоже какой-то std :: boost для java :) Спасибо за подсказку! - person xtofl; 01.09.2011

Вы знаете, что вам действительно нужна эта сложность. Что происходит, когда вы пытаетесь неэкспортировать то, что не экспортируется?

// one try finally to rule them all.
try {
   connection = DBusConnection.SessionBus(); // may throw, needs cleanup
   connection.export("/MyObject", myObject ); // may throw, needs cleanup
   // more things which could throw an exception

} finally {
   // unwind more things which could have thrown an exception

   try { connection.unExport( "/MyObject" ); } catch(Exception ignored) { }
   if (connection != null ) connection.disconnect();
}

Используя вспомогательные методы, вы могли бы сделать

unExport(connection, "/MyObject");
disconnect(connection);

Я бы подумал, что отключение означает, что вам не нужно отменять экспорт ресурсов, которые использует соединение.

person Peter Lawrey    schedule 31.08.2011
comment
Затем он мог запустить unExport, даже если экспорт не был запущен. Не в приведенном выше простом примере, но идею вы поняли. (Может обрабатываться множеством логических значений). - person Thilo; 31.08.2011
comment
В самом деле: это решение полезно только там, где дополнительная сложность действительно увеличивает читаемость кода. Возможно, этот пример не достаточно исключительный :). - person xtofl; 31.08.2011
comment
@Thilo, тогда он мог запустить unExport, хоть экспорт не запускался. ты знаешь, что это вообще проблема? - person Peter Lawrey; 31.08.2011
comment
@xtofl, Исключения должны быть исключительными. Если они немного уродливы ради упрощения основного кода, это цена, которую стоит заплатить. - person Peter Lawrey; 31.08.2011
comment
Я думаю, что отказ от unExport имеет больше смысла. Это также упрощает реализацию export и unExport. - person xtofl; 31.08.2011
comment
@xtofl, вы имеете в виду не вызывать его вообще (как я подозреваю, вам не нужно) или использовать флаг, который может упростить библиотеку, но усложняет вызывающего. (ИМХО свою сложность библиотека должна брать у вызывающей) - person Peter Lawrey; 31.08.2011
comment
Я имею в виду «не вызывать unExport, если вы успешно не вызвали экспорт». Ваше скромное мнение явно противоречит моему :) - person xtofl; 31.08.2011
comment
@xtofl, все еще непонятно, зачем вам это нужно. Если вы map.put(key, value), вы можете вызвать map.remove(key); независимо от того, добавлен ключ или нет. Если у вас есть файл, вы можете вызвать delete() независимо от того, существует он или нет. - person Peter Lawrey; 31.08.2011
comment
Да, ты можешь. И если вы это сделаете, вы сделаете свою библиотеку довольно сложной. Я говорю, что нет смысла закрывать дверь, которую вы не открывали, поэтому вам не нужно это реализовывать. В результате получается дверь, которую можно закрыть, только если она открыта. «Надежная» библиотека также может предоставить двери метод close_if_open, но я не ожидаю, что моя входная дверь реализует это удобство. На самом деле это другое обсуждение, но добавление этого удобства нарушает принцип единой ответственности и часто приводит к слишком сложным классам. OTOH, я не строитель библиотек, так кто я ... - person xtofl; 31.08.2011
comment
Но с другой стороны, я думаю, что добавление «защиты от дурака» в библиотеку - это последнее, что нужно сделать, и оно приходит после всех усилий, направленных на то, чтобы сделать ее правильной. В моем конкретном случае (libdbus) мне бы это понравилось. - person xtofl; 31.08.2011
comment
Я думаю, вы путаете действие закрытия с желаемым результатом закрытия. Вы действительно хотите, чтобы действие завершилось ошибкой, потому что вы закрываете его, когда он уже закрыт, или это тот случай, когда вы не хотите подвергаться воздействию состояния объекта, и вы просто хотите, чтобы оно было закрыто. Ключевым принципом объектно-ориентированного программирования является инкапсуляция, при которой вызывающий объект не должен поддерживать состояние, которое вызываемый должен поддерживать в любом случае, и оставлять вам задачу убедиться, что они одинаковы. Возможно, libdbus не был разработан с учетом принципов ООП. ;) - person Peter Lawrey; 31.08.2011

Вам следует изучить try-with-resources, впервые представленный в Java 7. Это должно уменьшить необходимую вложенность.

person soc    schedule 31.08.2011
comment
Я сделал. Они ограничивают ресурсы Closeable разработчикам, что, по-моему, слишком ограничительно. (И я еще не использую Java 7, но это небольшая проблема :) - person xtofl; 31.08.2011
comment
Вы, вероятно, могли бы зарегистрировать ошибку в трекере ошибок DBus. Имхо нет причин, по которым этот класс не реализует Closeable. - person soc; 31.08.2011