Zmuś innych programistów do wywołania metody po zakończeniu pracy

34

W bibliotece w Javie 7 mam klasę, która świadczy usługi dla innych klas. Po utworzeniu instancji tej klasy usługi można wywołać kilka jej doWork()metod (nazwijmy ją metodą). Nie wiem więc, kiedy zakończy się praca klasy usług.

Problem polega na tym, że klasa usług używa ciężkich przedmiotów i powinna je zwolnić. Ustawiłem tę część w metodzie (nazwijmy ją release()), ale nie ma gwarancji, że inni programiści użyją tej metody.

Czy istnieje sposób zmuszenia innych programistów do wywołania tej metody po zakończeniu zadania klasy usług? Oczywiście mogę to udokumentować, ale chcę je zmusić.

Uwaga: Nie mogę wywołać release()metody w doWork()metodzie, ponieważ doWork() potrzebuje tych obiektów, gdy zostanie wywołana w następnej kolejności.

hasanghaforian
źródło
46
To, co robisz, jest formą czasowego połączenia i zwykle jest uważane za zapach kodu. Zamiast tego możesz zmusić się do wymyślenia lepszego projektu.
Frank
1
@Frank Zgadzam się, jeśli zmiana projektu warstwy podstawowej jest opcją, to byłby najlepszy zakład. Ale podejrzewam, że jest to opakowanie na czyjąś usługę.
Dar Brett
Jakiego rodzaju ciężkich przedmiotów potrzebuje Twoja usługa? Jeśli klasa Service nie jest w stanie sama zarządzać tymi zasobami automatycznie (bez konieczności czasowego łączenia), być może zasoby te powinny być zarządzane gdzie indziej i wprowadzane do usługi. Czy napisałeś testy jednostkowe dla klasy Service?
PRZYJEDŹ Z
18
Wymaganie tego jest bardzo „nie-Java”. Java jest językiem służącym do wyrzucania elementów bezużytecznych, a teraz pojawia się pewien rodzaj urządzenia, w którym wymaga się od programistów „czyszczenia”.
Pieter B
22
@PieterB dlaczego? AutoCloseablezostał stworzony do tego właśnie celu.
BgrWorker

Odpowiedzi:

48

Pragmatycznym rozwiązaniem jest stworzenie klasy AutoCloseablei zapewnienie finalize()metody zabezpieczającej (w razie potrzeby ... patrz poniżej!). Następnie polegasz na użytkownikach swojej klasy, którzy używają try-with-resource lub close()jawnie dzwonią .

Oczywiście mogę to udokumentować, ale chcę je zmusić.

Niestety, nie ma sposobu, 1 w języku Java, aby wymusić programista zrobić dobry uczynek. Najlepsze, co możesz zrobić, to wykryć nieprawidłowe użycie w statycznym analizatorze kodów.


Na temat finalistów. Ta funkcja Java ma bardzo niewiele dobrych przypadków użycia. Jeśli polegasz na finalizatorach, masz problem, że uporządkowanie może zająć bardzo dużo czasu. Finalizer będą działać tylko po GC decyduje, że obiekt nie jest już dostępne. Może się to nie zdarzyć, dopóki JVM nie wykona pełnej kolekcji.

Tak więc, jeśli problemem, który chcesz rozwiązać, jest odzyskanie zasobów, które trzeba wcześniej zwolnić , to przegapiłeś łódź, używając finalizacji.

I na wypadek, gdybyś nie miał tego, co mówię powyżej ... prawie nigdy nie jest właściwe używanie finalizatorów w kodzie produkcyjnym i nigdy nie powinieneś na nich polegać!


