Java9 Cleaner świetną ilustracją enkapsulacji

pawelwlodarski.blogspot.com 6 lat temu

10 lat temu w Effective java mogliśmy przeczytać by nigdy nie używać finalize. Ja się zastosowałem i tylko raz niechcący w ferworze refaktoringu przezwałem jakąś metode na finalize i zaczęły dziać się śmieszne rzeczy. Latka minęły i świeżo na półeczki księgarni informatyka wyszło trzecie wydanie Effective java z apdejtem do Java 9. No i w tej najnowszej Javie (najnowszej jeszcze tylko 2 miesiące!) w rozdziale o finalize pojawia się nowy mechanizm java.lang.ref.Cleaner

Koniec końców Joshua Bloch napisał, iż ten Cleaner tez zjebany ale ogólniej mniej i jest taki jeden kejs gdzie go bez strachu użyć można. No i tak się składa, iż moim zdaniem ten przykład użycia jednocześnie niesie ze sobą ogromną wartość edukacyjną jak adekwatnie enkapsulować stan w klasie. A do tego możemy sobie zobaczyć jak inne języki na JVM poradzą sobie ze standardową sytuacją w Javie.

Przez chwile miałem wątpliwości czy aby temat będzie dla ogółu interesujący ale potem sobie przypomniałem, iż to mój blog i będę tutaj pisał co mi się chce.

Dobra Enkapsulacja

Na poczatku wytniemy kawałeczki kodu by łatwiej było zauważyć wspomnianą enkapsulację i ogólnie pojęte ukrywanie informacji, które bardzo pomaga w utrzymaniu i rozbudowie systemu. jeżeli ktoś chce brzmieć bardziej Fancy to może używać terminu anglojęzycznego https://en.wikipedia.org/wiki/Information_hiding. jeżeli ktoś chce brzmieć bardziej biznesowo to może powiedzieć, iż informejszyn hajding redukuje ołweral meintenance cost and reduces tajm to market for fiuczer rilises.

public class InstanceAroundResource implements AutoCloseable{
//We will delegeta cleaning to Cleaner from Java 9
private static final Cleaner cleaner=Cleaner.create();

//This is definition of internal state, static -> so it has no ref to external instance
//private - to better hide information
private static class EncapsulatedResource implements Runnable{
(...)
}

//no getters for those two fields, no strange hakiers annotations either
private final EncapsulatedResource state;

//this triggers cleaning procedure
private final Cleaner.Cleanable cleanable;

public InstanceAroundResource(String resourceId) {
//notice that both instances are created inside constructor , no direct assignment,
//no information how resourceId is escapes outside
//compare this with stntaxt 'this.field = field' which is hiding information like in the sentence
// "think about number seven but don't tell what the number is"
this.state = new EncapsulatedResource("[opened :"+resourceId+"]");
this.cleanable = cleaner.register(this, state);
}

// here we are connecting Cleaner with Autocloseable from Java7
@Override
public void close() throws Exception {
cleanable.clean();
}
}

I teraz tak, po pierwsze primo od javy 9 mamy import do dyspozycji:

import java.lang.ref.Cleaner;
i idąc dalej :
  1. Zauważcie, iż nic z tej klasy nie ucieka poza abstrakcyjnem resourceId przekazanym do konstruktora - to jest niejako kawałek publicznego interfejsu.
  2. Drugim kawałkiem publicznego interfejsu jest - no własnie interfejs - Autocloseable , czyli obietnicę posiadania metody/zachowania close
  3. Totalnie ukryliśmy przed użytkownikiem użycie Cleanera. Nie ma ani dziwnych ch** wie po co getterów setterów.
  4. Jak Bloch przykazał pola są finalne - przynajmniej pod katem referencji wnętrze jest niezmienne.
  5. Ukryliśmy przed światem zewnętrznym istnienie klasy EncapsulatedResource co daje nam pełne pole do refaktoringu tego kawałka kodu w przyszłości (i minimalizacji tajm to market)
  6. Jeszcze raz podkreslmy, iż parametry konstruktora również nie zdradzają za wiele o bebechach.
  7. Sam Cleaner z tego co zrozumiałem jest lepszy od starej metody finalize pod tym katem, iż delegując czyszczenie do Cleanera - który nasłuchuje (listenerek taki) - nie będzie problemu z wyjątkami przy sprzątaniu bo Cleaner ma pełną kontrolę nad tym watkiem/procedurą i ogarnia. Tak przynajmniej napisał autor i ja mu wierzę!

I cały kod

import java.lang.ref.Cleaner;

public class InstanceAroundResource implements AutoCloseable{
private static final Cleaner cleaner=Cleaner.create();

//static to not have reference to external instance
private static class EncapsulatedResource implements Runnable{

String handleToSystemResource; //don't need to be private because EncapsulatedResource is private

EncapsulatedResource(String handleTosystemResource) {
this.handleToSystemResource = handleTosystemResource;
}

@Override
public void run() {
System.out.println("Closing system resource by cleaner :"+handleToSystemResource);
handleToSystemResource="CLOSED";
}
}

private final EncapsulatedResource state;

private final Cleaner.Cleanable cleanable;

public InstanceAroundResource(String resourceId) {
this.state = new EncapsulatedResource("[opened :"+resourceId+"]");
this.cleanable = cleaner.register(this, state);
}

@Override
public void close() throws Exception {
System.out.println("First In Auto-Closable");
cleanable.clean();
}
}

Zerknijmy teraz na odpalenie tego :

//1
try(InstanceAroundResource r = new InstanceAroundResource("BLOG-POST")){
System.out.println("Using resource1");
}

