niedziela, 3 kwietnia 2016

Patrz Panie jakie fajne wstrzykiwanie

To co widzicie poniżej to stos.

Zanim stos został użyty w programowaniu do przechowywania argumentów funkcji służył jako ostateczne narzędzie do wygrywania sporów. Np. jak ktoś zadawał niewygodne pytania :

- ziemia jest płaska i leży na 4 słoniach!
- a na czym stoją te słonie?
- na ogromnym żółwiu
- a ten żółw na czym stoi?
- a tam już jest piekło!
- no ale jak ten żółw stoi tam na tym piekle?
- a co ciebie interesuje tak to piekło?!?! Diabeł ludzie diabeł! na stos z nim !!

I w ten sposób kolejna dyskusja na forum została wygrana a mainstream nie zmieniał się przez kilkaset lat.

Niestety w programowaniu mamy także zestaw niepodważalnych reguł w których kontekście się dyskutuje ale "o których się nie dyskutuje". I tak generalnie tezą, za którą można stracić miejsce w lokalnym klubiku szachowo-obiektowym byłoby "Bo czasem jak ustawisz pole jako 'public final' - no wiesz tak bez getera to też będzie dobrze a czasem nawet lepiej".

Tydzień temu starałem się pokazać ( Tutaj jest wpis sprzed tygodnia) , że metody można nie tylko wywoływać na instancji przekazując argument - co jest żelaznym ograniczeniem w klasycznej Javie - ale również można wywołać akcję wpierw określając argumenty a później decydując się jakiej instancji ma to wywołanie dotyczyć. Taka operacja otwiera przed nami szereg nowych możliwości, które jeśli ktoś ciekaw są opisane w tamtym artykule.

Dzisiaj z kolei zobaczymy co się stanie jeśli złamiemy założenie najpierw wstrzykuję zależność a później ją wykorzystuję. Dokładnie - najpierw wykorzystamy zależność a na samym końcu określimy czym ta zależność jest. Mam nadzieję, że już trochę ryje czaszkę i zachęca zdanie poprzednie do dalszej lektury.

Klasycznie

W "klasycznym" wstrzykiwaniu będziemy mieli jakiś serwis (być może z 10 adnotacjami) do którego będzie wstrzyknięte repozytorium (być może też z 10 adnotacjami). I zrobimy sobie jakąś taką małą symboliczną domenkę w kontekście, które jest użytkownik i on może mieć status

I własnie taką akcją mała i symboliczną na potrzebę tego artykułu będzie zmiana statusu z NEW na CONFIRMED

No i standardowo serwis przyjmie żądanie, wyciągnie usera, coś w nim zmieni i zapisze. Rezultatem zaś będzie id nowej wersji usera .Kodzik - po raz kolejny podkreślę, że na potrzeby edukacji symboliczny - może wyglądac tak:

case class UserId(id:Int) extends AnyVal
case class User(login:String,status:String)

trait Repository{
    def find(id:UserId):User
    def save(u:User):UserId  
}

class Service(repository:Repository){
    def confirUser(id:UserId):UserId={
      val user = repository.find(id)
      val confirmed = user.copy(status = "CONFIRMED")
      repository.save(confirmed)
    }
}

Teraz tak chwilę zastanówmy się czym charakteryzowała będzie się praca z tym kodem i czy pewne aspekty tej pracy nie będą nam przeszkadzać :

  • Test będzie wymagał zmockowania Repozytorium, ustawienia, że ma zwracać takiego a takiego usera i będziemy musieli jakoś przechwycić argument, który poszedł do save by sprawdzić, że status faktycznie jest zmieniony na "CONFIRMED"
  • Chociaż możemy kawałek ze zmianą na "CONFIRMED" wynieść do osobnej metody, która przetestujemy (w sumie dobry pomysł) - to jednocześnie nie da się łatwo sparametryzować tego szkieletu :

    1) repository.find(id)
    2) Przekształć Usera <- ten kawałek chcę sobie skonfigurować
    3) repository.save(newUser)
  • Nawet można by pójść z ta "Kastomizacją" krok dalej

    1) Zrob se cos na repository i dostaniesz usera <- a co dokładnie to pomyśle później
    2) Przekształć Usera <- a co dokładnie to pomyśle później
    3) I zrób se cos z tym nowym Userem na Repozytorium <- iiiiii zgadnij, tak przekładam tę decyzję w kodzie na chwile gdy będę miał więcej informacji

