for lenge siden i landet Scala dukket opp ny type-sikker måte avhengighet injeksjon. På lang sikt gir det mer problemer enn det er verdt.
Kakemønster
la oss minne om hva som er et kakemønster. For å forstå det må vi vite selvtyper. De var ment å bli brukt som mixins og ser mer eller mindre ut som det:
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 legge traitUserRepositoryLogging
til noe som erUserRepository
implementering – det ville ikke kompilere noe annet. I tillegg antar vi at det er implementeringen avUserRepository
. Så alt tilgjengelig innenforUserRepository
er også tilgjengelig der.
Nå, siden vi kan få tilgang til hva som ble erklært type (er) som brukes til selvtype, har vi lov til å gjø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
InnenforSomeController
vi erklærer egen type, som sikrer, at dens instantiable implementering vil hauserRepository
metode. Så ved å legge til mixin gir vi implementeringen, og så sikrer vi at avhengighet injiseres på kompileringstidspunktet. Uten kjøretidsrefleksjon, med typesikkerhet, ingen ekstra konfigurasjoner eller biblioteker.
Hver slik komponent kan være et lag av vår søknad (forretningslogikklag, infrastrukturlag, etc), som noen sammenlignet med lagene i kaken. Så, ved å lage din søknad slik måte du effektivt baker en kake. Dermed kake mønster.
Nå… hvorfor er det et problem?
Legge til avhengigheter
i større prosjekter vil antall tjenester, repositorier og verktøy sikkert være større enn 1-2. Så kaken din vil begynne å se ut som:
object PaymentTransactionApiController extends TransactionApiController with ConfigComponentImpl with DatabaseComponentImpl with UserRepositoryComponentImpl with SessionRepositoryComponentImpl with SecurityServicesComponentImpl with ExternalPaymentApiServicesComponentImpl with PaymentServicesComponentImpl with TransactionServicesComponentImpl
Som et spørsmål om at jeg så en gang en kake derComponentImpl
gikk gjennom 2-3 skjermer. Du injiserte ikke bare komponenter-de-trengte-på – en-tid, du kombinerte alt sammen på en gang: infrastruktur, domenetjenester, utholdenhet, forretningslogikk…
forestill deg nå hva som skjer når du må legge til en avhengighet. Du starter med å legge til en annen with Dependency
til din egen type. Deretter sjekker du hvor den oppdaterte egenskapen brukes. Du må muligens legge til selvtype her også. Du klatrer opp mixins treet til du når en klasse eller et objekt. Uff.
Bortsett Fra, denne gangen har du lagt til dette selv, så du vet hva som må legges til. Men når du gjør rebase eller løse en konflikt, kan du ende opp i en situasjon når noen ny avhengighet dukket opp som du ikke har. Kompilatorfeil sier bare at self-type X does not conform to Y
. Med 50 + komponenter kan du også gjette hvilken som feiler. (Eller begynn å gjøre et binært søk med å fjerne komponenter til feilen forsvinner).
når kakestørrelsen og mengden vokser videre, vil man kanskje implementere TØRRREGEL og dele den i mindre kaker,som vil bli satt sammen senere. Dette avdekker nye dybder av helvete.
Fjerne avhengigheter
med normal DI når du slutter å bruke et objekt, KAN IDE/kompilatoren fortelle deg at DET ikke lenger er nødvendig, så du kan fjerne det. Med kake mønster, vil du ikke bli informert om slike ting.som et resultat er opprydding mye vanskeligere, og det er ganske mulig at du i et øyeblikk vil ende opp med mye mer avhengigheter enn du virkelig trenger.
Kompileringstid
Alt dette legger opp til kompilertiden. Det kan ikke være opplagt ved første øyekast, men du kan ende opp med:
trait ModuleAComponentImpl { self: ModuleBComponent => }trait ModuleBComponentImpl { self: ModuleAComponent => }
som er en syklisk avhengighet. (Vi vet alle at de er dårlige, men du kan ikke engang merke til at du nettopp opprettet en. Spesielt hvis du initialiserer alt lat).
så de 2 trekkene vil bli veldig ofte kompilert sammen, Og Zinc inkrementell kompilator vil ikke hjelpe med det. I tillegg vil endringer i noen av komponentene i en kake utløse rekompilering av ting over det i avhengighetsgrafen.
Testing
Sykliske avhengigheter skaper et annet problem. Du kan godt ende opp med noe sånt:
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 merke denne avhengigheten. Tester med mock vil ikke hjelpe deg med det:
trait BComponentMock { val b: B = mock }val aComponent = new AComponentImpl with BComponentMock {}aComponent.a // works!
så det blir alt bra og dandy til du faktisk kjører koden. Du vet – koden er typesikker, noe som betyr at verdien du vil konstruere vil følge typebegrensninger. Men disse begrensningene betyr ingenting som Nothing
(kastet unntak) er en helt gyldig undertype av ønsket type.
Initialiseringsordre
det forrige problemet er relatert til dette: hva er rekkefølgen på komponentinitialiseringen?
med normal DI er det åpenbart – før du legger en ting inn i den andre må du opprette den. Så du ser bare på rekkefølgen der du lager objekter.
med en kake kan du glemme det. Verdier i komponenter vil ofte være lazy val
s eller object
s. så først tilgang til attributtet vil prøve å instantiere dets avhengigheter, som vil forsøke å instantiere dets avhengigheter, etc.
Selv om vi går medval
s det er et spørsmål om rekkefølge der vi komponerte kake – du vet, egenskap linearisering. Så lenge hver komponent vil bli brukt nøyaktig en gang-ikke noe problem. Men Hvis du Tørket deg kake, og noen komponenter gjenta, når du flette dem…
i en perfekt verden slike ting som initialisering orden bør ikke saken, men ganske ofte det gjør, og vi ønsker å vite f. eks når noen forbindelse TIL DB eller abonnement På Kafka emnet startet, og logge ting for å feilsøke potensielle feil.
Boilerplate og lock-in
som du sikkert har lagt merke til, genererer dette mønsteret mye boilerplate – i utgangspunktet må hvert depot eller tjeneste ha wrapper, bare for å kommentere avhengighetene.
når du går ned den veien vil du legge merke til at er er ganske vanskelig å bli kvitt kake mønster. Hver implementering er satt inne i en egenskap, og dens avhengigheter kommer fra omfanget, ikke via parametere. Så for hver klasse må du først utføre refactor for å kunne instantiere den uten omslaget.
men da må avhengighetene også refactoreres og så videre. Kake mønster er en stygg forpliktelse, som smerter og motsetter deg når du prøver å trekke seg fra det gradvis.Bokstavelig talt, enhver annen FORM FOR DI I Scala, som jeg vet (runtime reflection a ‘la Guice, makro merknader a’ la MacWire, implisitter, kombinasjoner av disse 3) lar deg si stopp! når som helst, og gradvis bytte til en annen løsning.
Sammendrag
Kakemønster er en løsning PÅ ET di-problem, som i det lange løp gjør mer skade enn godt. Fra det jeg horte det ble brukt I Scala compiler, og de angre pa det. Det var standard di mekanisme For Spill Rammeverk, men på grunn av alle disse problemene forfattere besluttet å bytte Til Guice-ja, funksjonelle programmerere foretrukket runtime refleksjon til type-safe kake, slik at forteller mye.
hva er alternativene?
- vanlig gammelt manuell argument passerer – ingen grunn til å forklare at
- implisitter-har sine fordeler til prosjektet opprettholdes av mer enn deg selv, og kollegaer begynner å klage på ikke å vite, hva som skjer, langsom kompilering, ikke forstå implisitter…
- runtime reflection – Guice er et modent rammeverk som GJØR DI på akkurat den måten jeg hater. Det ble brukt i Mange prosjekter, Java-programmerere elsker Det-akkurat som De elsker Våren. Noen av dem lar abstraksjon lekke ved å sende injektor inn i klassen. Noen av dem ønsker deres config å være fleksibel – så fleksibel at klassen ledninger kan mislykkes under kjøring, bleh!macwire-halvveis gjennom bestått argumenter manuelt og bruke makroer for alt-den skanner gjeldende omfang i kompilere tid og injiserer avhengigheter for deg. Hvis noen mangler kompilering mislykkes. Du skriver i mellomtiden bare
wire
- Airframe – makrobasert rammeverk hvor du bygger oppskrift PÅ DI med EN DSL. Den har støtte for object lifecycle management
- pulp – my shameless auto-promotion. DET brukes implicits og makro merknader FOR å generere leverandører for klasser
IMHO hver AV DEM bør trene bedre, enn kake mønster.