Zwingen Sie andere Entwickler, die Methode nach Abschluss ihrer Arbeit aufzurufen

34

In einer Bibliothek in Java 7 habe ich eine Klasse, die Dienste für andere Klassen bereitstellt. Nach dem Erstellen einer Instanz dieser Serviceklasse kann eine Methode davon mehrmals aufgerufen werden (nennen wir sie die doWork()Methode). Ich weiß also nicht, wann die Arbeit der Serviceklasse abgeschlossen ist.

Das Problem ist, dass die Serviceklasse schwere Objekte verwendet und diese freigeben sollte. Ich habe diesen Teil in einer Methode festgelegt (nennen wir es release()), aber es ist nicht garantiert, dass andere Entwickler diese Methode verwenden.

Gibt es eine Möglichkeit, andere Entwickler zu zwingen, diese Methode aufzurufen, nachdem die Aufgabe der Serviceklasse abgeschlossen wurde? Natürlich kann ich das dokumentieren, aber ich möchte sie zwingen.

Hinweis: Ich kann die release()Methode in der doWork()Methode nicht aufrufen , da doWork() diese Objekte beim nächsten Aufruf benötigt werden.

hasanghaforian
quelle
46
Was Sie dort tun, ist eine Form der zeitlichen Kopplung und wird normalerweise als Codegeruch betrachtet. Vielleicht möchten Sie sich zwingen, stattdessen ein besseres Design zu entwickeln.
Frank
1
@Frank Ich würde zustimmen, wenn das Ändern des Designs der darunter liegenden Ebene eine Option ist, ist dies die beste Wahl. Aber ich vermute, das ist eine Hülle um den Dienst eines anderen.
Dar Brett
Welche schweren Gegenstände benötigt Ihr Service? Wenn die Serviceklasse diese Ressourcen nicht automatisch selbst verwalten kann (ohne dass eine zeitliche Kopplung erforderlich ist), sollten diese Ressourcen möglicherweise an einer anderen Stelle verwaltet und in den Service eingefügt werden. Haben Sie Komponententests für die Serviceklasse geschrieben?
KOMMEN SIE VOM
18
Es ist sehr "un-Java", das zu verlangen. Java ist eine Sprache mit Garbage Collection und jetzt haben Sie eine Art Apparat, in dem Entwickler "aufräumen" müssen.
Pieter B
22
@PieterB warum? AutoCloseablewurde für genau diesen Zweck gemacht.
BgrWorker

Antworten:

48

Die pragmatische Lösung besteht darin, die Klasse zu erstellen AutoCloseableund eine finalize()Methode als Backstop bereitzustellen (falls zutreffend ... siehe unten!). Dann verlassen Sie sich darauf, dass Benutzer Ihrer Klasse try-with-resource verwenden oder close()explizit anrufen .

Natürlich kann ich das dokumentieren, aber ich möchte sie zwingen.

Leider gibt es in Java keine Möglichkeit 1 , den Programmierer dazu zu zwingen, das Richtige zu tun. Das Beste, was Sie hoffen können, ist, eine falsche Verwendung in einem statischen Code-Analysator zu erkennen.


Zum Thema Finalizer. Diese Java-Funktion hat nur sehr wenige gute Anwendungsfälle. Wenn Sie sich darauf verlassen, dass die Finalizer aufräumen, besteht das Problem, dass das Aufräumen sehr lange dauern kann. Die Finalizerthread wird nur dann ausgeführt werden , nachdem die GC entscheidet , dass das Objekt nicht mehr erreichbar ist . Dies kann nicht passieren, bis die JVM eine vollständige Sammlung durchführt.

Wenn das Problem, das Sie lösen möchten, darin besteht, Ressourcen zurückzugewinnen, die vorzeitig freigegeben werden müssen , haben Sie das Boot mit der Finalisierung verpasst.

Und nur für den Fall, dass Sie nicht haben, was ich oben sage ... es ist fast nie angebracht, Finalizer im Produktionscode zu verwenden, und Sie sollten sich niemals auf sie verlassen!


