9.1 Avhengighetsinversjon
Husk at vi en gang sa at i en serverapplikasjon kan du ikke bare lage strømmer gjennom new Thread().start()
? Bare beholderen skal lage tråder. Vi vil nå utvikle denne ideen ytterligere.
Alle objekter skal også bare opprettes av beholderen . Vi snakker selvfølgelig ikke om alle objekter, men heller om de såkalte forretningsobjektene. De blir også ofte referert til som søppelkasser. Benene til denne tilnærmingen vokser fra det femte prinsippet til SOLID, som krever å bli kvitt klasser og flytte til grensesnitt:
- Toppnivåmoduler bør ikke avhenge av laverenivåmoduler. Både de og andre bør avhenge av abstraksjoner.
- Abstraksjoner bør ikke avhenge av detaljer. Gjennomføringen må avhenge av abstraksjonen.
Moduler skal ikke inneholde referanser til spesifikke implementeringer, og alle avhengigheter og interaksjoner mellom dem skal bygges utelukkende på grunnlag av abstraksjoner (det vil si grensesnitt). Selve essensen av denne regelen kan skrives i én setning: alle avhengigheter må være i form av grensesnitt .
Til tross for sin grunnleggende natur og tilsynelatende enkelhet, brytes denne regelen oftest. Nemlig, hver gang vi bruker den nye operatøren i koden til programmet/modulen og oppretter et nytt objekt av en bestemt type, dannes dermed avhengigheten av implementeringen i stedet for å være avhengig av grensesnittet.
Det er klart at dette ikke kan unngås og objekter må lages et sted. Men du må i det minste minimere antall steder hvor dette gjøres og hvilke klasser som er spesifisert, samt lokalisere og isolere slike steder slik at de ikke er spredt over hele programkoden.
En veldig god løsning er den vanvittige ideen om å konsentrere opprettelsen av nye objekter innenfor spesialiserte objekter og moduler - fabrikker, servicelokaliser, IoC-containere.
På en måte følger en slik avgjørelse Single Choice-prinsippet, som sier: "Når et programvaresystem må støtte mange alternativer, bør den komplette listen kun være kjent for én modul i systemet" .
Derfor, hvis det i fremtiden er nødvendig å legge til nye alternativer (eller nye implementeringer, som i tilfellet med å lage nye objekter vi vurderer), vil det være nok å oppdatere bare modulen som inneholder denne informasjonen, og alle andre moduler vil forbli upåvirket og vil kunne fortsette arbeidet sitt som vanlig.
Eksempel 1
new ArrayList
I stedet for å skrive noe sånt som , ville det være fornuftig List.new()
for JDK å gi deg den riktige implementeringen av et blad: ArrayList, LinkedList eller til og med ConcurrentList.
For eksempel ser kompilatoren at det er anrop til objektet fra forskjellige tråder og legger en trådsikker implementering der. Eller for mange innlegg i midten av arket, da vil implementeringen være basert på LinkedList.
Eksempel 2
Dette har allerede skjedd med sorteringer, for eksempel. Når skrev du sist en sorteringsalgoritme for å sortere en samling? I stedet bruker nå alle metoden Collections.sort()
, og elementene i samlingen må støtte Comparable-grensesnittet (comparable).
Hvis sort()
du sender en samling på mindre enn 10 elementer til metoden, er det fullt mulig å sortere den med en boblesortering (Bubble sort), og ikke Quicksort.
Eksempel 3
Kompilatoren ser allerede på hvordan du setter sammen strenger og vil erstatte koden din med StringBuilder.append()
.
9.2 Avhengighetsinversjon i praksis
Nå det mest interessante: la oss tenke på hvordan vi kan kombinere teori og praksis. Hvordan kan moduler opprette og motta "avhengigheter" på riktig måte og ikke bryte avhengighetsinversjon?
For å gjøre dette, når du designer en modul, må du bestemme selv:
- hva modulen gjør, hvilken funksjon den utfører;
- så trenger modulen fra omgivelsene, det vil si hvilke objekter / moduler den må forholde seg til;
- Og hvordan skal han få det?
For å overholde prinsippene for Dependency Inversion, må du definitivt bestemme hvilke eksterne objekter modulen din bruker og hvordan den vil få referanser til dem.
Og her er følgende alternativer:
- modulen selv lager objekter;
- modulen tar gjenstander fra beholderen;
- modulen har ingen anelse om hvor objektene kommer fra.
Problemet er at for å lage et objekt, må du kalle en konstruktør av en bestemt type, og som et resultat vil modulen ikke avhenge av grensesnittet, men av den spesifikke implementeringen. Men hvis vi ikke vil at objekter skal opprettes eksplisitt i modulkoden, kan vi bruke Factory Method- mønsteret .
"Konklusjonen er at i stedet for å instansiere et objekt direkte via nytt, gir vi klientklassen et grensesnitt for å lage objekter. Siden et slikt grensesnitt alltid kan overstyres med riktig design, får vi en viss fleksibilitet ved bruk av moduler på lavt nivå. i høynivåmoduler" .
I tilfeller der det er nødvendig å opprette grupper eller familier av relaterte objekter, brukes en abstrakt fabrikk i stedet for en fabrikkmetode .
9.3 Bruke Service Locator
Modulen tar de nødvendige objektene fra den som allerede har dem. Det antas at systemet har et eller annet depot av objekter, der moduler kan "sette" objektene sine og "ta" objekter fra depotet.
Denne tilnærmingen er implementert av Service Locator-mønsteret , hvor hovedideen er at programmet har et objekt som vet hvordan man får alle avhengighetene (tjenestene) som kan være nødvendige.
Hovedforskjellen fra fabrikker er at Service Locator ikke lager objekter, men faktisk allerede inneholder instansierte objekter (eller vet hvor/hvordan de skal hentes, og hvis den oppretter, så bare én gang ved første anrop). Fabrikken ved hver samtale lager et nytt objekt som du får fullt eierskap til og du kan gjøre hva du vil med det.
Viktig ! Tjenestelokatoren produserer referanser til de samme allerede eksisterende objektene . Derfor må du være veldig forsiktig med objektene som utstedes av Service Locator, siden noen andre kan bruke dem samtidig som deg.
Objekter i Service Locator kan legges til direkte gjennom konfigurasjonsfilen, og faktisk på noen måte som er praktisk for programmereren. Selve Service Locator kan være en statisk klasse med et sett med statiske metoder, en singleton eller et grensesnitt, og kan sendes til de nødvendige klassene via en konstruktør eller metode.
Tjenestesøkeren kalles noen ganger et antimønster og frarådes (fordi den skaper implisitte forbindelser og bare gir inntrykk av god design). Du kan lese mer fra Mark Seaman:
9.4 Avhengighetsinjeksjon
Modulen bryr seg ikke om å "gruve" avhengigheter i det hele tatt. Den bestemmer bare hva den trenger for å fungere, og alle nødvendige avhengigheter blir levert (introdusert) fra utsiden av noen andre.
Dette er det som kalles - Dependency Injection. Vanligvis sendes de nødvendige avhengighetene enten som konstruktørparametere (Constructor Injection) eller gjennom klassemetoder (Setter-injection).
Denne tilnærmingen inverterer prosessen med å skape avhengigheter - i stedet for selve modulen, blir opprettelsen av avhengigheter kontrollert av noen utenfra. Modulen fra den aktive emitteren av objekter blir passiv - det er ikke han som skaper, men andre skaper for ham.
Denne retningsendringen kalles Inversion of Control , eller Hollywood-prinsippet - "Ikke ring oss, vi ringer deg."
Dette er den mest fleksible løsningen, og gir modulene størst autonomi . Vi kan si at bare den fullt ut implementerer "Single Responsibility Principle" - modulen skal være fullstendig fokusert på å gjøre jobben sin godt og ikke bekymre deg for noe annet.
Å gi modulen alt som er nødvendig for arbeidet er en egen oppgave, som bør håndteres av den aktuelle "spesialisten" (vanligvis er en viss beholder, en IoC-beholder, ansvarlig for å administrere avhengigheter og implementeringen av dem).
Faktisk er alt her som i livet: i et godt organisert selskap programmerer programmerere, og skrivebordene, datamaskinene og alt de trenger til jobben kjøpes og leveres av kontorsjefen. Eller, hvis du bruker metaforen til programmet som konstruktør, skal modulen ikke tenke på ledninger, noen andre er involvert i å sette sammen konstruktøren, og ikke delene i seg selv.
Det vil ikke være en overdrivelse å si at bruken av grensesnitt for å beskrive avhengigheter mellom moduler (Dependency Inversion) + riktig opprettelse og injeksjon av disse avhengighetene (primært Dependency Injection) er nøkkelteknikker for avkobling .
De tjener som grunnlaget på hvilken den løse koblingen av koden, dens fleksibilitet, motstand mot endringer, gjenbruk, og uten hvilken alle andre teknikker gir liten mening. Dette er grunnlaget for løs kobling og god arkitektur.
Prinsippet for inversjon av kontroll (sammen med Dependency Injection and Service Locator) er diskutert i detalj av Martin Fowler. Det er oversettelser av begge artiklene hans: "Inversion of Control Containers and the Dependency Injection pattern" og "Inversion of Control" .