niedziela, 21 stycznia 2018

Java9 Cleaner świetną ilustracją enkapsulacji

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ł, że 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, że moim zdaniem ten przykład użycia jednocześnie niesie ze sobą ogromną wartość edukacyjną jak właściwie 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, że 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śli ktoś chce brzmieć bardziej Fancy to może używać terminu anglojęzycznego https://en.wikipedia.org/wiki/Information_hiding. Jeśli ktoś chce brzmieć bardziej biznesowo to może powiedzieć, że 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, że 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, że 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, że 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, że 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śli chcemy by cleaner zadziałał jako backup trzeba ręcznie 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, że 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, że Java i Kotlin mają gotowce na to w standardowej bibliotece. I tutaj też zwróć uwagę, że 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, że google jakoś segreguje wyniki wyszukiwania na podstawie ciasteczek i mogą być dla każdego 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, że 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

sobota, 6 stycznia 2018

Nauka Mechanizmów Programowania

... a nie tylko konkretnych języków. Taki oto temat dzisiejszego wypracowania. Można by rzec, że mechanizmów starych ale subiektywnie nowych dla ludzi, którzy siedzą w Javie. Przed Javą 8 warto było uczyć się mechanizmów z innych języków na JVM by być przygotowanym na nadejście funkcji. Teraz może warto pochylić się nad innymi elementami, których w Javie nie uświadczysz (albo napotkasz w formie ubogiej) jak 'declaration site variance' , wykrywanie typów czy modelowanie 'unii' przy pomocy słówka 'sealed'.

Narzędzie edukacyjne - by zmotywować uczestników - musi nosić jakieś znamiona przydatności na rynku lokalnym bo spotkania wokół Haskella czy bardziej zaawansowanego wykorzystania FP w Scali zainteresowały jedynie ograniczone grono. Scala jest fajna i bogata w przeróżne mechanizmy ale trudno zmotywować ludzi do jej nauki kiedy nie ma szans zrobić nań pieniądza. I tu pojawia się coś pomiędzy Scalą i Java czyli Kotlin.

Taka ma teoria, że ludzie z JetBrains pracując latami nad wsparciem scali nauczyli się dobrze, które ficzery łatwo wspierać w IDE a co lepiej usunąć. I tak w sumie jak ktoś przejdzie od Javy do Kotlina to z Kotlina dużo bliżej do Scali (jakby ktoś chciał). Język jest 'wystarczająco przyjemny' w porównaniu z Javą i ma wiele wygodnych mechanizmów, które występują w scali a do tego to już nie tylko Android ale różne narzędzia i inne Springi zaczynają go wspierać.

No to po kolei...

Plan Nauki

Na razie wbite są dwa meetupy

  1. Wprowadzenie do FP i OOP - https://www.meetup.com/Java-User-Group-Lodz/events/246218922/. To wprowadzenie należy rozumieć jako poznanie składni aniżeli jakiejś abstrakcyjnej teorii. Jak Kotlin się kompiluje do Javy, jak rozmawia z Javą a przede wszystkim o ile mniej pracy trzeba w tym języku włożyć w zadeklarowanie prostej klasy czy funkcji.
  2. System typów - https://www.meetup.com/Java-User-Group-Lodz/events/246378280/ - Kolejna rzecz trochę podobna do Scali czyli Niedziurawy system typów z Any na górze i Nothing na dole - do tego dochodzą interesujące Unie Typu i Nulla , które w końcu mogą rozwiązać problem porozrzucanych nullpointerexceptionów po kodzie

I co dalej :

  • Dużo ciekawszy mechanizm generyków, declaration site variance,covariance,contravariance i konsekwencje, o których pisałem rok temu : kowariantne konsekwencje
  • Kompozycja funkcji o niebo wygodniejsza ze względu na obecność prostych typów funkcji jak Int -> Int zamiast Function<Int,Int>, currying i takie tam podobne
  • Pattern matching, funkcje częściowe i całkowite i dlaczego to ważne. No i ADT czyli Algebraiczne typy Danych
  • Tutaj może wejdzie bardziej zaawansowane FP z bibliotekami https://github.com/MarioAriasC/funKTionale oraz Kategory przemianowane na Arrow
  • No i w międzyczasie może porobimy jakieś Code Retreat czy coding kata czy jakieś ćwiczenia w ten deseń

Także sytuacja będzie się rozwijać i jak ktoś jest zainteresowany niech śledzi meetupa.

W tym miejscu artykułów zazwyczaj występuje zakończenie także i tutaj nie może go zabraknąć.