1 - Istnieją sposoby. Jeśli jesteś gotów „ukryć” obiekty usług przed kodem użytkownika lub ściśle kontrolować ich cykl życia (np. Https://softwareengineering.stackexchange.com/a/345100/172 ), to kod użytkownika nie musi dzwonić release(). Jednak interfejs API staje się bardziej skomplikowany, ograniczony ... i „brzydki” IMO. Ponadto nie zmusza to programisty do robienia właściwych rzeczy . Usuwa zdolność programisty do robienia złych rzeczy!

Stephen C.
źródło
1
Finalizatorzy uczą bardzo złej lekcji - jeśli to spieprzysz, naprawię to dla ciebie. Wolę podejście netty -> parametr konfiguracyjny, który instruuje fabrykę (ByteBuffer), aby losowo tworzyła z prawdopodobieństwem X% bajtów buforów za pomocą finalizatora, który drukuje lokalizację, w której ten bufor został pobrany (jeśli jeszcze nie został zwolniony). Tak więc w produkcji wartość tę można ustawić na 0% - tj. Bez narzutu, a podczas testowania i dev na wyższą wartość, taką jak 50% lub nawet 100%, w celu wykrycia wycieków przed wypuszczeniem produktu
Svetlin Zarev
11
i pamiętaj, że finalizatory nie są gwarantowane, aby kiedykolwiek działały, nawet jeśli instancja obiektu jest odśmiecana!
jwenting
3
Warto zauważyć, że Eclipse emituje ostrzeżenie kompilatora, jeśli AutoCloseable nie jest zamknięty. Spodziewałbym się również innych Java IDE.
meriton - podczas strajku
1
@jwenting W jakim przypadku nie działają, gdy obiekt jest GC? Nie mogłem znaleźć żadnego zasobu, który mógłby to wyjaśnić lub wyjaśnić.
Cedric Reichenbach
3
@ Voo Źle zrozumiałeś mój komentarz. Masz dwie implementacje -> DefaultResourcei FinalizableResourcektóra rozciąga się pierwszy i dodaje finalizatora. W produkcji używasz, DefaultResourcektóry nie ma finalizatora-> bez kosztów ogólnych. Podczas programowania i testowania konfigurujesz aplikację, FinalizableResourcektóra ma używać finalizatora, a zatem dodaje koszty ogólne. Podczas tworzenia i testowania nie stanowi to problemu, ale może pomóc zidentyfikować wycieki i naprawić je przed osiągnięciem produkcji. Jednak jeśli wyciek osiągnie PRD, możesz skonfigurować fabrykę, aby utworzyć X%, FinalizableResourceaby
zidentyfikować
55

Wydaje się, że jest to przypadek użycia AutoCloseableinterfejsu i try-with-resourcesinstrukcja wprowadzona w Javie 7. Jeśli masz coś takiego

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

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

wtedy Twoi konsumenci mogą go używać jako

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

i nie musisz się martwić wywołaniem jawnego, releaseniezależnie od tego, czy jego kod zgłasza wyjątek.


Dodatkowa odpowiedź

Jeśli naprawdę szukasz, aby nikt nie mógł zapomnieć o korzystaniu z tej funkcji, musisz zrobić kilka rzeczy

  1. Upewnij się, że wszystkie konstruktory MyServicesą prywatne.
  2. Zdefiniuj pojedynczy punkt wejścia do użycia, MyServicektóry zapewni, że zostanie on później wyczyszczony.

Zrobiłbyś coś takiego

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

Użyłbyś go jako

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

Początkowo nie polecałem tej ścieżki, ponieważ jest obrzydliwa bez lambd Java-8 i interfejsów funkcjonalnych (gdzie MyServiceConsumersię Consumer<MyService>pojawia) i jest dość imponująca dla twoich konsumentów. Ale jeśli potrzebujesz tylko tego, release()aby zadzwonić, zadziała.

walpen
źródło
1
Ta odpowiedź jest prawdopodobnie lepsza niż moja. Moja droga ma opóźnienie, zanim włączy się Garbage Collector.
Dar Brett
1
W jaki sposób upewniasz się, że klienci będą korzystać, try-with-resourcesa nie tylko omijać try, a tym samym przegapić closepołączenie?
Frank
@walpen W ten sposób występuje ten sam problem. Jak mogę zmusić użytkownika do korzystania try-with-resources?
hasanghaforian
1
@Frank / @hasanghaforian, Tak, ten sposób nie wymusza wywołania bliskości. Ma tę zaletę, że jest znana: programiści Java wiedzą, że naprawdę powinieneś upewnić się, że .close()jest wywoływany, gdy klasa się implementuje AutoCloseable.
walpen
1
Czy nie można sparować finalizatora z usingblokiem deterministycznej finalizacji? Przynajmniej w języku C # staje się to dość wyraźnym wzorcem dla deweloperów, którzy mają zwyczaj korzystania z zasobów, które wymagają deterministycznej finalizacji. Coś łatwego i nawyk jest kolejną najlepszą rzeczą, gdy nie możesz zmusić kogoś do zrobienia czegoś. stackoverflow.com/a/2016311/84206
AaronLS
52

tak, możesz

Możesz absolutnie zmusić ich do wydania i sprawić, że będzie w 100% niemożliwe do zapomnienia, uniemożliwiając im tworzenie nowych Serviceinstancji.

Jeśli zrobili coś takiego wcześniej:

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

Następnie zmień go, aby mieli do niego dostęp w ten sposób.

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

Ostrzeżenia:

  • Rozważ ten pseudokod, minęło wiele lat, odkąd napisałem Javę.
  • W dzisiejszych czasach mogą istnieć bardziej eleganckie warianty, być może zawierające AutoCloseableinterfejs, o którym ktoś wspomniał; ale podany przykład powinien zadziałać od razu po wyjęciu z pudełka i poza elegancją (która sama w sobie jest przyjemnym celem) nie powinno być żadnego ważnego powodu, aby to zmienić. Zauważ, że zamierza to powiedzieć, że możesz użyć AutoCloseablew swojej prywatnej implementacji; zaletą mojego rozwiązania nad korzystaniem z niego przez użytkowników AutoCloseablejest to, że znowu nie można go zapomnieć.
  • Będziesz musiał go dopracować w razie potrzeby, na przykład, aby wprowadzić argumenty do new Servicewywołania.
  • Jak wspomniano w komentarzach, pytanie, czy konstrukcja tego typu (tj. Podejmowanie procesu tworzenia i niszczenia a Service) należy do osoby dzwoniącej, czy nie, jest poza zakresem tej odpowiedzi. Ale jeśli zdecydujesz, że jest to absolutnie potrzebne, możesz to zrobić w ten sposób.
  • Jeden komentator podał te informacje dotyczące obsługi wyjątków: Książki Google
AnoE
źródło
2
Cieszę się z komentarzy do recenzji, więc mogę poprawić odpowiedź.
AnoE
2
Nie przegłosowałem, ale ten projekt może sprawić więcej kłopotów niż jest wart. Dodaje to dużo komplikacji i nakłada duże obciążenie na osoby dzwoniące. Programiści powinni być zaznajomieni z klasami typu try-with-resources, że używanie ich jest drugą naturą, gdy klasa implementuje odpowiedni interfejs, więc zazwyczaj jest wystarczająco dobra.
jpmc26
30
@ jpmc26, zabawnie wydaje mi się, że takie podejście zmniejsza złożoność i obciążenie dzwoniących, ponieważ w ogóle nie pozwala im myśleć o cyklu życia zasobów. Poza tym; OP nie zapytał „czy powinienem zmusić użytkowników ...”, ale „jak zmusić użytkowników”. A jeśli jest to wystarczająco ważne, jest to jedno rozwiązanie, które działa bardzo dobrze, jest strukturalnie proste (po prostu zawinięcie go w zamknięcie / uruchomienie), a nawet przeniesienie na inne języki, które mają również lambdas / procs / zamknięcia. Cóż, pozostawię osąd demokracji z SE; i tak już PO zdecydował. ;)
AnoE
14
Ponownie, @ jpmc26, nie jestem zbytnio zainteresowany dyskutowaniem, czy to podejście jest poprawne w przypadku każdego pojedynczego przypadku użycia. OP pyta, jak wymusić taką rzecz, a ta odpowiedź mówi, jak wymusić taką rzecz . Nie do nas należy decyzja, czy należy to zrobić w pierwszej kolejności. Pokazana technika jest narzędziem, niczym więcej, niczym innym i, jak sądzę, użytecznym w torbie z narzędziami.
AnoE
11
To jest poprawna odpowiedź, imho, jest to w zasadzie wzór dziury w środku
jk.
11