1 - Es gibt Wege. Wenn Sie zu „verstecken“ vorbereitet sind die Service - Objekte aus Benutzercode oder fest ihren Lebenszyklus steuern (zB https://softwareengineering.stackexchange.com/a/345100/172 ) , dann dem Benutzer - Code nicht brauchen , um Anruf release(). Die API wird jedoch komplizierter, eingeschränkter ... und "hässlicher" IMO. Dies zwingt den Programmierer auch nicht dazu, das Richtige zu tun . Es beseitigt die Fähigkeit des Programmierers, das Falsche zu tun!

Stephen C
quelle
1
Die Finalisten haben eine sehr schlechte Lektion - wenn Sie es vermasseln, repariere ich es für Sie. Ich bevorzuge den Ansatz von netty -> einen Konfigurationsparameter, der die (ByteBuffer) -Factory anweist, zufällig mit einer Wahrscheinlichkeit von X% Bytepuffern mit einem Finalizer zu erstellen, der den Ort druckt, an dem dieser Puffer erfasst wurde (falls nicht bereits freigegeben). So kann dieser Wert in der Produktion auf 0% gesetzt werden - dh kein Overhead, und während des Testens und Entwickelns auf einen höheren Wert wie 50% oder sogar 100%, um die Undichtigkeiten zu erkennen, bevor das Produkt freigegeben wird
Svetlin Zarev
11
und denken Sie daran, dass Finalizer garantiert nicht ausgeführt werden, auch wenn die Objektinstanz nicht ordnungsgemäß erfasst wird!
Jwenting
3
Es ist erwähnenswert, dass Eclipse eine Compiler-Warnung ausgibt, wenn ein AutoCloseable nicht geschlossen wird. Ich würde auch von anderen Java-IDEs erwarten, dass sie dies tun.
Meriton - im Streik
1
@jwenting In welchem ​​Fall werden sie nicht ausgeführt, wenn das Objekt mit GCs versehen ist? Ich konnte keine Ressource finden, die das angibt oder erklärt.
Cedric Reichenbach
3
@Voo Du hast meinen Kommentar falsch verstanden. Sie haben zwei Implementierungen -> DefaultResourceund FinalizableResourcedie erste erweitert und fügt einen Finalizer hinzu. In der Produktion verwenden Sie, DefaultResourcedie keinen Finalizer-> keinen Overhead hat. Während der Entwicklung und des Testens konfigurieren Sie die zu verwendende App, FinalizableResourcedie über einen Finalizer verfügt und somit zusätzlichen Aufwand verursacht. Während des Entwickelns und Testens ist dies kein Problem, aber es kann Ihnen helfen, Lecks zu identifizieren und zu beheben, bevor Sie die Produktion erreichen. Wenn ein FinalizableResourceLeck PRD erreicht, können Sie die Fabrik so konfigurieren, dass X% erstellt werden, um das Leck zu
identifizieren
55

Dies scheint der Anwendungsfall für die AutoCloseableSchnittstelle und die in Java 7 eingeführte try-with-resourcesAnweisung zu sein

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

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

dann können Ihre Verbraucher es als

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

und müssen sich nicht darum kümmern, einen expliziten releaseCode aufzurufen, unabhängig davon, ob sein Code eine Ausnahme auslöst.


Zusätzliche Antwort

Wenn Sie wirklich sicherstellen möchten, dass niemand vergessen kann, Ihre Funktion zu verwenden, müssen Sie einige Dinge tun

  1. Stellen Sie sicher, dass alle Konstruktoren von MyServiceprivat sind.
  2. Definieren Sie einen einzelnen zu verwendenden Eintrittspunkt MyService, der sicherstellt, dass er nach dem Bereinigen gelöscht wird.

Du würdest sowas machen

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

Sie würden es dann als verwenden

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

Ich habe diesen Pfad ursprünglich nicht empfohlen, da er ohne Java-8-Lambdas und funktionale Schnittstellen (wo MyServiceConsumerwird Consumer<MyService>) abscheulich ist und für Ihre Verbraucher ziemlich imposant ist. Aber wenn Sie nur wollen, dass release()aufgerufen werden muss, wird es funktionieren.

walpen
quelle
1
Diese Antwort ist wahrscheinlich besser als meine. Mein Weg hat eine Verzögerung, bevor der Garbage Collector eintrifft.
Dar Brett
1
Wie stellen Sie sicher, dass Clients das verwenden try-with-resourcesund nicht einfach weglassen tryund somit den closeAnruf verpassen ?
Frank
@walpen Dieser Weg hat das gleiche Problem. Wie kann ich den Benutzer zur Nutzung zwingen try-with-resources?
Hasanghaforian
1
@Frank / @hasanghaforian, Ja, dieser Weg zwingt nicht dazu, aufgerufen zu werden. Es hat den Vorteil der Vertrautheit: Java-Entwickler wissen, dass Sie wirklich sicherstellen sollten, dass .close()es aufgerufen wird, wenn die Klasse implementiert wird AutoCloseable.
Walpen
1
Könnten Sie einen Finalizer nicht mit einem usingBlock für die deterministische Finalisierung verbinden? Zumindest in C # wird dies zu einem ziemlich klaren Muster für Entwickler, die sich angewöhnen, Ressourcen zu nutzen, die deterministisch finalisiert werden müssen. Etwas Leichtes und Gewohnheitsbildendes ist das Nächstbeste, wenn man jemanden nicht zwingen kann, etwas zu tun. stackoverflow.com/a/2016311/84206
AaronLS
52

Ja, du kannst

Sie können sie zwingen, neue ServiceInstanzen zu erstellen, und dies zu 100% unmöglich machen, indem Sie es ihnen unmöglich machen, neue Instanzen zu erstellen .

Wenn sie so etwas schon einmal gemacht haben:

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

Ändern Sie es dann so, dass sie so darauf zugreifen müssen.

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

Vorsichtsmaßnahmen:

  • Betrachten Sie diesen Pseudocode, es ist eine Ewigkeit her, seit ich Java geschrieben habe.
  • Es könnte heutzutage elegantere Varianten geben, einschließlich der von AutoCloseablejemandem erwähnten Schnittstelle. Aber das gegebene Beispiel sollte sofort funktionieren, und abgesehen von der Eleganz (die ein schönes Ziel für sich ist) sollte es keinen wichtigen Grund geben, es zu ändern. Beachten Sie, dass dies bedeuten soll, dass Sie es AutoCloseablein Ihrer privaten Implementierung verwenden können. Der Vorteil meiner Lösung gegenüber der Verwendung durch Ihre Benutzer AutoCloseablebesteht darin, dass sie nicht vergessen werden kann.
  • Sie müssen es nach Bedarf ausarbeiten, um beispielsweise Argumente in den new ServiceAufruf einzufügen.
  • Wie in den Kommentaren erwähnt, liegt die Frage, ob ein solches Konstrukt (dh der Prozess des Erzeugens und Zerstörens von a Service) in der Hand des Aufrufers liegt oder nicht, außerhalb des Bereichs dieser Antwort. Aber wenn Sie sich dazu entschließen, dass Sie das unbedingt brauchen, können Sie es so machen.
  • Ein Kommentator lieferte diese Informationen zur Ausnahmebehandlung: Google Books
AnoE
quelle
2
Ich freue mich über Kommentare zum Downvote, damit ich die Antwort verbessern kann.
AnoE
2
Ich habe nicht abgelehnt, aber dieses Design ist wahrscheinlich mehr Mühe als es wert ist. Dies erhöht die Komplexität und stellt eine große Belastung für die Anrufer dar. Entwickler sollten mit den Try-with-Resources-Stilklassen vertraut sein, deren Verwendung immer dann selbstverständlich ist, wenn eine Klasse die entsprechende Schnittstelle implementiert. Dies ist in der Regel ausreichend.
jpmc26
30
@ jpmc26, ich finde, dass dieser Ansatz die Komplexität und die Belastung der Anrufer verringert, indem sie davon abgehalten werden, überhaupt über den Ressourcenlebenszyklus nachzudenken. Außerdem; Das OP fragte nicht "sollte ich die Benutzer erzwingen ...", sondern "wie erzwinge ich die Benutzer". Und wenn es wichtig genug ist, ist dies eine Lösung, die sehr gut funktioniert, strukturell einfach ist (einfach in einen Closure / Runnable einwickeln) und sogar auf andere Sprachen übertragbar ist, die ebenfalls Lambdas / Procs / Closures enthalten. Nun, ich überlasse es der SE-Demokratie, darüber zu urteilen. das OP hat sowieso schon entschieden. ;)
AnoE
14
Nochmals, @ jpmc26, ich bin nicht so sehr daran interessiert zu diskutieren, ob dieser Ansatz für einen einzelnen Anwendungsfall geeignet ist. Das OP fragt, wie so etwas durchgesetzt werden soll, und diese Antwort gibt Auskunft darüber, wie so etwas durchgesetzt werden soll . Es ist nicht unsere Aufgabe, zu entscheiden, ob dies überhaupt angebracht ist. Die gezeigte Technik ist ein Werkzeug, nicht mehr und nicht weniger, und ich glaube, sie ist nützlich, um sie in der Werkzeugtasche zu haben.
AnoE
11
Dies ist die richtige Antwort imho, es ist im Wesentlichen das Loch-in-der-Mitte-Muster
jk.
11