Dobra popili, pojedli to teraz ...

Funkcyjnie ale nada klasycznie

Gdybyśmy chcieli zapisać wspomniane obliczenia jako typ funkcji to było by coś w stylu Repository => UserId => User czyli najpierw ustawiam repozytorium i dostaje funkcję, która userId przekształci w Usera.

  //KLASYKA
val findUserClassic:Repository => UserId => User = repository => id => repository.find(id)
val injectedRepository = findUserClassic(MockRepository)
val user = injectedRepository(UserId(1))

No i fajnie. Na tym można by skończyć pisanie posta z wnioskiem "w FP też można DI!" i w sumie pogoda jest ładna a siłka też otwarta to lecimy. ALE... ale teraz dopiero zaczną się dziać ciekawe rzeczy...

Złamać Intuicje

Odwróćmy ciąg przyczynowo skutkowy :

val findUser : UserId=>Repository=>User = id=>repository=>repository.find(id)

Czy dokładnie widać co się stało? Na wszelki wypadek jeszcze rysunek.

OTÓŻ - Wstrzyknęliśmy argument zanim jeszcze wstrzyknęliśmy repozytorium! Oczywiście powstaje pytanie - "ale po co?"

Za chwile zobaczymy jak niezwykle ciekawe możliwości otwiera przed nami ten zabieg ale jeszcze dla kwestii edukacyjnych wprowadźmy taki wygodny alias dla funkcji Repository => A

type Reader[A] = Repository=>A
//albo bardziej "javowo"  type Reader[A] = Function[Repository,A]

Przyjść na gotowe

No dobra pierwsza obserwacja - kiedy najpierw wstrzykiwaliśmy repozytorium to w testach niejako musieliśmy tworzyć Serwis od nowa z Mockami. Teraz w zasadzie wszystkie parametry obliczenia są określone poza jedynym który może się wywalić czyli Repozytorium. Dlaczego "jedynym, który może się wywalić" ? Ponieważ repozytorium wygląda na jedyny moment kodu gdzie łamane jest "Referential Transparency" czyli gdzie wynik obliczeń jest zależny od jakiegoś stanu zewnętrznego.

A ponieważ dostaliśmy formę Repository=>A to potencjalna zjebka jest odłożona w czasie do momentu decyzji jak ten efekt uboczny najlepiej ogarnąć.

//wczesniejszy kawąłek kodu - tutaj raczej wszystko jest pod kontrolą
val findUser1: (Repository) => User = findUser(UserId(1))

//późniejszy kawałek kodu, tutaj postanawiamy robić test to wstzrykniemy sobie Mock
findUser1(MockRepository)

//ale równie dobrze już działac produkcyjnie
findUser1(ProductionRepository)

No i fajnie ale nadal to wygląda jak taki trochę bajer tylko. Dobra to idziemy dalej...

Operacje domenowe

Mamy taką sobie skromną operację domenową, o której wspominaliśmy wcześniej czyli ustawienie usera na "CONFIRMED"

val confirmUser:User=>User= u => u.copy(status = "CONFIRMED")


(moglibyśmy napisać "mamy taką małą łatwą w użyciu funkcje" ale jak odmieniamy słowo "Domena" i "Biznesowe" to jest tak no bardziej profeszional)

No i teraz możemy podejść do problemu, który poruszyliśmy przy okazji implementacji "klasycznej" - czyli kompozycji dwóch niezależnych od siebie kawałków kodu. Ponieważ pierwsza funkcja ma typ Repository=>User a druga User => User to można by zrobić standardowe andThen lub compose jednakże tutaj przygotujemy pewną funkcję "mapującą", która przyda się później.

def mapR[B,C](reader:Reader[B])(f:B=>C): Reader[C] =  reader andThen f
def mapf[A,B,C](base:A=>B)(f:B=>C): A=>C =  base andThen f


  • mapR[B,C] - to jest sygnatura funkcji, która wykorzystuje nasz alias type Reader[A] = Repository=>A
  • mapf[A,B,C] - to dla porównania bardziej generyczna funkcja, która nie jest ograniczona do Readera i może zmapować dowolną funkcję A=>B do A=>C

Poniższa funkcja confirmUser1 już nie tylko czyta usera o id=1 z repozytorium, które jeszcze nie jest określone ale już ustawia status na "CONFIRMED".