Możesz spróbować użyć wzorca poleceń .

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

}

Daje to pełną kontrolę nad pozyskiwaniem i uwalnianiem zasobów. Dzwoniący przekazuje tylko zadania do Twojej Usługi, a Ty sam jesteś odpowiedzialny za pozyskiwanie i czyszczenie zasobów.

Jest jednak pewien haczyk. Dzwoniący nadal może to zrobić:

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

Ale możesz rozwiązać ten problem, gdy się pojawi. Gdy wystąpią problemy z wydajnością i okaże się, że niektórzy dzwoniący to robią, po prostu pokaż im właściwy sposób i rozwiąż problem:

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

Możesz to dowolnie skomplikować, wdrażając obietnice dla zadań zależnych od innych zadań, ale późniejsze wydawanie rzeczy również staje się bardziej skomplikowane. To tylko punkt wyjścia.

Asynchronizacja

Jak wskazano w komentarzach, powyższe tak naprawdę nie działa z żądaniami asynchronicznymi. To jest poprawne. Ale można to łatwo rozwiązać w Javie 8 za pomocą CompleteableFuture , szczególnie w CompleteableFuture#supplyAsynccelu tworzenia indywidualnych przyszłości i CompleteableFuture#allOfzapewnienia uwolnienia zasobów po zakończeniu wszystkich zadań. Alternatywnie, zawsze można użyć Threads lub ExecutorServices do opracowania własnej implementacji kontraktów futures / obietnic.