Sie können versuchen, das Befehlsmuster zu verwenden.

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

}

Dies gibt Ihnen die volle Kontrolle über die Beschaffung und Freigabe von Ressourcen. Der Anrufer übergibt nur Aufgaben an Ihren Dienst, und Sie sind selbst dafür verantwortlich, Ressourcen zu beschaffen und zu bereinigen.

Es gibt jedoch einen Haken. Der Anrufer kann dies trotzdem tun:

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

Aber Sie können dieses Problem ansprechen, wenn es auftritt. Wenn es Leistungsprobleme gibt und Sie feststellen, dass einige Anrufer dies tun, zeigen Sie ihnen einfach den richtigen Weg und beheben Sie das Problem:

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

Sie können dies beliebig komplex gestalten, indem Sie Versprechungen für Aufgaben implementieren, die von anderen Aufgaben abhängig sind. Das Freigeben von Inhalten wird jedoch auch komplizierter. Dies ist nur ein Ausgangspunkt.

Async

Wie in den Kommentaren ausgeführt wurde, funktioniert das oben Genannte nicht wirklich mit asynchronen Anforderungen. Das ist richtig. Dies kann in Java 8 mit CompleteableFuture problemlos gelöst werden , insbesondere CompleteableFuture#supplyAsyncum die einzelnen Zukünfte zu erstellen und CompleteableFuture#allOfdie Freigabe der Ressourcen durchzuführen, sobald alle Aufgaben abgeschlossen sind. Alternativ kann man jederzeit Threads oder ExecutorServices verwenden, um eine eigene Implementierung von Futures / Promises durchzuführen.

