2011-08-31 9 views
21

Vorrei qualche consiglio su una tecnica su cui mi sono imbattuto. Può essere facilmente compreso guardando i frammenti di codice, ma lo documenterò un po 'di più nei paragrafi seguenti.consigli su nested Java try/finally code sandwiches


L'utilizzo dell'idioma "Code Sandwich" è comune per gestire la gestione delle risorse. Utilizzato per l'idioma RAII di C++, sono passato a Java e ho trovato la mia gestione delle risorse eccezionalmente sicura con conseguente codice profondamente annidato, in cui ho davvero un momento difficile ottenere grip sul flusso di controllo regolare .

Apparentemente (java data access: is this good style of java data access code, or is it too much try finally?, Java io ugly try-finally block e molti altri) Non sono solo.

ho provato diverse soluzioni per far fronte a questo:

  1. mantenere lo stato del programma in modo esplicito: resource1aquired, fileopened ..., e la pulizia condizionatamente: if (resource1acquired) resource1.cleanup() ... Ma mi evitano la duplicazione dello stato del programma in esplicita variabili - il runtime conosce lo stato e non voglio preoccuparmene.

  2. avvolgere ogni blocco nidificato nelle funzioni - si traduce in ancora più difficile da seguire il flusso di controllo, e rende per i nomi delle funzioni davvero imbarazzante: runResource1Acquired(r1), runFileOpened(r1, file), ...

E finalmente sono arrivato a un idioma anche (concettualmente) sostenuta da alcuni research paper on code sandwiches:


Invece di questo:

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

Utilizzando una costruzione di supporto, è possibile arrivare a un costrutto più lineare, in cui il codice di compensazione si trova proprio accanto all'originatore.

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

E il codice nidificato diventa lineare:

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(); 
} 

mi ha fatto molto piacere: non importa quante percorsi eccezionali, il flusso di controllo rimane lineare, e il codice di pulitura è visivamente accanto al codice di origine. Inoltre, non è necessario un metodo closeQuietly limitato artificialmente, che lo rende più flessibile (ad esempio non solo gli oggetti Closeable, ma anche Disconnectable, Rollbackable e qualsiasi altro).

Ma ...

ho trovato alcuna menzione di questa tecnica altrove. Quindi, ecco la domanda:


Questa tecnica è valida? Quali bug vedi?

Grazie mille.

+1

Non sembrano essere si occupano del caso in cui un'eccezione potrebbe essere generata da un metodo compensate(). Ciò impedirebbe il susseguirsi di compensazioni successive. –

+0

@Kevin: in effetti - troppa idioma C++ qui: i distruttori non devono gettare, e io mi attengo a quell'idioma qui. Dipende dalle implementazioni di "compensare" per gestire le eccezioni. – xtofl

+1

@xtofl - che sembra in disaccordo con te usando 'try-finally' - puoi semplicemente adottare quell'idioma con il codice stesso, e non preoccuparti di un blocco' finally'. Inoltre, molti 'Compensator' genererebbero naturalmente delle eccezioni (ad esempio' SQLException' per chiudere una risorsa DB); ogni implementazione ** ha ** di catturare e ingoiare tutte le eccezioni singolarmente, piuttosto che il blocco 'finally' che lo gestisce in modo coerente in un unico posto. Non è facoltativo per loro farlo (un'eccezione di runtime potrebbe violare le loro specifiche, e ciò potrebbe accadere in qualsiasi momento), quindi sarebbe solo incoraggiante copia-incolla. –

risposta

2

È bello.

Non possiamo lamentarci principali, cose minori al largo della parte superiore della mia testa:

  • un po 'di prestazioni onere
  • è necessario fare alcune cose final per la compensazione di vederli. Forse questo impedisce alcuni casi d'uso
  • si dovrebbero catturare eccezioni durante compensare e continuare a eseguire compensazioni non importa quale
  • (bit inverosimile) si potrebbe accidentalmente svuotare la coda di compensazione in fase di esecuzione a causa di un errore di programmazione. Gli errori di programmazione OTOH possono comunque accadere. E con la tua coda di compensazione, ottieni "blocchi condizionali alla fine".
  • non spingere troppo lontano. All'interno di un singolo metodo sembra a posto (ma probabilmente non sono necessari troppi tentativi/finalmente blocchi), ma non passare le code di compensazione su e giù nello stack delle chiamate.
  • ritarda il risarcimento per il "più esterno alla fine" che potrebbe essere un problema per cose che devono essere pulite presto
  • ha senso solo quando è necessario il blocco try solo per il blocco finally. Se hai comunque un blocco di cattura, potresti semplicemente aggiungere l'ultimo, proprio lì.
+0

Grazie. Cosa intendi per "blocchi condizionali alla fine"? – xtofl

+0

Beh, si potrebbe dire 'if (qualcosa) componentensation.push (extraCompensation)'. – Thilo

0

Sai che hai davvero bisogno di questa complessità. Cosa succede quando provi a esportare qualcosa che non viene esportato?

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