Polygnome
źródło
1
Byłbym wdzięczny, gdyby zwolennicy zostawili komentarz, dlaczego uważają, że to źle, żebym mógł albo bezpośrednio zająć się tymi obawami, albo przynajmniej uzyskać inny punkt widzenia na przyszłość. Dzięki!
Polygnome
+1 głos pozytywny. To może być podejście, ale usługa jest asynchroniczna, a konsument musi uzyskać pierwszy wynik, zanim poprosi o następną usługę.
hasanghaforian
1
To tylko szablon od podstaw. JS rozwiązuje to z obietnicami, możesz robić podobne rzeczy w Javie z futures i kolektorem, który jest wywoływany, gdy wszystkie zadania są zakończone, a następnie czyszczone. Zdecydowanie możliwe jest uzyskanie w nim asynchronizacji, a nawet zależności, ale bardzo to komplikuje.
Polygnome
3

Jeśli możesz, nie zezwalaj użytkownikowi na tworzenie zasobu. Pozwól użytkownikowi przekazać obiekt gościa do metody, która utworzy zasób, przekaże go do metody obiektu gościa, a następnie zwolni.

9000
źródło
Dziękuję za odpowiedź, ale przepraszam, czy masz na myśli, że lepiej jest przekazać zasoby konsumentowi, a następnie otrzymać go ponownie, gdy poprosi o obsługę? Wtedy problem pozostanie: Konsument może zapomnieć o uwolnieniu tych zasobów.
hasanghaforian
Jeśli chcesz kontrolować zasób, nie puszczaj go, nie przekazuj go całkowicie komuś innemu. Jednym ze sposobów na to jest uruchomienie predefiniowanej metody obiektu użytkownika. AutoCloseablerobi bardzo podobne rzeczy za kulisami. Pod innym kątem jest podobny do wstrzykiwania zależności .
9000
1

Mój pomysł był podobny do Polygnome.

Możesz mieć metody „do_something ()” w swojej klasie, po prostu dodając polecenia do prywatnej listy poleceń. Następnie zastosuj metodę „commit ()”, która faktycznie działa, i wywołuje „release ()”.

Jeśli więc użytkownik nigdy nie wywoła metody commit (), praca nigdy nie zostanie wykonana. Jeśli tak, zasoby zostaną uwolnione.

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

}

Następnie możesz użyć go jak foo.doSomething.doSomethineElse (). Commit ();

Sava B.
źródło
Jest to to samo rozwiązanie, ale zamiast poprosić osobę dzwoniącą o utrzymanie listy zadań, zachowujesz ją wewnętrznie. Podstawowa koncepcja jest dosłownie taka sama. Ale to nie jest zgodne z żadnymi konwencjami kodowania dla Javy, jakie kiedykolwiek widziałem i jest w rzeczywistości dość trudne do odczytania (treść pętli w tej samej linii co nagłówek pętli, _ na początku).
Polygnome
Tak, to wzór poleceń. Po prostu odłożyłem trochę kodu, aby streścić to, o czym myślałem, w możliwie zwięzły sposób. W dzisiejszych czasach piszę głównie C #, gdzie zmienne prywatne są często poprzedzone _.
Sava B.
-1

Inną alternatywą dla tych wymienionych byłoby użycie „inteligentnych referencji” do zarządzania enkapsulowanymi zasobami w sposób, który umożliwia automatycznemu czyszczeniu śmieci bez ręcznego zwalniania zasobów. Ich przydatność zależy oczywiście od wdrożenia. Przeczytaj więcej na ten temat w tym cudownym poście na blogu: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references

Dracam
źródło
To ciekawy pomysł, ale nie widzę, jak użycie słabych referencji rozwiązuje problem PO. Klasa usługi nie wie, czy otrzyma kolejne żądanie doWork (). Jeśli zwolni swoje odwołanie do swoich ciężkich obiektów, a następnie otrzyma kolejne wywołanie doWork (), zakończy się niepowodzeniem.
Jay Elston,
-4

W przypadku każdego innego języka zorientowanego na klasy umieściłbym go w destruktorze, ale ponieważ nie jest to opcja, myślę, że można zastąpić metodę finalizacji. W ten sposób, gdy instancja wyłączy się z zakresu i zostanie wyrzucona, zostaną zwolnione.

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

Nie znam się zbytnio na Javie, ale myślę, że jest to cel, dla którego przeznaczona jest finalize ().

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

Dar Brett
źródło
7
To złe rozwiązanie problemu z tego powodu, że Stephen mówi w swojej odpowiedzi -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
I nawet wtedy finalizator może tak naprawdę nie działać ...
jwenting
3
Finalizatory są notorycznie niewiarygodne: mogą działać, nigdy nie mogą działać, jeśli działają, nie ma gwarancji, kiedy będą działać. Nawet architekci Java, tacy jak Josh Bloch, twierdzą, że finalizatory to okropny pomysł (odniesienie: Effective Java (2nd Edition) ).
Tego rodzaju problemy sprawiają, że tęsknię za C ++ z jego deterministycznym zniszczeniem i RAII ...
Mark K Cowan