//2
new InstanceAroundResource("UNHANDLED-RESOURCE");
System.out.println("r2 left alone");
//System.gc();

sytuacja nr 1 prosta bo na końcu zostanie wykonane close i mamy


Using resource1
First In Auto-Closable
Closing system resource by cleaner :[opened :BLOG-POST]
Co do drugiej sytuacji to podobnie jak u autora książki także i u mnie backup nie zadziałał bez sztucznego wywołania System.gc()
r2 left alone
Closing system resource by cleaner :[opened :UNHANDLED-RESOURCE]

A Java 10 ?

W Javie 10 zadziała już poniższe :

jshell> try(var r=new InstanceAroundResource("Java10")){
...> System.out.println("Doing something in java 10");
...> }

Doing something in java 10
First In Auto-Closable
Closing system resource by cleaner :[opened :Java10]

Czyli już nie trzeba deklarować nowego InstanceAroundResource z zaznaczeniem iż jest typu InstanceAroundResource. Mniej czytania, inżynier może przeznaczyć moc operacyjną muzgu na redukcję tajm to market.

Inne Języki

Kotlin

Kotlin ma coś takiego :

public inline fun <T : AutoCloseable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
this.closeFinally(exception)
}
}

Co oznacza, iż mechanizmem "extends method" dorzuci metodę use do wszystkiego co rozszerza AutoCloseable. Także zadziała to i z naszym - napisanym w Javie - InstanceAroundResource.

val r=InstanceAroundResource("KOTLIN-EXAMPLE")

r.use { r: InstanceAroundResource ->
println("Doing something in kotlin")
}

I to w sumie w niektórych kręgach półświatka programistycznego można nazwać metodą/funkcją wyższego rzędu. Wynik będzie analogiczny z przykładem Javowym :
Using resource1
First In Auto-Closable
Closing system resource by cleaner :[opened :BLOG-POST]

No i analogie idzie dalej, jeżeli chcemy by cleaner zadziałał jako backup trzeba manualnie gc wywołać :

InstanceAroundResource("UNHANDLED-IN-KOTLIN")
println("r2 left alone")
System.gc()

A co z nullem

Ogarnianie nulli z javy to jeden z lepszych fiuczerów Kotlina, zerknijmy czy jebnie coś takiego :

val r3:InstanceAroundResource? = null
r3.use { r: InstanceAroundResource? -> println("jebnie?") } //nie jebnie

No i po pierwsze nie da się łatwo oszukać kompilatora, iż null jest typu InstanceAroundResource (ten taki znak zapytania na końcu) a po drugie to nie jebnie. Jak zerkniesz jeszcze raz na implementacje "use" to zobaczysz , ze w finally nie ma "close" a jest " this.closeFinally(exception)". I to wygląda mniej więcej tak : internal fun AutoCloseable?.closeFinally(cause: Throwable?) = when {
this == null -> {}
cause == null -> close()
else ->
try {
close()
} catch (closeException: Throwable) {
cause.addSuppressed(closeException)
}
}

Czyli takie zamknięcie-otwarcie RISORSA nie jest rzeczą prostą. I całe szczęście, iż Java i Kotlin mają gotowce na to w standardowej bibliotece. I tutaj też zwróć uwagę, iż Java natywnie na poziomie języka a kotlin zaimplementowane przy pomocy bardziej abstrakcyjnego natywnego mechanizmu - "rozszerzalnych metod". Zaraz zobaczymy, ze to może iść jeszcze dalej.

Scala

I teraz tak : wiem, iż google jakoś segreguje wyniki wyszukiwania na podstawie ciasteczek i mogą być dla wszystkich unikalne ale jak wpisałem w googla własnie "scala try with resources " to kurcze same tutoriale wyskakują jak samemu napisać a nie byłem w stanie jakiejś natywnej konstrukcji odnaleźć.

jak ktoś zna to niech da namiar bo inaczej ryzykujemy w każdym projeckie "not inwented here" z pytaniem "a w której paczce mamy zaimplementowany loan-pattern, no wiesz ten try z javy?". Jest biblioteczka https://github.com/jsuereth/scala-arm ale tez jakos nie jest dobrze na googlu spozycjonowana a znalazłem ją bo już o niej kiedyś słyszałem. W każdym razie jak już biblioteczkę mamy to dalej kod wygląda bardzo podobnie:

val r=new InstanceAroundResource("SCALA-EXAMPLE")

val mr: ManagedResource[InstanceAroundResource] = managed(r)

mr.foreach{r=>
println("Doing something in scala")
}

new InstanceAroundResource("UNHANDLED-IN-SCALA")
println("r2 left alone")
System.gc()

Doing something in scala
First In Auto-Closable
Closing system resource by cleaner :[opened :SCALA-EXAMPLE]
r2 left alone
Closing system resource by cleaner :[opened :UNHANDLED-IN-SCALA]

Poziomy abstrakcji mechanizmów językowych

Obczajcie to :

  • Java ma try-with-resources
  • Kotlin ma extension method przy pomocy, którego można zaimplementować try-with-resources i inne rozszerzenia
  • Scala ma implicity przy pomocy których można zaimplementować extension method i inne meta-rozszerzenia przy pomocy których można zaimplementować try-with-resources i inne rozszerzenia
No tylko, iż im bardziej ogólny mechanizm tym trudniej go komuś wytłumaczyć i dłuższa edukacja - try-with-resources jest banalne w użyciu a teraz porównaj to z ogarnianiem implicit class i jakichś optymalizacji jak rozszerzanie AnyVal. Także coś za coś

Linki

Idź do oryginalnego materiału