kubuszok.com

1 Feb 2018 * serie: tilfældig om Scala * tags: scala kage mønster afhængighed injektion

for lang tid siden i landet Scala opstod ny type-sikker måde afhængighed injektion. På lang sigt bringer det flere problemer, end det er værd.

Kagemønster

lad os minde om, hvad der er et kagemønster. For at forstå det skal vi kende selvtyper. De var beregnet til at blive brugt som blandinger og ser mere eller mindre sådan ud:

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

Her kan vi tilføje traitUserRepositoryLoggingtil noget, der erUserRepository implementering – det ville ikke kompilere ellers. Derudover inde UserRepositoryLogging vi antager, at det er implementeringen af UserRepository. Så alt tilgængeligt inden for UserRepository er også tilgængeligt der.

nu, da vi kan få adgang til, hvad der blev erklæret type(er), der blev brugt til selvtype, har vi lov til at gøre dette:

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

inden forSomeControllerVi erklærer selvtype, der sikrer, at dens instantiable implementering vil haveuserRepository metode. Så ved at tilføje blandingen leverer vi implementeringen, og så sikrer vi, at afhængighed injiceres på kompileringstidspunktet. Uden runtime refleksion, med type-sikkerhed, ingen yderligere konfigurationer eller biblioteker.

hver sådan komponent kan være et lag af vores applikation (forretningslogiklag, infrastrukturlaget osv.), som nogen sammenlignede med lagene i kagen. Så ved at oprette din ansøgning på en sådan måde bager du effektivt en kage. Således kage mønster.

nu … hvorfor er det et problem?

tilføjelse af afhængigheder

i større projekter vil antallet af tjenester, arkiver og hjælpeprogrammer helt sikkert være større end 1-2. Så din kage vil begynde at se ud:

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

som et spørgsmål om, så jeg en gang en kage, hvorComponentImpl gik gennem 2-3 skærme. Du injicerede ikke kun komponenter-de-nødvendige-ad gangen, du kombinerede alt sammen på en gang: infrastruktur, domænetjenester, vedholdenhed, forretningslogik…

forestil dig nu, hvad der sker, når du skal tilføje en afhængighed. Du starter med at tilføje en anden with Dependency til din selvtype. Derefter kontrollerer du, hvor det opdaterede træk bruges. Du skal muligvis også tilføje selvtype her. Du klatrer op ad træet, indtil du når en klasse eller et objekt. Uff.

undtagen denne gang tilføjede Du dette selv, så du ved slags, hvad der skal tilføjes. Men når du laver rebase eller løser en konflikt, kan du ende i en situation, hvor der opstod en ny afhængighed, som du ikke har. Compiler fejl siger kun, at self-type X does not conform to Y. Med 50 + komponenter kan du lige så godt gætte, hvilken der fejler. (Eller begynde at gøre en binær søgning med at fjerne komponenter indtil fejl forsvinder).

Når kagestørrelse og mængden vokser yderligere, vil man måske implementere TØRREGEL og opdele den i mindre kager, der vil blive sat sammen senere. Dette afslører nye dybder af helvede.

fjernelse af afhængigheder

med normal DI når du holder op med at bruge et objekt, kan din IDE / compiler fortælle dig, at det ikke længere er nødvendigt, så du kan fjerne det. Med kage mønster, vil du ikke blive informeret om sådanne ting.

som et resultat er oprydning meget vanskeligere, og det er meget muligt, at du på et tidspunkt ender med meget mere afhængigheder, end du virkelig har brug for.

kompileringstid

alt dette tilføjer kompileringstiden. Det er måske ikke indlysende ved første øjekast, men du kan ende med:

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

som er en cyklisk afhængighed. (Vi ved alle, at de er dårlige, men du bemærker måske ikke engang, at du lige har oprettet en. Især hvis du initialiserer alt dovent).

så disse 2 Træk vil blive meget ofte kompileret sammen, og det vil ikke hjælpe med det. Derudover vil ændringer til nogen af komponenterne i en kage udløse genkompilering af ting over det i afhængighedsgrafen.

test

cykliske afhængigheder skaber et andet problem. Du kan meget vel ende med noget som:

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!

hver komponent vil være i sin egen fil, så du vil ikke bemærke denne afhængighed. Test med mock hjælper dig ikke med det:

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

