Подтвердить что ты не робот

Советы по вложенным Java try/finally бутербродам кода

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


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

По-видимому (доступ к данным java: это хороший стиль кода доступа к данным Java, или он слишком сильно пытается наконец?, Java io уродливый блок try-finally и многие другие) Я не одинок.

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

  • явно поддерживать состояние программы: resource1aquired, fileopened... и очищать условно: if (resource1acquired) resource1.cleanup()... Но я избегаю дублирования состояния программы в явных переменных - среда выполнения знает состояние, и я не хочу заботиться об этом.

  • обертывание каждого вложенного блока в функции - приводит к еще большему отслеживанию потока управления и делает действительно неудобные имена функций: 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 и любых других).

Но...

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


Является ли этот метод действительным? Какие ошибки вы видите в нем?

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

4b9b3361

Ответ 1

Это приятно.

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

  • небольшое снижение производительности
  • вам нужно сделать некоторые вещи final для компенсации, чтобы увидеть их. Возможно, это предотвращает некоторые случаи использования
  • вы должны перехватывать исключения во время компенсации и продолжать выполнение Компенсаций независимо от того, что
  • (бит надуманный) вы можете случайно выгрузить очередь Compensation во время выполнения из-за ошибки программирования. Ошибки программирования OTOH могут случиться в любом случае. И с вашей очередью "Компенсация" вы получаете "условные блоки finally".
  • Не нажимайте слишком далеко. В рамках одного метода это выглядит нормально (но тогда вам, вероятно, не нужно слишком много блоков try/finally), но не пропускайте очереди компенсации вверх и вниз по стеку вызовов.
  • он задерживает компенсацию "крайнему окончательному", что может быть проблемой для вещей, которые нужно очистить раньше.
  • это имеет смысл только тогда, когда вам нужен блок try только для блока finally. Если у вас есть блок catch в любом случае, вы можете просто добавить окончательно там.

Ответ 2

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

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

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

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

Ответ 3

То, как я это вижу, то, что вы хотите, это транзакции. Вы компенсируете транзакции, реализованные несколько иначе. Я предполагаю, что вы не работаете с ресурсами 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();
   }
}

Ответ 4

Деструкторное устройство для 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

Ответ 5

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

Ответ 6

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

// 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);

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