val confirmUser1: (Repository) => User = mapf(findUser1)(confirmUser)

Możemy testować niezależnie :
  • funkcję czytającą Usera
  • funkcję ustawiajaca status "CONFIRMED"
  • funkcję wyższego rzędu mapującą jedną funkcję prostą przy pomocy innej funkcji prostej
I w zasadzie testując te trzy składniki mamy pewność, że ich kompozycja również działa : mapf(findUser(UserId(1)))(confirmUser). Ale to cały czas jest naciągane bo ten sam efekt można również uzyskać wstrzykując klasycznie najpierw repo a później argument. Otóż siła tej konstrukcji pojawi się w sytuacji gdy ponownie będziemy chcieli użyć Repozytorium do zapisu.

FLAT MAP

Będziemy chcieli zrobić coś takiego :

I tutaj może być taki trochę MINDFUCK moment. Generalnie flatMap ma sygnaturę A=>F[B] gdzie F to będzie jakiś typ wyższego rzędu np A=>List[B] albo A=>Option[B]. Więc w naszym przypadku to będzie A=>Reader[B] a ponieważ Reader[B] to funkcja Repository=>B to flatMap w tym przypadku (teraz będzie ten moment) A=>Repository=>B

def flatMapR[B,C,D](reader:Reader[B])(f:B=>Reader[C]) : Reader[C] = a=> f(reader(a))(a)
def flatMapf[A,B,C,D](base:A=>B)(f:B=>(A=>C)) : A=>C = a=> f(base(a))(a)

I znowu mamy dwie funkcje do porównania. Generyki w wersji z Readerem oznaczyłem od B aby zachować podobieństwo. Jaka funkcja spełnia ten warunek : f:B=>Reader[C]. Jedną już widzieliśmy :

val findUser : UserId=>Repository=>User = id=>repository=>repository.find(id)

To teraz czas na kolejną :

val persistUser : User=>Repository=>UserId = user => repository => repository.save(user)

I teraz możemy sobie skomponować caluśką operacyję :

val databaseOperation: (Repository) => UserId = flatMapf(mapf(findUser(UserId(1)))(confirmUser))(persistUser)
databaseOperation(MockRepository) //nowe Id zapisanego usera

Ktoś może powiedzieć, że to wygląda trochę nieczytelnie. Będzie on w błędzie bo to wygląda bardzo nieczytelnie ale mamy na to patent!

Pora na FORa

Teraz zaznamy słodyczy języka Scala. Najpierw w postaci niejawnej konwersji :

implicit class FunctionTransformation[A,B](base:A=>B){
    def map[C](f:B=>C):A=>C = base andThen f
    def flatMap[C](f:B=>A=>C):A=>C = a=>f(base(a))(a)
}

która umożliwi taki zapis :
val program: (Repository) => UserId = findUser(UserId(1)).map(confirmUser).flatMap(persistUser)
program(MockRepository)

Przy minimalnej znajomości składni scali ten kod już jest czytelny ale można jeszcze (być może) lepiej.

val forResultProgram: (Repository) => UserId = for{
      user <-findUser(UserId(1))
      confirmed=confirmUser(user)
      newId <- persistUser(confirmed)
} yield newId

forResultProgram(MockRepository)

I znowu przy odpowiedniej znajomości składni scali wiadomo co tam się dzieje. Przy braku znajomości składni scali to nic nie będzie czytelne ale to już nie problem języka...

Wyważanie otwartych drzwi

Nie trzeba samemu tworzyć tej niejawnej (implicit) konwersji dodającej (map i flatMap) do funkcji. Można się wspomóc biblioteką :

import cats.std.all._
import cats.syntax.functor._
import cats.syntax.flatMap._

val catsResult: (Repository) => UserId = for{
      user <-findUser(UserId(1))
      confirmed=confirmUser(user)
      newId <- persistUser(confirmed)
} yield newId

Separacja definicji od wykonania

Na to co do tej pory robiliśmy, można także spojrzeć z zupełnie innej strony. W sumie zbudowaliśmy działającą kompozycje funkcji, która jest wyzwalana poprzez podanie parametru repozytorium. Czyli w jednym miejscu zdefiniowaliśmy "co" ma się stać (logika przekształceń) a w innym "jak" to ma się stać (produkcyjne repo/ testowe repo).

Funkcje częściowe w przebraniu - źródło wszelkich bugów

