9.1 Beroendeinversion
Kom ihåg att vi en gång sa att i en serverapplikation kan du inte bara skapa strömmar genom new Thread().start()
? Endast behållaren ska skapa trådar. Vi kommer nu att utveckla denna idé ytterligare.
Alla objekt bör också skapas endast av behållaren . Vi pratar förstås inte om alla objekt utan snarare om de så kallade affärsobjekten. De kallas också ofta för papperskorgar. Benen för detta tillvägagångssätt växer från den femte principen i SOLID, som kräver att bli av med klasser och flytta till gränssnitt:
- Toppnivåmoduler bör inte vara beroende av lägre nivåmoduler. Både de och andra borde vara beroende av abstraktioner.
- Abstraktioner bör inte bero på detaljer. Implementeringen måste bero på abstraktionen.
Moduler bör inte innehålla referenser till specifika implementeringar, och alla beroenden och interaktioner mellan dem bör byggas enbart på basis av abstraktioner (det vill säga gränssnitt). Själva kärnan i denna regel kan skrivas i en fras: alla beroenden måste vara i form av gränssnitt .
Trots dess grundläggande karaktär och uppenbara enkelhet överträds denna regel oftast. Nämligen, varje gång vi använder den nya operatorn i programmets/modulens kod och skapar ett nytt objekt av en specifik typ, så bildas istället för att vara beroende av gränssnittet beroendet av implementeringen.
Det är klart att detta inte går att undvika och föremål måste skapas någonstans. Men du måste åtminstone minimera antalet platser där detta görs och där klasser är explicit specificerade, samt lokalisera och isolera sådana platser så att de inte är utspridda i programkoden.
En mycket bra lösning är den galna idén att koncentrera skapandet av nya objekt inom specialiserade objekt och moduler - fabriker, servicelokaliserare, IoC-containrar.
På sätt och vis följer ett sådant beslut Single Choice-principen, som säger: " När ett programvarusystem måste stödja många alternativ, bör deras fullständiga lista endast vara känd för en modul i systemet. "
Därför, om det i framtiden är nödvändigt att lägga till nya alternativ (eller nya implementeringar, som i fallet med att skapa nya objekt som vi överväger), kommer det att räcka att bara uppdatera modulen som innehåller denna information och alla andra moduler kommer att förbli opåverkade och kommer att kunna fortsätta sitt arbete som vanligt.
Exempel 1
new ArrayList
Istället för att skriva något som , skulle det vara vettigt List.new()
för JDK att ge dig den korrekta implementeringen av ett blad: ArrayList, LinkedList eller till och med ConcurrentList.
Till exempel ser kompilatorn att det finns anrop till objektet från olika trådar och lägger en trådsäker implementering där. Eller för många inlägg i mitten av arket, då kommer implementeringen att baseras på LinkedList.
Exempel 2
Det har redan hänt med sorter, till exempel. När skrev du senast en sorteringsalgoritm för att sortera en samling? Istället använder nu alla metoden Collections.sort()
, och elementen i samlingen måste stödja gränssnittet Comparable (jämförbart).
Om sort()
du skickar en samling på mindre än 10 element till metoden är det fullt möjligt att sortera den med en bubbelsortering (Bubblesortering), och inte Quicksort.
Exempel 3
Kompilatorn tittar redan på hur du sammanfogar strängar och kommer att ersätta din kod med StringBuilder.append()
.
9.2 Beroendeinversion i praktiken
Nu är det mest intressanta: låt oss fundera på hur vi kan kombinera teori och praktik. Hur kan moduler korrekt skapa och ta emot sina "beroenden" och inte bryta mot beroendeinversion?
För att göra detta, när du designar en modul, måste du själv bestämma:
- vad modulen gör, vilken funktion den utför;
- då behöver modulen från sin omgivning, det vill säga vilka objekt/moduler den kommer att behöva hantera;
- Och hur ska han få det?
För att följa principerna för Dependency Inversion måste du definitivt bestämma vilka externa objekt som din modul använder och hur den ska få referenser till dem.
Och här är följande alternativ:
- modulen själv skapar objekt;
- modulen tar objekt från behållaren;
- modulen har ingen aning om var objekten kommer ifrån.
Problemet är att för att skapa ett objekt måste du anropa en konstruktör av en specifik typ, och som ett resultat kommer modulen inte att bero på gränssnittet utan på den specifika implementeringen. Men om vi inte vill att objekt ska skapas explicit i modulkoden, så kan vi använda Factory Method- mönstret .
"Sammanfattningen är att istället för att direkt instansiera ett objekt via nytt, förser vi klientklassen med något gränssnitt för att skapa objekt. Eftersom ett sådant gränssnitt alltid kan åsidosättas med rätt design får vi en viss flexibilitet när vi använder lågnivåmoduler i högnivåmoduler" .
I de fall det är nödvändigt att skapa grupper eller familjer av relaterade objekt, används en abstrakt fabrik istället för en fabriksmetod .
9.3 Använda Service Locator
Modulen tar de nödvändiga objekten från den som redan har dem. Det antas att systemet har något arkiv med objekt, där moduler kan "sätta" sina objekt och "ta" objekt från arkivet.
Detta tillvägagångssätt implementeras av Service Locator-mönstret , vars huvudidé är att programmet har ett objekt som vet hur man får alla beroenden (tjänster) som kan krävas.
Den största skillnaden från fabriker är att Service Locator inte skapar objekt, utan faktiskt redan innehåller instansierade objekt (eller vet var/hur man får dem, och om den skapar, då bara en gång vid första anropet). Fabriken vid varje samtal skapar ett nytt objekt som du får full äganderätt till och du kan göra vad du vill med det.
Viktigt ! Tjänstelokaliseringen producerar referenser till samma redan existerande objekt . Därför måste du vara mycket försiktig med objekten som utfärdas av Service Locator, eftersom någon annan kan använda dem samtidigt som du.
Objekt i Service Locator kan läggas till direkt via konfigurationsfilen, och verkligen på något sätt som är bekvämt för programmeraren. Själva Service Locator kan vara en statisk klass med en uppsättning statiska metoder, en singleton eller ett gränssnitt, och kan skickas till de obligatoriska klasserna via en konstruktor eller metod.
Service Locator kallas ibland ett antimönster och avskräcks (eftersom den skapar implicita kopplingar och bara ger sken av bra design). Du kan läsa mer från Mark Seaman:
9.4 Beroendeinjektion
Modulen bryr sig inte om att "bryta" beroenden alls. Den bestämmer bara vad den behöver för att fungera, och alla nödvändiga beroenden tillhandahålls (införs) utifrån av någon annan.
Detta är vad som kallas - Dependency Injection. Vanligtvis skickas de nödvändiga beroendena antingen som konstruktorparametrar (Constructor Injection) eller genom klassmetoder (Setter-injection).
Detta tillvägagångssätt inverterar processen att skapa beroenden - istället för själva modulen styrs skapandet av beroenden av någon utifrån. Modulen från den aktiva sändaren av objekt blir passiv - det är inte han som skapar, utan andra skapar åt honom.
Denna riktningsändring kallas Inversion of Control , eller Hollywood-principen - "Ring inte oss, vi ringer dig."
Detta är den mest flexibla lösningen som ger modulerna den största autonomin . Vi kan säga att bara den fullt ut implementerar "Single Responsibility Principle" - modulen ska vara helt fokuserad på att göra sitt jobb bra och inte oroa sig för något annat.
Att förse modulen med allt som behövs för arbetet är en separat uppgift, som bör hanteras av lämplig "specialist" (vanligtvis är en viss behållare, en IoC-behållare, ansvarig för att hantera beroenden och deras implementering).
Faktum är att allt här är som i livet: i ett välorganiserat företag programmerar programmerare, och skrivborden, datorerna och allt de behöver för arbetet köps och tillhandahålls av kontorschefen. Eller, om du använder programmets metafor som konstruktör, så ska modulen inte tänka på ledningar, någon annan är inblandad i att montera konstruktören, och inte delarna i sig.
Det skulle inte vara en överdrift att säga att användningen av gränssnitt för att beskriva beroenden mellan moduler (Dependency Inversion) + korrekt skapande och injicering av dessa beroenden (primärt Dependency Injection) är nyckeltekniker för frikoppling .
De fungerar som grunden på vilken den lösa kopplingen av koden, dess flexibilitet, motståndskraft mot förändringar, återanvändning, och utan vilken alla andra tekniker inte är meningsfulla. Detta är grunden för lös koppling och bra arkitektur.
Principen för Inversion of Control (tillsammans med Dependency Injection and Service Locator) diskuteras i detalj av Martin Fowler. Det finns översättningar av båda hans artiklar: "Inversion of Control Containers and the Dependency Injection pattern" och "Inversion of Control" .