Utilizzando metodi di supporto che si potrebbe fare

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

avrei pensato di scollegare significherebbe che non è necessario per unexport le risorse del collegamento sta usando.

+1

Quindi potrebbe eseguire unExport, anche se l'esportazione non è stata eseguita. Non nel semplice esempio sopra, ma hai un'idea. (Potrebbe essere gestito da molti booleani). – Thilo

+0

Infatti: questa soluzione è utile solo dove la complessità aggiunta aggiunge davvero la leggibilità del codice. Forse questo esempio non è abbastanza eccezionale :). – xtofl

+0

@Thilo, "Quindi potrebbe eseguire unExport, anche se l'esportazione non è stata eseguita." sai che questo è anche un problema? –

3

Mi piace l'approccio, ma vedo un paio di limitazioni.

Il primo è che nell'originale, un lancio in un blocco iniziale non influisce sui blocchi successivi. Nella tua dimostrazione, un lancio nell'azione inexport interromperà la compensazione della disconnessione.

Il secondo è che il linguaggio è complicato dalla bruttezza delle classi anonime di Java, compresa la necessità di introdurre un mucchio di variabili "finali" in modo che possano essere viste dai compensatori. Non è colpa tua, ma mi chiedo se la cura sia peggiore della malattia.

Ma nel complesso, mi piace l'approccio, ed è piuttosto carino.

1

È necessario esaminare le risorse try-with, introdotte per la prima volta in Java 7. Ciò dovrebbe ridurre il necessario nidificazione.

+1

L'ho fatto. Limitano le risorse agli implementatori 'Closeable', che è troppo restrittivo. (E non sto ancora usando Java 7, ma questo è un problema minore :) – xtofl

+0

Probabilmente potresti trovare un bug nel bug tracker di DBus. Non c'è motivo per cui questa classe non implementa Closeable. – soc

3

Il modo in cui lo vedo, quello che vuoi sono le transazioni. Le compensazioni sono transazioni, implementate in modo leggermente diverso. Suppongo che tu non stia lavorando con le risorse JPA o con qualsiasi altra risorsa che supporti transazioni e rollback, perché allora sarebbe piuttosto semplice usare semplicemente JTA (Java Transaction API). Inoltre, presumo che le tue risorse non siano state sviluppate da te, perché ancora una volta potresti semplicemente implementare le interfacce corrette da JTA e utilizzare le transazioni con esse.

Quindi, mi piace il tuo approccio, ma quello che vorrei fare è nascondere la complessità di scoppiare e compensare dal client. Inoltre, è possibile passare le transazioni in modo trasparente quindi.

Così (attenzione, brutto codice di percorso):

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(); 
    } 
} 
+0

... codice brutto? Posso fare più brutto :) Questo è almeno un buon modo per avvolgere la tecnica in una classe riutilizzabile. – xtofl

+1

l'unica cosa che mi manca qui è il wrapping try-catch per 'compensations.pop(). Compensate();' per gestire le eccezioni che possono verificarsi lì come indicato in un paio di risposte precedenti – gnat

+1

Beh, mi mancano anche getter e setter, un costruttore corretto, controlli dei parametri nulli, ecc. - piuttosto brutti per i miei gusti. :) Ma sì, gnat, il codice per eseguire il rollback dovrebbe essere più elaborato, come il controllo di compensi non validi o non funzionali e simili. Cosa vuoi fare se una compensazione fallisce è una storia completamente diversa: continui o no? Le compensazioni dipendono l'una dall'altra? Lanciare un'eccezione (il mio preferito dal momento che lo stato dell'applicazione non è definito in questo caso)? I tuoi requisiti probabilmente ti dicono cosa fare, quindi buttati fuori! :) – LeChe

3

un distruttore come il dispositivo per Java, che verrà richiamato alla fine del campo di applicazione lessicale, è un argomento interessante; è meglio affrontato a livello linguistico, tuttavia gli zar di lingua non lo trovano molto avvincente.

Specificare l'azione di distruzione immediatamente dopo l'azione di costruzione è stata discussa (niente di nuovo sotto il sole). Un esempio è http://projectlombok.org/features/Cleanup.html

Un altro esempio, da una discussione privata:

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

    reader.read(); 
} 

Questo funziona trasformando

{ 
    A 
    finally: 
     F 
    B 
} 

in

{ 
    A 
    try 
    { 
     B 
    } 
    finally 
    { 
     F 
    } 
} 

Se Java 8 aggiunge la chiusura, potremmo implementare questa funzione in modo succinto in chiusura:

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

Tuttavia, con chiusura, la maggior parte libreria di risorse farebbe il proprio fornitore di dispositivo automatico di pulizia, i clienti non ha realmente bisogno di gestire da soli

File.open(fileName) #{ 

    read... 

}; // auto close 
+0

lombok ... sembra un po 'di std :: boost per java :) Grazie per il suggerimento! – xtofl