Jets taki fajny cytat, że bugi/błędy pojawiają się wtedy gdy funkcję częściową traktujemy jak całkowitą. No bo jakby dziwnie to nie zabrzmiało program z błędami to funkcja częściowa, która jest określona poza wszystkimi miejscami w których się wypie***la.

Pytanie co zrobić z sytuacją gdy chcemy jakoś zaznaczyć, że czytanie/zapisywanie w repo może się popsuć. Cóż pamiętajmy, że to zajebistego w funkcjach jest, że to jeden ujednolicony interfejs i bardzo łatwo go komponować w bardziej wyrafinowane konstrukcje.

Dlatego też prostym i szybkim rozwiązaniem ale nie koniecznie najlepszym może być :

def hasSideEffect[A,B](f:A=>B): A=>Option[B]= a=> try{
     Some(f(a))
}catch{
     case e:Throwable => None
}

val programWithEffects: (Repository) => Option[UserId] = hasSideEffect(forResultProgram)

Jak mówiłem szybkie ale nie koniecznie najlepsze. Tak czy inaczej w kontekście całego artykułu - być może warto się czasem zastanowić, które założenia w naszym rozumieniu świata to faktycznie "stałe", a które to "leniwe zmienne", do których się przyzwyczailiśmy i nie chce nam się pomyśleć "a co by było gdyby..."

