Советы по вложенным 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
и любых других).
Но...
Я не нашел упоминания об этой технике в другом месте. Итак, вот вопрос:
Является ли этот метод действительным? Какие ошибки вы видите в нем?
Большое спасибо.
Ответы
Ответ 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);
Я бы подумал, что разъединение означает, что вам не нужно отключать ресурсы, которые использует соединение.