Forza gli altri sviluppatori a chiamare il metodo dopo aver completato il loro lavoro


34

In una libreria in Java 7, ho una classe che fornisce servizi ad altre classi. Dopo aver creato un'istanza di questa classe di servizio, un suo metodo può essere chiamato più volte (chiamiamolo il doWork()metodo). Quindi non so quando il lavoro della classe di servizio è completato.

Il problema è che la classe di servizio utilizza oggetti pesanti e dovrebbe rilasciarli. Ho impostato questa parte in un metodo (chiamiamolo release()), ma non è garantito che altri sviluppatori useranno questo metodo.

C'è un modo per costringere altri sviluppatori a chiamare questo metodo dopo aver completato l'attività della classe di servizio? Certo che posso documentarlo, ma voglio forzarli.

Nota: non posso chiamare il release()metodo nel doWork()metodo, perché ho doWork() bisogno di quegli oggetti quando viene chiamato in seguito.


46
Quello che stai facendo è una forma di accoppiamento temporale e in genere considerato un odore di codice. Potresti voler forzarti a trovare un design migliore invece.
Frank,

1
@Frank Concordo, se cambiare il design del layer sottostante è un'opzione, quella sarebbe la scommessa migliore. Ma ho il sospetto che questo sia un involucro attorno al servizio di qualcun altro.
Dar Brett,