Polygnom
quelle
1
Ich würde es begrüßen, wenn Downvoter einen Kommentar hinterlassen, warum sie dies für schlecht halten, damit ich diese Bedenken entweder direkt ansprechen oder zumindest eine andere Sichtweise für die Zukunft erhalten kann. Vielen Dank!
Polygnome
+1 positive Bewertung Dies mag ein Ansatz sein, aber der Dienst ist asynchron und der Verbraucher muss das erste Ergebnis erhalten, bevor er nach dem nächsten Dienst fragt.
Hasanghaforian
1
Dies ist nur eine Barebones-Vorlage. JS löst dies mit Versprechungen, Sie können ähnliche Aufgaben in Java mit Futures und einem Collector ausführen, der aufgerufen wird, wenn alle Aufgaben abgeschlossen sind und anschließend bereinigt werden. Es ist definitiv möglich, Asynchronität und sogar Abhängigkeiten zu bekommen, aber es erschwert die Dinge auch sehr.
Polygnome
3

Wenn Sie können, erlauben Sie dem Benutzer nicht, die Ressource zu erstellen. Lassen Sie den Benutzer ein Besucherobjekt an Ihre Methode übergeben, wodurch die Ressource erstellt, an die Methode des Besucherobjekts übergeben und anschließend freigegeben wird.

