Forza 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 metodo doWork() ). Quindi non so quando il lavoro della classe di servizio è completato.

Il problema è che la classe di servizio usa oggetti pesanti e dovrebbe rilasciarli. Ho impostato questa parte in un metodo (chiamiamolo release() ), ma non è garantito che altri sviluppatori utilizzeranno 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 metodo release() nel metodo doWork() , perché doWork() ha bisogno di quegli oggetti quando viene chiamato in next.

    
posta hasanghaforian 28.03.2017 - 06:49
fonte

8 risposte

48

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

Of course I can document that, but I want to force them.

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

Sull'argomento dei finalizzatori. Questa funzione Java ha pochissimi casi d'uso buoni . Se ti affidi ai finalizzatori per riordinare, ti imbatti nel problema che può richiedere molto tempo perché il riordino avvenga. Il finalizzatore verrà eseguito solo dopo il GC decide che l'oggetto non è più raggiungibile. Questo potrebbe non succedere finché la JVM non fa una raccolta completa .

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

E nel caso in cui non hai quello che sto dicendo sopra ... è quasi mai appropriato usare i finalizzatori nel codice di produzione, e dovresti mai fidatevi di loro!

1 - Ci sono modi. Se sei pronto a "nascondere" gli oggetti di servizio dal codice utente o a controllare strettamente il loro ciclo di vita (ad es. link ) allora il codice degli utenti non ha bisogno di per chiamare release() . Tuttavia, l'API diventa più complicata, limitata ... e "brutta" IMO. Inoltre, non è che obbliga il programmatore a fare la cosa giusta . Sta rimuovendo la capacità del programmatore di fare la cosa sbagliata!

    
risposta data 28.03.2017 - 09:57
fonte
57

Questo sembra il caso d'uso per l'interfaccia AutoCloseable e la dichiarazione try-with-resources presentata in Java 7. Se hai qualcosa di simile

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

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

allora 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 release esplicito indipendentemente dal fatto che il loro codice generi un'eccezione.

Risposta aggiuntiva

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

  1. Accertati che tutti i costruttori di MyService siano privati.
  2. Definisci un singolo punto di entrata per utilizzare MyService che assicura che venga ripulito dopo.

Faresti qualcosa di simile

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 utilizzeresti come

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

Non ho consigliato questo percorso in origine perché è orribile senza Java-8 lambdas e interfacce funzionali (dove MyServiceConsumer diventa Consumer<MyService> ) ed è abbastanza imponente per i tuoi consumatori. Ma se quello che vuoi è che release() debba essere chiamato, funzionerà.

    
risposta data 28.03.2017 - 07:27
fonte
50

Sì, puoi

Puoi assolutamente forzarli a rilasciare e renderli così è impossibile dimenticarli al 100%, rendendo impossibile per loro creare nuove istanze di Service .

Se hanno fatto qualcosa di simile prima:

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

Quindi cambialo 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 anni che ho scritto Java.
  • Potrebbero esserci varianti più eleganti in questi giorni, forse includendo quell'interfaccia AutoCloseable citata da qualcuno; ma l'esempio dato dovrebbe funzionare fuori dalla scatola, e fatta eccezione per l'eleganza (che è un bel obiettivo in sé) non ci dovrebbero essere motivi importanti per cambiarlo. Tieni presente che questo intende dire che potresti utilizzare AutoCloseable nella tua implementazione privata; il vantaggio della mia soluzione rispetto al fatto che i tuoi utenti utilizzino AutoCloseable è che non può, di nuovo, essere dimenticato.
  • Dovrai adattarlo come necessario, ad esempio per inserire argomenti nella chiamata new Service .
  • Come menzionato nei commenti, la questione se un costrutto come questo (cioè, prendere il processo di creare e distruggere un Service ) appartiene nella mano del chiamante o meno è al di fuori dello scopo di questa risposta. Ma se decidi di averne assolutamente bisogno, è così che puoi farlo.
  • Un commentatore ha fornito queste informazioni sulla gestione delle eccezioni: Google Libri
risposta data 28.03.2017 - 16:36
fonte
11

Potresti provare a utilizzare il pattern 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, comunque. Il chiamante può ancora fare questo:

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

Ma puoi affrontare questo problema quando si verifica. Quando ci sono problemi di prestazioni e scopri che alcuni chiamano, fai semplicemente vedere 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 anche il rilascio di materiale diventa più complicato. Questo è solo un punto di partenza.

Async

Come è stato sottolineato nei commenti, quanto sopra non funziona davvero con le richieste asincrone. È corretto. Ma questo può essere facilmente risolto in Java 8 con l'uso di CompleteableFuture , in particolare CompleteableFuture#supplyAsync per creare i singoli futures e CompleteableFuture#allOf per eseguire il rilascio delle risorse una volta terminate le attività tutte . In alternativa, si può sempre usare Threads o ExecutorServices per implementare la propria implementazione di Futures / Promises.

    
risposta data 28.03.2017 - 17:00
fonte
3

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

    
risposta data 28.03.2017 - 17:09
fonte
2

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, avere un metodo "commit ()" che effettivamente funziona e chiama "release ()".

Quindi, se l'utente non chiama mai commit (), il lavoro non viene mai eseguito. Se lo fanno, 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 ();

    
risposta data 28.03.2017 - 17:33
fonte
-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 effettuare automaticamente la pulizia senza liberare manualmente le risorse. La loro utilità dipende ovviamente dalla tua implementazione. Leggi di più su questo argomento in questo meraviglioso post: link

    
risposta data 29.03.2017 - 14:28
fonte
-3

Per qualsiasi altra lingua orientata alla classe, direi di inserirla nel distruttore, ma poiché non è un'opzione, penso che sia possibile sovrascrivere il metodo finalizzato. In questo modo, quando l'istanza esce dal campo di applicazione e viene raccolta in una garbage, verrà rilasciata.

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

Non ho molta familiarità con Java, ma penso che questo sia lo scopo per il quale finalize () è inteso.

link

    
risposta data 28.03.2017 - 07:25
fonte

Leggi altre domande sui tag