Di che tipo di oggetti pesanti ha bisogno il tuo servizio? Se la classe di servizio non è in grado di gestire automaticamente tali risorse da sola (senza richiedere l'accoppiamento temporale), forse tali risorse dovrebbero essere gestite altrove e iniettate nel servizio. Hai scritto test unitari per la classe Service?
VIENI DAL

18
È molto "non Java" richiederlo. Java è un linguaggio con Garbage Collection e ora ti viene in mente un qualche tipo di aggeggio in cui devi "ripulire" gli sviluppatori.
Pieter B,

22
@PieterB perché? AutoCloseableè stato creato per questo preciso scopo.
BgrWorker,

Risposte:


48

La soluzione pragmatica è quella di creare la classe AutoCloseablee fornire un finalize()metodo come backstop (se appropriato ... vedi sotto!). Quindi fai affidamento sugli utenti della tua classe per utilizzare try-with-resource o chiamare close()esplicitamente.

Certo che posso documentarlo, ma voglio forzarli.

Sfortunatamente, non c'è modo 1 in Java di forzare il programmatore a fare la cosa giusta. Il meglio che potresti sperare di fare è raccogliere un utilizzo errato in un analizzatore di codice statico.


Sul tema dei finalizzatori. Questa funzionalità Java ha pochissimi casi d'uso validi . Se fai affidamento sui finalizzatori per riordinare, ti imbatti nel problema che può richiedere molto tempo prima che il riordino abbia luogo. Il finalizzatore verrà eseguito solo dopo che il GC ha deciso che l'oggetto non è più raggiungibile. Ciò potrebbe non accadere fino a quando la JVM non eseguirà una raccolta completa .

Pertanto, se il problema che stai cercando di risolvere è recuperare risorse che devono essere rilasciate in anticipo , allora hai perso la barca usando la finalizzazione.

E nel caso in cui non abbiate ottenuto quello che sto dicendo sopra ... non è quasi mai appropriato usare i finalizzatori nel codice di produzione e non dovreste mai fare affidamento su di essi!


1 - Ci sono modi. Se si è pronti a "nascondere" gli oggetti di servizio dal codice utente o controllare strettamente il loro ciclo di vita (ad es. Https://softwareengineering.stackexchange.com/a/345100/172 ), non è necessario chiamare il codice degli utenti release(). Tuttavia, l'API diventa più complicata, limitata ... e "brutta" IMO. Inoltre, ciò non obbliga il programmatore a fare la cosa giusta . Rimuove la capacità del programmatore di fare la cosa sbagliata!


1
I finalizzatori insegnano una brutta lezione: se la rovini, la aggiusterò per te. Preferisco l'approccio di netty -> un parametro di configurazione che indica alla factory (ByteBuffer) di creare casualmente con probabilità di X% bytebuffer con un finalizzatore che stampa la posizione in cui è stato acquisito questo buffer (se non è già stato rilasciato). Quindi nella produzione questo valore può essere impostato allo 0% - cioè senza spese generali, e durante i test e dev a un valore più alto come il 50% o addirittura il 100% al fine di rilevare le perdite prima di rilasciare il prodotto
Svetlin Zarev

11
e ricorda che non è garantito che i finalizzatori vengano mai eseguiti, anche se l'istanza dell'oggetto viene raccolta in modo inutile!
jwenting

3
Vale la pena notare che Eclipse emette un avviso del compilatore se un AutoCloseable non è chiuso. Mi aspetto che anche altri IDE Java lo facciano.
meriton - in sciopero

1
@jwenting In quale caso non corrono quando l'oggetto è GC'd? Non sono riuscito a trovare alcuna risorsa che affermi o spieghi ciò.
Cedric Reichenbach il

3
@Voo Hai frainteso il mio commento. Hai due implementazioni -> DefaultResourcee FinalizableResourceche estende la prima e aggiunge un finalizzatore. In produzione si utilizza DefaultResourceche non ha finalizzatore-> nessun sovraccarico. Durante lo sviluppo e il test, l'app viene configurata per l'uso FinalizableResourceche ha un finalizzatore e quindi aggiunge un sovraccarico. Durante lo sviluppo e il test non è un problema, ma può aiutarti a identificare le perdite e risolverle prima di raggiungere la produzione. Tuttavia, se una perdita raggiunge PRD, è possibile configurare la fabbrica in modo da creare l'X% FinalizableResourceper
identificare

55

Questo sembra il caso d'uso per l' AutoCloseableinterfaccia e l' try-with-resourcesistruzione introdotta in Java 7. Se hai qualcosa di simile

public class MyService implements AutoCloseable {
    public void doWork() {
        // ...
    }

    @Override
    public void close() {
        // release resources
    }
}

quindi i tuoi consumatori possono usarlo come

public class MyConsumer {
    public void foo() {
        try (MyService myService = new MyService()) {
            //...
            myService.doWork()
            //...
            myService.doWork()
            //...
        }
    }
}

e non devi preoccuparti di chiamare un esplicito releaseindipendentemente dal fatto che il loro codice generi un'eccezione.


Risposta aggiuntiva

Se quello che stai veramente cercando è assicurarti che nessuno possa dimenticare di usare la tua funzione, devi fare un paio di cose

  1. Assicurarsi che tutti i costruttori di MyServicesiano privati.
  2. Definire un singolo punto di accesso da utilizzare per MyServicegarantire che venga ripulito dopo.

Faresti qualcosa del genere

public interface MyServiceConsumer {
    void accept(MyService value);
}

public class MyService implements AutoCloseable {
    private MyService() {
    }


    public static void useMyService(MyServiceConsumer consumer) {
        try (MyService myService = new MyService()) {
            consumer.accept(myService)
        }
    }
}

Lo useresti quindi come

public class MyConsumer {
    public void foo() {
        MyService.useMyService(new MyServiceConsumer() {
            public void accept(MyService myService) {
                myService.doWork()
            }
        });
    }
}

All'inizio non ho raccomandato questo percorso perché è orribile senza lambda Java-8 e interfacce funzionali (dove MyServiceConsumerdiventa Consumer<MyService>) ed è abbastanza imponente per i tuoi consumatori. Ma se quello che vuoi solo è che release()deve essere chiamato, funzionerà.


1
Questa risposta è probabilmente migliore della mia. La mia strada ha un ritardo prima che entri in azione il Garbage Collector.
Dar Brett,

1
Come si fa a garantire che i clienti utilizzino try-with-resourcese non solo omettano try, perdendo così la closechiamata?
Frank,

@walpen In questo modo ha lo stesso problema. Come posso forzare l'utente a utilizzare try-with-resources?
Hasanghaforian

1
@Frank / @hasanghaforian, Sì, in questo modo non forza per essere chiamato. Ha il vantaggio della familiarità: gli sviluppatori Java sanno che dovresti davvero assicurarti che .close()venga chiamato quando la classe implementa AutoCloseable.
Walpen,

1
Non potresti associare un finalizzatore a un usingblocco per la finalizzazione deterministica? Almeno in C #, questo diventa un modello abbastanza chiaro per gli sviluppatori di prendere l'abitudine di usare quando sfruttano risorse che necessitano di finalizzazione deterministica. Qualcosa di facile e di abitudine è la prossima cosa migliore quando non puoi costringere qualcuno a fare qualcosa. stackoverflow.com/a/2016311/84206
AaronLS

52

si, puoi

Puoi assolutamente costringerli a rilasciarlo e renderlo impossibile da dimenticare al 100%, rendendo impossibile per loro creare nuove Serviceistanze.

Se hanno fatto qualcosa del genere prima:

Service s = s.new();
try {
  s.doWork("something");
  if (...) s.doWork("something else");
  ...
}
finally {
  s.release(); // oops: easy to forget
}

Quindi modificalo in modo che debbano accedervi in ​​questo modo.

// Their code

Service.runWithRelease(new ServiceConsumer() {
  void run(Service s) {
    s.doWork("something");
    if (...) s.doWork("something else");
    ...
  }  // yay! no s.release() required.
}

// Your code

interface ServiceConsumer {
  void run(Service s);
}

class Service {

   private Service() { ... }      // now: private
   private void release() { ... } // now: private
   public void doWork() { ... }   // unchanged

   public static void runWithRelease(ServiceConsumer consumer) {
      Service s = new Service();
      try {
        consumer.run(s);
      }
      finally {
        s.release();
      } 
    } 
  }

Avvertenze:

  • Considera questo pseudocodice, sono passati secoli da quando ho scritto Java.
  • In questi giorni potrebbero esserci varianti più eleganti, forse includendo quell'interfaccia AutoCloseablemenzionata da qualcuno; ma l'esempio dato dovrebbe funzionare fuori dagli schemi, e fatta eccezione per l'eleganza (che è un bel obiettivo in sé) non ci dovrebbero essere ragioni importanti per cambiarlo. Si noti che ciò intende dire che è possibile utilizzare AutoCloseableall'interno dell'implementazione privata; il vantaggio della mia soluzione rispetto all'utilizzo dei tuoi utenti AutoCloseableè che non può essere nuovamente dimenticato.
  • Dovrai perfezionarlo secondo necessità, ad esempio per inserire argomenti nella new Servicechiamata.
  • Come menzionato nei commenti, la questione se un costrutto come questo (ovvero, prendere il processo di creazione e distruzione di a Service) appartenga o meno alla mano del chiamante non rientra nell'ambito di questa risposta. Ma se decidi di averne assolutamente bisogno, ecco come puoi farlo.
  • Un commentatore ha fornito queste informazioni sulla gestione delle eccezioni: Google Libri

2
Sono contento dei commenti per il downvote, quindi posso migliorare la risposta.
AnoE

2
Non ho votato in negativo, ma è probabile che questo progetto sia più un problema di quanto valga la pena. Aggiunge molta complessità e impone un grande onere ai chiamanti. Gli sviluppatori dovrebbero avere abbastanza familiarità con le classi di stile prova con le risorse che il loro utilizzo è di seconda natura ogni volta che una classe implementa l'interfaccia appropriata, quindi in genere è abbastanza buona.
jpmc26,

30
@ jpmc26, stranamente sento che quell'approccio riduce la complessità e l'onere per i chiamanti salvandoli dal pensare al ciclo di vita delle risorse. A parte quello; l'OP non ha chiesto "dovrei forzare gli utenti ..." ma "come forzare gli utenti". E se è abbastanza importante, questa è una soluzione che funziona molto bene, è strutturalmente semplice (semplicemente avvolgendola in una chiusura / eseguibile), anche trasferibile in altre lingue che hanno lambdas / procs / chiusure pure. Bene, lo lascerò alla democrazia SE per giudicare; l'OP ha già deciso comunque. ;)
AnoE

14
Ancora una volta, @ jpmc26, non sono molto interessato a discutere se questo approccio è corretto per ogni singolo caso in uso. L'OP chiede come applicare una cosa del genere e questa risposta spiega come applicare tale cosa . Non spetta a noi decidere se è opportuno farlo in primo luogo. Credo che la tecnica mostrata sia uno strumento, niente di più, niente di meno e utile da avere in una borsa degli attrezzi.
AnoE

11
Questa è la risposta corretta imho, è essenzialmente il modello hole-in-the-middle
jk.

11

Potresti provare a utilizzare il modello di comando .

class MyServiceManager {

    public void execute(MyServiceTask tasks...) {
        // set up everyting
        MyService service = this.acquireService();
        // execute the submitted tasks
        foreach (Task task : tasks)
            task.executeWith(service);
        // cleanup yourself
        service.releaseResources();
    }

}

Questo ti dà il pieno controllo sull'acquisizione e il rilascio delle risorse. Il chiamante invia solo attività al tuo Servizio e tu stesso sei responsabile dell'acquisizione e della pulizia delle risorse.

C'è un problema, tuttavia. Il chiamante può ancora fare questo:

MyServiceTask t1 = // some task
manager.execute(t1);
MyServiceTask t2 = // some task
manager.execute(t2);

Ma puoi risolvere questo problema quando si presenta. Quando ci sono problemi di prestazioni e scopri che alcuni chiamanti lo fanno, mostra loro il modo corretto e risolvi il problema:

MyServiceTask t1 = // some task
MyServiceTask t2 = // some task
manager.execute(t1, t2);

Puoi renderlo arbitrariamente complesso implementando promesse per attività che dipendono da altre attività, ma poi rilasciare cose diventa anche più complicato. Questo è solo un punto di partenza.

Async

Come è stato sottolineato nei commenti, quanto sopra non funziona davvero con richieste asincrone. È corretto. Ma questo può essere facilmente risolto in Java 8 con l'uso di CompleteableFuture , in particolare CompleteableFuture#supplyAsyncper creare i singoli futuri e CompleteableFuture#allOfperfezionare il rilascio delle risorse al termine di tutte le attività. In alternativa, è sempre possibile utilizzare Thread o ExecutorServices per implementare la propria implementazione di Futures / Promesse.


1
Gradirei se i downvoter lasciano un commento sul perché ritengono che ciò sia negativo, in modo da poter affrontare direttamente tali preoccupazioni o almeno ottenere un altro punto di vista per il futuro. Grazie!
Polygnome,

+1 voto. Questo può essere un approccio, ma il servizio è asincrono e il consumatore deve ottenere il primo risultato prima di richiedere il servizio successivo.
Hasanghaforian

1
Questo è solo un modello barebone. JS risolve questo con le promesse, potresti fare cose simili in Java con i futures e un raccoglitore che viene chiamato quando tutte le attività sono terminate e poi ripulite. È sicuramente possibile ottenere asincrono e persino dipendenze lì, ma complica anche molto le cose.
Polygnome,

3

Se possibile, non consentire all'utente di creare la risorsa. Consenti all'utente di passare un oggetto visitatore al tuo metodo, che creerà la risorsa, passerà al metodo dell'oggetto visitatore e rilascerà successivamente.


Grazie per la tua risposta, ma scusami, vuoi dire che è meglio passare risorse al consumatore e poi ottenerlo di nuovo quando chiede assistenza? Quindi il problema rimarrà: il consumatore potrebbe dimenticare di rilasciare tali risorse.
Hasanghaforian

Se vuoi controllare una risorsa, non lasciarla andare, non passarla completamente al flusso di esecuzione di qualcun altro. Un modo per farlo è eseguire un metodo predefinito dell'oggetto visitatore di un utente. AutoCloseablefa una cosa molto simile dietro le quinte. Sotto un'altra prospettiva, è simile all'iniezione di dipendenza .
9000

1

La mia idea era simile a quella di Polygnome.

Puoi avere i metodi "do_something ()" nella tua classe semplicemente aggiungendo comandi a un elenco di comandi privato. Quindi, disporre di un metodo "commit ()" che effettivamente fa il lavoro e chiama "release ()".

Quindi, se l'utente non chiama mai commit (), il lavoro non viene mai eseguito. In tal caso, le risorse vengono liberate.

public class Foo
{
  private ArrayList<ICommand> _commands;

  public Foo doSomething(int arg) { _commands.Add(new DoSomethingCommand(arg)); return this; }
  public Foo doSomethingElse() { _commands.Add(new DoSomethingElseCommand()); return this; }

  public void commit() { 
     for(ICommand c : _commands) c.doWork();
     release();
     _commands.clear();
  }

}

Puoi quindi usarlo come foo.doSomething.doSomethineElse (). Commit ();


Questa è la stessa soluzione, ma invece di chiedere al chiamante di mantenere l'elenco delle attività lo gestisci internamente. Il concetto di base è letteralmente lo stesso. Ma questo non segue alcuna convenzione di codifica per Java che io abbia mai visto ed è in realtà abbastanza difficile da leggere (corpo del ciclo sulla stessa linea dell'intestazione del ciclo, all'inizio).
Poligome

Sì, è il modello di comando. Ho appena messo un po 'di codice per dare un'idea di cosa stavo pensando nel modo più conciso possibile. Scrivo principalmente C # in questi giorni, dove le variabili private sono spesso precedute da _.
Sava B.

-1

Un'altra alternativa a quelle menzionate sarebbe l'uso di "riferimenti intelligenti" per gestire le risorse incapsulate in un modo che consenta al garbage collector di ripulire automaticamente senza liberare manualmente le risorse. La loro utilità dipende ovviamente dalla vostra implementazione. Maggiori informazioni su questo argomento in questo meraviglioso post sul blog: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references


Questa è un'idea interessante, ma non riesco a vedere come l'uso di riferimenti deboli risolva il problema del PO. La classe di servizio non sa se otterrà un'altra richiesta doWork (). Se rilascia il riferimento ai suoi oggetti pesanti e quindi riceve un'altra chiamata doWork (), fallirà.
Jay Elston,

-4

Per qualsiasi altro linguaggio orientato alla classe direi di metterlo nel distruttore, ma dal momento che questa non è un'opzione penso che tu possa scavalcare il metodo finalize. In questo modo, quando l'istanza non rientra nell'ambito di applicazione e viene raccolta spazzatura, verrà rilasciata.

@Override
public void finalize() {
    this.release();
}

Non ho molta familiarità con Java, ma penso che questo sia lo scopo per cui è finalizzato ().

http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#finalize ()


7
Questa è una cattiva soluzione al problema per il motivo che dice Stephen nella sua risposta -If you rely on finalizers to tidy up, you run into the problem that it can take a very long time for the tidy-up to happen. The finalizer will only be run after the GC decides that the object is no longer reachable. That may not happen until the JVM does a full collection.
Daenyth,

4
E anche allora il finalizzatore potrebbe non funzionare ...
jwenting

3
I finalizzatori sono notoriamente inaffidabili: possono funzionare, potrebbero non funzionare mai, se funzionano non vi è alcuna garanzia quando verranno eseguiti. Persino gli architetti Java come Josh Bloch affermano che i finalizzatori sono un'idea terribile (riferimento: Effective Java (2nd Edition) ).

Questo tipo di problemi mi fanno perdere il C ++ con la sua distruzione deterministica e RAII ...
Mark K Cowan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.