совет по вложенной Java попробуйте / наконец код бутерброды - PullRequest
21 голосов
/ 31 августа 2011

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


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

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

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

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

  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 и любых других).

Но ...

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


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

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

Ответы [ 6 ]

3 голосов
/ 31 августа 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
3 голосов
/ 31 августа 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();
   }
}
3 голосов
/ 31 августа 2011

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

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

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

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

2 голосов
/ 31 августа 2011

Это приятно.

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

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

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

0 голосов
/ 31 августа 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);

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

...