hacer - switch java ejemplo
Consejos sobre Java anidado. (6)
Me gustaría un consejo sobre una técnica con la que me encontré. Se puede entender fácilmente mirando los fragmentos de código, pero lo documento un poco más en los párrafos siguientes.
Usar el lenguaje "Code Sandwich" es un lugar común para tratar con la administración de recursos. Acostumbrado al lenguaje RAII de C ++, me cambié a Java y encontré mi gestión de recursos segura en casos de excepción, lo que resultó en un código profundamente anidado, en el que me resulta muy difícil controlar el flujo de control regular .
Aparentemente ( acceso a datos en java: ¿es este buen estilo de código de acceso a datos en java, o es demasiado intentarlo finalmente?, Bloque uy feo de intentarlo de Java y mucho más) No estoy solo.
He intentado diferentes soluciones para hacer frente a esto:
mantenga el estado del programa explícitamente: se
fileopened
resource1aquired
, sefileopened
..., y se limpió de manera condicional: sefileopened
resource1aquired
(sefileopened
if (resource1acquired) resource1.cleanup()
. No quiero cuidarlo.envuelve cada bloque anidado en funciones: resulta aún más difícil seguir el flujo de control y hace que los nombres de las funciones sean realmente difíciles:
runResource1Acquired( r1 )
,runFileOpened( r1, file )
, ...
Y finalmente llegué a un idioma también (conceptualmente) respaldado por un trabajo de investigación sobre sándwiches de código :
En lugar de esto:
// (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();
}
Usando una construcción auxiliar, puede llegar a una construcción más lineal, en la que el código de compensación está justo al lado del originador.
class Compensation {
public void compensate(){};
}
compensations = new Stack<Compensation>();
Y el código anidado se vuelve lineal:
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();
}
Estaba encantado: no importa cuántas rutas excepcionales, el flujo de control se mantenga lineal y el código de limpieza esté visualmente al lado del código de origen. Además de eso, no necesita un método de closeQuietly
restringido artificialmente, lo que lo hace más flexible (es decir, no solo los objetos que se pueden Closeable
, sino también los objetos Disconnectable
, Rollbackable
y otros).
Pero...
No encontré ninguna mención de esta técnica en ninguna otra parte. Así que aquí está la pregunta:
¿Es esta técnica válida? ¿Qué errores ves en ella?
Muchas gracias.
¿Sabes que realmente necesitas esta complejidad? ¿Qué sucede cuando intentas desexportar algo que no se exporta?
// 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();
}
Usando métodos de ayuda que podrías hacer
unExport(connection, "/MyObject");
disconnect(connection);
Habría pensado que desconectar significaría que no necesita desexportar los recursos que utiliza la conexión.
Debe buscar en try-with-resources, introducido por primera vez en Java 7. Esto debería reducir el anidamiento necesario.
Es agradable.
No hay quejas importantes, cosas menores fuera de mi cabeza:
- una pequeña carga de rendimiento
- necesita hacer algunas cosas
final
para que la Compensación las vea. Tal vez esto evita algunos casos de uso. - debe detectar excepciones durante la compensación y continuar con las Compensaciones sin importar qué
- (poco probable) podría vaciar accidentalmente la cola de compensación en tiempo de ejecución debido a un error de programación. Los errores de programación de OTOH pueden ocurrir de todos modos. Y con la cola de Compensación, obtienes "bloques condicionales finalmente".
- no empujes esto demasiado lejos Dentro de un solo método parece estar bien (pero, de todos modos, es probable que no necesites demasiados bloqueos de prueba / finalmente), pero no pases las colas de Compensación arriba y abajo de la pila de llamadas.
- demora la compensación al "extremo externo", lo que podría ser un problema para las cosas que deben limpiarse temprano
- solo tiene sentido cuando necesita el bloque try solo para el bloque finally. Si tienes un bloque catch de todos modos, puedes agregar el finalmente justo allí.
La forma en que lo veo, lo que quieres son transacciones. Sus compensaciones son transacciones, implementadas de forma un poco diferente. Supongo que no está trabajando con los recursos de JPA ni con ningún otro recurso que admita transacciones y retrocesos, porque entonces sería bastante fácil simplemente usar JTA (API de transacciones de Java). Además, asumo que usted no desarrolla sus recursos, porque nuevamente, podría simplemente hacer que implementen las interfaces correctas de JTA y usar las transacciones con ellos.
Por lo tanto, me gusta su enfoque, pero lo que haría sería ocultar la complejidad de hacer estallar y compensar al cliente. Además, puede pasar las transacciones de forma transparente a continuación.
Así (cuidado, el código feo por delante):
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();
}
}
Me gusta el enfoque, pero veo un par de limitaciones.
La primera es que en el original, un lanzamiento en un bloque final finalmente no afecta a los bloques posteriores. En su demostración, un lanzamiento en la acción de no exportación detendrá la activación de la desconexión.
La segunda es que el lenguaje se complica por la fealdad de las clases anónimas de Java, incluida la necesidad de introducir un montón de variables "finales" para que los compensadores las puedan ver. Esto no es tu culpa, pero me pregunto si la cura es peor que la enfermedad.
Pero en general, me gusta el enfoque, y es bastante lindo.
Un dispositivo similar a un destructor para Java, que se invocará al final del ámbito léxico, es un tema interesante; se aborda mejor a nivel del idioma, sin embargo, a los zares del idioma no les resulta muy convincente.
Especificar la acción de destrucción inmediatamente después de que se haya discutido la acción de construcción (nada nuevo bajo el sol). Un ejemplo es http://projectlombok.org/features/Cleanup.html
Otro ejemplo, de una discusión privada:
{
FileReader reader = new FileReader(source);
finally: reader.close(); // any statement
reader.read();
}
Esto funciona mediante la transformación.
{
A
finally:
F
B
}
dentro
{
A
try
{
B
}
finally
{
F
}
}
Si Java 8 agrega el cierre, podríamos implementar esta característica de forma sucinta en el cierre:
auto_scope
#{
A;
on_exit #{ F; }
B;
}
Sin embargo, con el cierre, la mayoría de las bibliotecas de recursos proporcionarían su propio dispositivo de limpieza automática, los clientes realmente no necesitan manejarlo por sí mismos
File.open(fileName) #{
read...
}; // auto close