kubuszok.com

1.2.2018• series: Random about Scala • tags: scala cake pattern dependence injection

kauan sitten Scalan maassa syntyi uusi tyyppiturvallinen tapa dependence injection. Pitkässä juoksussa se tuo enemmän ongelmia kuin kannattaa.

Kakkukuvio

muistuttakaamme, mikä on kakkukuvio. Ymmärtääksemme sitä meidän täytyy tuntea minätyypit. Ne oli tarkoitettu mixiineiksi ja näyttämään enemmän tai vähemmän tuolta.:

trait UserRepository { def fetchUsers(userIds: Seq): Seq}class UserRepositoryDbImpl(database: Database) extends UserRepository { def fetchUsers(userIds: Seq): Seq = ...}trait UserRepositoryLogging { self: UserRepository => private lazy val logger: Logger = ... override def fetchUsers(userIds: Seq): Seq = { val users = super.fetchUsers(usersIds) logger.debug(s"""Fetched users by ids: ${users.mkString(", ")}""") users }}val userRepository: UserRepository = new UserRepositoryDbImpl(database) with UserRepositoryLogging

tässä voidaan lisätä ominaisuus UserRepositoryLoggingkaikkeen, mikä onUserRepositorytoteutus – se ei koostuisi muuten. Lisäksi UserRepositoryLogging oletamme, että kyseessä on UserRepositorytoteutus. Sielläkin on siis kaikki saavutettavissa UserRepository.

nyt, koska voimme käyttää mitä tahansa self-typelle ilmoitettua tyyppiä(T), meillä on lupa tehdä tämä:

trait UserRepository { def fetchUsers(userIds: Seq): Seq}trait UserRepositoryComponent { def userRepository: UserRepository}trait UserRepositoryComponentImpl extends UserRepositoryComponent { lazy val userRepository: UserRepository = new UserRepository { // ... }}trait SomeController { self: UserRepositoryComponent => def handleRequest(userIds: Seq): Response = { // ... userRepository.fetchUsers(userIds) // ... }}object SomeController extends SomeController with UserRepositoryComponentImpl

Within SomeController julistamme itse-tyypin, joka varmistaa, että sen instantiable implementation on userRepository method. Joten lisäämällä mixin tarjoamme toteutuksen ja, joten varmistamme, että riippuvuus ruiskutetaan käännösaikaan. Ilman ajonaikaista heijastusta, tyyppiturvallisuudella, ei ylimääräisiä kokoonpanoja tai kirjastoja.

jokainen tällainen komponentti voisi olla sovelluksemme kerros (bisneslogiikan kerros, infrastruktuurikerros jne.), että joku vertaa kakun kerroksiin. Joten luomalla sovelluksen siten olet tehokkaasti leipoa kakku. Näin kakku kuvio.

nyt … miksi se on ongelma?

lisäämällä riippuvuudet

isommissa projekteissa palveluiden, arkistojen ja apuohjelmien määrä on varmasti suurempi kuin 1-2. Kakkusi alkaa siis näyttää tältä:

object PaymentTransactionApiController extends TransactionApiController with ConfigComponentImpl with DatabaseComponentImpl with UserRepositoryComponentImpl with SessionRepositoryComponentImpl with SecurityServicesComponentImpl with ExternalPaymentApiServicesComponentImpl with PaymentServicesComponentImpl with TransactionServicesComponentImpl

näin kerran kakun, jossaComponentImpl meni läpi 2-3 ruutua. Et pistänyt sinne vain tarvittavia komponentteja. yhdistit kaiken kerralla.: infrastruktuuri, Verkkotunnuspalvelut, pysyvyys, liiketoiminnan logiikka…

nyt, kuvittele mitä tapahtuu, kun sinun täytyy lisätä riippuvuus. Aloitat lisäämällä toisen omatyyppiisi. Sitten tarkistetaan, missä päivitettyä ominaisuutta käytetään. Sinun on mahdollisesti lisättävä self-tyyppi täällä samoin. Kiivetään mixinien puuhun, kunnes päästään johonkin luokkaan tai esineeseen. Uff.

paitsi, että tällä kertaa lisäsitte tämän itse, joten tavallaan tiedätte, mitä pitää lisätä. Mutta kun olet palauttamassa tai ratkaisemassa konfliktia, saatat päätyä tilanteeseen, jossa ilmaantuu jokin uusi riippuvuus, jota sinulla ei ole. Kääntäjän virhe kertoo vain, että self-type X does not conform to Y. 50 + komponentit, voit yhtä hyvin arvata, kumpi epäonnistuu. (Tai aloita binäärihaku poistamalla komponentteja kunnes virhe katoaa).

kun kakun koko ja määrä kasvaa entisestään, kannattaa ehkä toteuttaa KUIVASÄÄNTÖ ja jakaa se pienempiin kakkuihin, jotka kootaan myöhemmin. Tämä paljastaa uusia syvyyksiä helvetissä.

riippuvuuksien poistaminen

normaalilla DI: llä kun lopetat jonkin objektin käytön, IDE / kääntäjä voi kertoa, että sitä ei enää tarvita, joten saatat poistaa sen. Kakkukuviolla et saa tietoa tällaisista asioista.

tämän seurauksena siivoaminen on paljon vaikeampaa ja on täysin mahdollista, että jossain vaiheessa sinulle jää paljon enemmän riippuvuuksia kuin todella tarvitset.

Käännösaika

kaikki tämä lasketaan yhteen käännösaikaan. Se ei ehkä ole selvää ensi silmäyksellä, mutta voit päätyä:

trait ModuleAComponentImpl { self: ModuleBComponent => }trait ModuleBComponentImpl { self: ModuleAComponent => }

, joka on syklinen riippuvuus. (Me kaikki tiedämme, että ne ovat huonoja, mutta et ehkä edes huomaa, että olet juuri luonut yhden. Varsinkin jos alustat kaiken laiskasti).

joten ne 2 ominaisuutta kootaan hyvin usein yhteen, eikä Sinkkikertoiminen kääntäjä auta siinä. Lisäksi muutokset mihin tahansa kakun komponenttiin käynnistävät sen yläpuolella olevan materiaalin uudelleenkompiloinnin riippuvuusdiagrammissa.

testaus

sykliset riippuvuudet luovat toisen ongelman. Saatat hyvinkin päätyä jotain:

trait AComponentImpl { self: BComponent => lazy val a: A = new A { b.someMethod() // use b }}trait BComponentImpl { self: AComponent => lazy val b: B = new B { a.someMethod() // use a }}object Cake extends AComponentImpl with BComponentImplCake.a // NPE!Cake.b // NPE!

jokainen komponentti on omassa tiedostossaan, joten et huomaa tätä riippuvuutta. Testit mockilla eivät auta sinua siinä:

trait BComponentMock { val b: B = mock }val aComponent = new AComponentImpl with BComponentMock {}aComponent.a // works!

joten kaikki on hyvin ja dandy kunnes oikeasti juokset koodin. Tiedät-koodi on tyyppi-turvallinen, mikä tarkoittaa, että arvo voit rakentaa seuraa tyyppi rajoituksia. Mutta nämä rajoitteet eivät merkitse mitään, sillä Nothing (heitetty poikkeus) on täysin kelvollinen alatyyppi vaaditussa tyypissä.

Alustusjärjestys

edellinen ongelma liittyy tähän: mikä on komponentin alustuksen järjestys?

normaalilla DI: llä se on ilmiselvää – ennen kuin laittaa yhden asian toiseen, se pitää luoda. Joten katsot vain, missä järjestyksessä, luot esineitä.

kakun kanssa sen voi unohtaa. Komponenttien arvot ovat usein lazy vals tai objects. joten ensin käytetty attribuutti yrittää instantioida sen riippuvuudet, joka yrittää instantioida sen riippuvuudet jne.

vaikka mennään vals se on järjestysasia, jossa sävellettiin kakku – tiedäthän, piirteen linearisointi. Niin kauan kuin jokainen komponentti käytetään täsmälleen kerran – ei ongelmaa. Mutta jos kuivasit kakkusi, ja jotkin osat toistuvat, kun yhdistät ne…

täydellisessä maailmassa, kuten alustusjärjestyksellä ei pitäisi olla väliä, mutta melko usein sillä on, ja haluaisimme tietää esim.milloin jokin yhteys DB: hen tai tilaus Kafka-aiheeseen alkoi, ja kirjata asioita, jotta mahdolliset viat voidaan debugata.

Kattilalevy ja lock-in

kuten luultavasti huomasit, Tämä kuvio luo paljon kattilalevyä – periaatteessa jokaisessa arkistossa tai palvelussa olisi oltava kääre, vain riippuvuuksien merkitsemistä varten.

kun sille tielle lähtee, huomaa, että on aika vaikea päästä eroon kakkukuviosta. Jokainen toteutus laitetaan piirteen sisään, ja sen riippuvuudet tulevat soveltamisalasta, eivät parametrien kautta. Joten kunkin luokan, sinun pitäisi suorittaa refactor ensin voidakseen asentaa sen ilman kääre.

mutta silloin riippuvuudet täytyisi myös refaktoida ja niin edelleen. Kakkukuvio on ruma sitoumus, joka särkee ja vastustaa, kun siitä yrittää vetäytyä vähitellen.

kirjaimellisesti, mikä tahansa muu Scalan di-muoto, jonka tiedän (runtime reflection a ’la Guice, makro annotations a’ la MacWire, implisiittisesti, näiden 3 yhdistelmiä) voit sanoa stop! milloin tahansa, ja vähitellen siirtyä toiseen ratkaisuun.

Yhteenveto

Kakkukuvio on ratkaisu DI-ongelmaan, josta on pitkässä juoksussa enemmän haittaa kuin hyötyä. Kuulin, että sitä käytettiin Scala-Kääntäjässä, ja he katuvat sitä. Se oli oletuksena di mekanismi Play Framework, mutta koska kaikki nämä kysymykset kirjoittajat päättivät siirtyä Guice-Kyllä, toiminnalliset ohjelmoijat mieluummin ajonaikainen heijastus tyyppi-turvallinen kakku, joten se kertoo paljon.

mitkä ovat vaihtoehdot?