5 komentarzy:

  1. Fajnie byłoby zobaczyć jakiś większy przykład, ja się niestety nie mogę przekonać do Reader monad (chociaż to pewnie wynik mojej niewiedzy, nie wady tego podejścia) z dwóch powodów:
    - jako klient serwisu muszę wiedzieć jakich zależności on wymaga (i jakich zależności wymagają serwisy które on woła, itd.)
    - wszystkie zależności trzeba wrzucić do jednego worka i przekazywać wszędzie, przez co zawsze mogę użyć dowolnej zależności. Można zawężac kontekst, ale wtedy dodanie zależności może wymagać zmian w paru warstwach (klientach)

    OdpowiedzUsuń
  2. cześć, ogólnie temat jest zainspirowany książką https://www.manning.com/books/functional-and-reactive-domain-modeling gdzie jest większy przykład jakkolwiek to cały czas książka.

    Natomiast co do punktów to może coś takiego :

    - jako klient serwisu nadal możesz tylko i wyłącznie przekazywać argumenty ustalając pierwszy kawałek wywołania funkcji a dalsza cześć czyli Repository=>A to już może być taki mechanizm wewnątrz samego serwisu, którego klient nie jest świadom.

    Czyli np wołasz Serwis.find(userId) i z zewnątrz nie widać, że tam jest jakieś repozytorium. Później taka funkcja "Repository=>A" już z zaapalikowanym userId może być wysłana do osobnego serwisu/komponentu.

    Gdzie to może być dobre? Teraz mam nadzieję trochę odpowiedzi na być może twoje drugie pytanie. Jeśli np. pojawi się sytuacja, że mamy jakąś konfigurację i zauważamy, że dosyć dużo przekazujemy jej pomiędzy komponentami to może wygodne wtedy będzie "odwrócenie kota ogonem" i po prostu działanie na takiej abstrakcji "ktoś gdzieś w przyszłości wstrzeli tutaj konfigurację"

    I to co jest dla mnie najważniejsze to nie jest tak, że ten sposób jest "lepszy zawsze" tylko to jest wariant wstrzykiwania, który do niedawna nawet nie przyszedł mi do głowy. Także spokojnie według mnie należy to rozpatrywać po prostu jako "wariant" i spokojnie kilka wariantów może działać obok siebie maksymalizując korzyść (chociaż fraza "maksymalizując korzyść" pasuje do prezentacji handlowej to w sumie tak to widzę :))

    Mam nadzieję, że to choć trochę odpowiada na twoje pytania i dzięki za komentarz. pzdr

    OdpowiedzUsuń
  3. Ale chyba jak wołasz ten Serwis.find(userId), to w typie zwracanym gdzieś to Repository siedzi, nie? Albo wprost jako funkcja albo opakowane jeszcze w Reader[]. I oczywiście chodzi mi tutaj o wieksza liczbę zależności - przy jednej to problem jest dosyć prosty ;) Tak więc choćby patrząc na bytecode klienta, jednak ta wiedza że Serwis potrzebuje w jakimś-tam wywołaniu Repository jest. Przy "zwykłym" DI tej zależności nie masz.

    Może to i dobrze - nie wiem, chociaż wydawało mi się zawsze fajne że mogę takie szczegóły ukryć.

    Zgoda że Reader to może być dodatkowy tool używany "obok" DI, ale jednak ciężko się oprzeć pokusie chęci znalezienia jakiejś ogólnieszej reguły kiedy co użyć. Np. - choćby te Repository/DAO w aplikacji CRUDowej - wstrzykiwać? Czy przez Readera? A może aplikacje CRUDową robić w PHP? ;) Jeden pomysł jaki na razie mam - który ułatwia testowanie, to do readera wyrzucać te zalezności które są potrzebne dla jednej/dwóch metod z klasy - tak żeby do testowania pozostałych nie trzeba było robić "pustych" mocków.

    Btw. tego Readera to już od dawna próbuje rozgryźć, patrz http://stackoverflow.com/questions/29174500/reader-monad-for-dependency-injection-multiple-dependencies-nested-calls ;)

    OdpowiedzUsuń
  4. Serwis cały czas może ukrywać istnienie repo bo możesz mieć zwykły interfejs finsUser(userId):User

    a ta forma Repository=>User może być zbudowana wewnątrz serwisu i użyta tylko do wewnętrznych przekształceń. Zaleta jest taka, że jak robisz częściowe wywołanie to raczej nic się nie wywali i ten "Serwis" martwi się tylko jak określić relację pomiędzy userId i Repository i później może wysłać ten przepis do egzekucji w innym komponencie/serwisie/klasie gdzie potencjalne zjebki mogą być obsłużone.

    I teraz to co jest dla mnie dużym plusem to że np. ten przepis Repository=>User jest ten sam i w testach i na produkcji bo dopiero przy Repository pojawi się różnica Mock/Produkcyjne. Przy normalnym wstrzykiwaniu przez konstruktor różnica pojawia się jakby "od razu".

    No i to co dla mnie jest bardzo wartościowe, to że można to Repository=>User przekształcać przy pomocy funkcji User=>A , które w ogóle istnienia Repozytorium są nieświadome . Oczywiście można tych funkcji używać i tka jak repo jest wstrzyknięte przez konstruktor ale wtedy mam takie odczucie, że kompozycja jest mniej elastyczna bo już z góry narzucony kontekst w którym wywołasz te funkcje jak masz
    "operacjaNaRepo andThen prostaFunkcja" to operacjaNaRepo jest niezależnym klockiem , jeśli repo jest wstzrknięte konstruktorem to operacjaNaRepo jest "tu i teraz" - to jest tak konsekwencja, która starałem sie pokazać w akapicie "Klasycznie"

    Co do większej ilości zależności to można wykorzystać fakt, że jak mamy "Repository=>A" to "A" może być "Notification=>B" a "B" może być OtherService=>C. Czyli tak jak jest Funkctor Aplikatywny w validacji w scalaz talk tutaj mógłby być teoretycznie "Reader aplikatywny" ale tu już trzeba samemu zdecydować czy taka rzeźba ma uzasadnienie.

    Generalnie użyłem koncepcji "Reader" w artykule bo mam odczucie, że programistom Javy prościej operuje się rzeczownikami ale osobiście polecam bardzo aby spróbować wyrzucić pojęcie Reader i patrzeć na to jako zestaw dwóch prostych mechanizmów "curried function" i "lazy evaluation". Moją intuicję zajebiście rozbudował rozdział z "Functional programming in scala" o lazy evaluation gdzie własnie te ziomy co pisały książkę totalnie odseparowały opis programu od momentu wykonania gdzie opisali, która cześć ma być wykonana równolegle i na ilu watkach.

    pzdr,

    OdpowiedzUsuń
    Odpowiedzi
    1. A, czyli ty byś myślał używać Readera tak bardziej "lokalnie". Jasne, można, i przy bardziej skomplikowanych przekształceniach danych (gdzie składasz pare funkcji) pewnie ma spory sens :)

      Mam jednak wrażenie że przez "funkcyjnych" DI jest często odrzucane "bo tak", i właśnie Reader wskazuja jako zastępstwo - dlatego cały czas szukam jakiegos dobrego przykładu aplikacji która byłaby w tym stylu zrobiona

      Usuń