så det vil være alt fint og dandy, indtil du rent faktisk kører koden. Du ved – koden er typesikker, hvilket betyder, at den værdi, du vil konstruere, følger typebegrænsninger. Men disse begrænsninger betyder intet som Nothing (kastet undtagelse) er en perfekt gyldig undertype af din krævede type.

Initialiseringsordre

det forrige problem er relateret til denne: Hvad er rækkefølgen af komponentinitialiseringen?

med normal DI er det indlysende – før du sætter en ting i den anden, skal du oprette den. Så du ser bare på den rækkefølge, hvor du opretter objekter.

med en kage kan du glemme det. Værdier inden for komponenter vil ofte være lazy vals eller objects. så først adgang til attribut vil forsøge at instantiere dens afhængigheder, som vil forsøge at instantiere dens afhængigheder osv.

selvom vi går med val s det er et spørgsmål om rækkefølge, hvor vi komponerede kage – du ved, træklinearisering. Så længe hver komponent vil blive brugt nøjagtigt en gang – ikke noget problem. Men hvis du tørret dig kage, og nogle komponenter gentage, når du flette dem…

i en perfekt verden sådanne ting som initialisering ordre bør ikke noget, men ganske ofte det gør, og vi vil gerne vide, f.eks. når nogle forbindelse til DB eller abonnement på Kafka emne startede, og logge ting for at debug potentielle fejl.

Boilerplate og lock-in

som du sikkert har bemærket dette mønster genererer en masse boilerplate – dybest set hver repository eller tjeneste skulle have indpakning, kun for anmærke afhængigheder.

når du går ned ad den vej, vil du bemærke, at det er ret svært at slippe af med kagemønster. Hver implementering sættes i et træk, og dens afhængigheder kommer fra omfanget, ikke via parametre. Så for hver klasse skal du først udføre refactor for at kunne instantiere det uden indpakningen.

men så skal afhængighederne også refactoreres og så videre. Kagemønster er et grimt engagement, der smerter og modsætter dig, når du prøver at trække dig gradvist ud af det.bogstaveligt talt giver enhver anden form for DI I Scala, som jeg kender (runtime reflection a ‘la Guice, Makro annotationer a’ la Macledning, implicerer kombinationer af disse 3) Dig mulighed for at sige stop! på ethvert tidspunkt, og gradvist skifte til en anden løsning.

Resume

Kagemønster er en løsning på et DI-problem, der på lang sigt gør mere skade end gavn. Fra hvad jeg hørte det blev brugt i Scala compiler, og de fortryder det. Det var standard di-mekanismen for Play – rammer, men på grund af alle disse problemer besluttede forfattere at skifte til Guice-Ja, funktionelle programmører foretrak runtime-refleksion frem for typesikker kage, så det fortæller meget.

så hvad er alternativerne?almindelig gammel manuel argumentation passerer-ingen grund til at forklare, at

  • implicerer – har sine fordele, indtil dit projekt opretholdes af mere end dig selv, og kolleger begynder at klage over ikke at vide, hvad der sker, langsom kompilering, ikke forståelse implicerer…
  • runtime reflection – Guice er en moden ramme, der gør DI på præcis den måde, jeg hader. Det blev brugt i mange projekter, Java – programmører elsker det-ligesom de elsker foråret. Nogle af dem Lader abstraktionslækage ved at føre injektor ind i klassen. Nogle af dem ønsker, at deres konfiguration skal være fleksibel – så fleksibel, at klasseledning muligvis mislykkes ved kørsel, bleh!halvvejs gennem passerer argumenter manuelt og ved hjælp af makroer for alt – det scanner aktuelle omfang i kompilere tid og injicerer afhængigheder for dig. Hvis nogle mangler kompilering mislykkes. Du skriver i mellemtiden bare wire
  • Airframe – makrobaseret ramme, hvor du bygger opskrift på DI med en DSL. Det har støtte til objekt lifecycle management
  • pulp – min skamløse auto-promotion. Det brugte implicits og makro anmærkninger for at generere udbydere til klasser
  • IMHO hver af dem skulle arbejde bedre, end kage mønster.

    Skriv et svar

    Din e-mailadresse vil ikke blive publiceret.