  • plain old manual argument passing – no need to explain that
  • implisiittiset – have its advantages until your project is about not knowing, what is happening, slow compilation, not understanding implisiittiset…
  • runtime reflection – Guice is a mature framework doing DI In tismalleen the way that I hate. Sitä käytettiin monissa projekteissa, Java-ohjelmoijat rakastavat sitä-aivan kuten he rakastavat kevättä. Jotkut päästivät vedenoton vuotamaan syöttämällä injektorin luokkaan. Jotkut heistä haluavat config olla joustava-niin joustava, että luokan johdotus voi epäonnistua runtime, bleh!
  • MacWire – puolivälissä kulkee argumentteja manuaalisesti ja käyttämällä makroja kaikkeen – se skannaa nykyisen soveltamisalan compile time ja injektoi riippuvuudet sinulle. Jos jotkut puuttuvat kokoelma epäonnistuu. Samaan aikaan kirjoitetaan vain wire
  • Airframe – makropohjainen kehys, jossa rakennetaan resepti DI: lle DSL: llä. Siinä on tuki object lifecycle management
  • pulp – my shameless auto-promotionille. Se käytti implisiittisiä ja makrohuomautuksia luodakseen tarjoajia luokille

IMHO jokaisen niistä pitäisi toimia paremmin, kuin kakkukuvio.

Vastaa

Sähköpostiosoitettasi ei julkaista.