9000
quelle
Vielen Dank für Ihre Antwort, aber entschuldigen Sie, meinen Sie, dass es besser ist, Ressourcen an den Verbraucher weiterzuleiten und diese dann erneut abzurufen, wenn er um Service bittet? Dann bleibt das Problem bestehen: Der Verbraucher vergisst möglicherweise, diese Ressourcen freizugeben.
Hasanghaforian
Wenn Sie eine Ressource steuern möchten, lassen Sie sie nicht los und geben Sie sie nicht vollständig an den Ausführungsfluss einer anderen Person weiter. Eine Möglichkeit besteht darin, eine vordefinierte Methode für das Besucherobjekt eines Benutzers auszuführen. AutoCloseablemacht eine sehr ähnliche Sache hinter den Kulissen. Unter einem anderen Gesichtspunkt ähnelt es der Abhängigkeitsinjektion .
9000
1

Meine Idee war ähnlich wie die von Polygnome.

Mit den Methoden "do_something ()" in Ihrer Klasse können Sie einfach Befehle zu einer privaten Befehlsliste hinzufügen. Dann haben Sie eine "commit ()" - Methode, die tatsächlich die Arbeit erledigt und "release ()" aufruft.

Wenn der Benutzer also niemals commit () aufruft, ist die Arbeit nie erledigt. In diesem Fall werden die Ressourcen freigegeben.

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

}

Sie können es dann wie folgt verwenden: foo.doSomething.doSomethineElse (). Commit ();

Sava B.
quelle
Dies ist die gleiche Lösung, aber anstatt den Anrufer zu bitten, die Aufgabenliste zu pflegen, pflegen Sie sie intern. Das Kernkonzept ist buchstäblich dasselbe. Dies folgt jedoch keinen Codierungskonventionen für Java, die ich jemals gesehen habe, und ist tatsächlich ziemlich schwer zu lesen (Schleifenkörper in derselben Zeile wie Schleifenkopf, _ am Anfang).
Polygnome
Ja, das ist das Befehlsmuster. Ich habe nur einen Code niedergeschrieben, um einen möglichst kurzen Überblick über meine Überlegungen zu geben. Ich schreibe heutzutage meistens C #, wobei privaten Variablen häufig _ vorangestellt wird.
Sava B.
-1

Eine andere Alternative zu den genannten wäre die Verwendung von "intelligenten Referenzen", um Ihre gekapselten Ressourcen so zu verwalten, dass der Garbage Collector automatisch bereinigen kann, ohne Ressourcen manuell freizugeben. Ihr Nutzen hängt natürlich von Ihrer Implementierung ab. Lesen Sie mehr zu diesem Thema in diesem wunderbaren Blog-Beitrag: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references

Dracam
quelle
Dies ist eine interessante Idee, aber ich kann nicht erkennen, wie die Verwendung von schwachen Referenzen das Problem des OP löst. Die Serviceklasse weiß nicht, ob sie eine weitere doWork () - Anfrage erhalten wird. Wenn er seinen Verweis auf seine schweren Objekte freigibt und dann einen weiteren doWork () -Aufruf erhält, schlägt er fehl.
Jay Elston
-4

Für jede andere klassenorientierte Sprache würde ich sagen, legen Sie sie in den Destruktor, aber da dies keine Option ist, können Sie die Finalisierungsmethode überschreiben. Auf diese Weise wird die Instanz freigegeben, wenn sie außerhalb des Gültigkeitsbereichs liegt und der Speicherplatz nicht mehr ausreicht.

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

Ich bin mit Java nicht allzu vertraut, denke aber, dass dies der Zweck ist, für den finalize () vorgesehen ist.

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

Dar Brett
quelle
7
Dies ist eine schlechte Lösung für das Problem, aus dem Grund, warum Stephen in seiner Antwort sagt -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
Und selbst dann läuft der Finalizer vielleicht nicht ...
28.
3
Finalizer sind notorisch unzuverlässig: Sie können laufen, sie können nie laufen, wenn sie laufen, gibt es keine Garantie, wann sie laufen werden. Selbst die Java-Architekten wie Josh Bloch halten Finalizer für eine schreckliche Idee (Referenz: Effective Java (2nd Edition) ).
Diese Art von Problemen lassen mich C ++ mit seiner deterministischen Zerstörung und RAII vermissen ...
Mark K Cowan