Planera Motivering Kontrollera

SOLID designprinciper. Beroendeinversionsprincip. En kritisk titt på principen om inversion av beroenden Principen för beroendeomvändning implementeras med

14 svar

Det säger i princip:

  • Abstraktioner bör aldrig bero på detaljer. Detaljerna bör bero på abstraktionen.

När det gäller varför detta är viktigt, i ett ord: förändringar är riskabla, och beroende på konceptet, inte implementeringen, minskar du behovet av förändring av samtalswebbplatser.

DIP minskar effektivt sammankopplingen mellan olika delar av koden. Tanken är att även om det finns många sätt att implementera, säg, en logger, så ska ditt sätt att använda det vara relativt stabilt över tiden. Om du kan extrahera ett gränssnitt som representerar loggningskonceptet, bör det gränssnittet vara mycket mer stabilt över tiden än dess implementering, och samtalsajter bör vara mycket mindre mottagliga för de ändringar du kan göra samtidigt som du behåller eller utökar denna loggningsmekanism ... .

Eftersom implementeringen beror på gränssnittet kan du vid körning välja vilken implementering som är bäst för just din miljö. Beroende på fallet kan detta också vara intressant.

Böckerna Agile Software Development, Principles, Patterns and Practices och Agile Principles, Patterns and Practices in C # är de bästa resurserna för att fullt ut förstå de ursprungliga målen och motivationerna bakom principen för beroendeomvändning. Dependency Inversion -principen är också en bra resurs, men eftersom det är en sammanfattad version av utkastet som hamnade i de tidigare nämnda böckerna lämnar det en viktig diskussion om begreppet paketägande och gränssnitt som är avgörande för skillnader i denna princip från mer allmänna råd"program för ett gränssnitt, inte en implementering", som finns i Design Patterns (Gamma, et al.).

Sammanfattningsvis är principen om beroendeomvändning främst riktad mot ändringen traditionell riktning av beroenden på komponenter "mer hög nivå"till komponenterna på lägre nivå" så att komponenterna på "lägre nivå" beror på gränssnitten, ägd av komponenter på högre nivå, (Obs! En "högre nivå" -komponent hänvisar här till en komponent som kräver externa beroenden / tjänster, inte nödvändigtvis dess konceptuella position i en skiktad arkitektur.) minskar lika mycket som hon skift från komponenter som är teoretiskt sett mindre värdefulla till komponenter som är teoretiskt sett mer värdefulla.

Detta uppnås genom att utveckla komponenter vars externa beroenden uttrycks som ett gränssnitt för vilket konsumenten av komponenten måste tillhandahålla en implementering. Med andra ord uttrycker vissa gränssnitt vad komponenten behöver, inte hur du använder komponenten (till exempel "INeedSomething", inte "IDoSomething").

Vad Dependency Inversion -principen inte refererar till är en enkel metod för att abstrahera beroenden med gränssnitt (t.ex. MyService ->). Även om detta kopplar bort komponenten från beroendets specifika implementeringsdetaljer, inverterar det inte förhållandet mellan konsument och beroende (t.ex. ⇐ Logger.

Betydelsen av principen om beroendeomvändning kan kokas ner till ett enda mål - möjligheten att återanvända programvarukomponenter som är beroende av externa beroenden för en del av dem. funktionalitet(registrering, verifiering etc.)

Inom detta övergripande mål återanvända vi kan skilja mellan två typer av återanvändning:

    Använda en programvarukomponent i flera applikationer med beroendeimplementeringar (till exempel har du utvecklat en DI -behållare och vill tillhandahålla loggning, men vill inte länka din behållare till en specifik logger, så alla som använder din behållare bör också använda ditt valda loggbibliotek) ...

    Genom att använda programvarukomponenter i ett utvecklande sammanhang (till exempel har du utvecklat logikkomponenter för företag som förblir desamma i versioner av din applikation där implementeringsdetaljer utvecklas).

I det första fallet att återanvända komponenter i flera applikationer, till exempel med ett infrastrukturbibliotek, är målet att förse konsumenterna med den underliggande infrastrukturen utan att knyta dina konsumenter till beroenden i ditt eget bibliotek, eftersom att få beroenden från sådana beroenden kräver att konsumenterna också kräver samma beroenden ... Detta kan vara problematiskt när konsumenterna i ditt bibliotek bestämmer sig för att använda ett annat bibliotek för samma infrastrukturbehov (som NLog och log4net), eller om de bestämmer sig för att använda en senare version av biblioteket som inte är bakåtkompatibelt med den version ditt bibliotek har kräver.

I det andra fallet med återanvändning av affärslogikkomponenter (dvs. "högre nivåskomponenter") är målet att isolera tillämpningsomfattningen av tillämpningsområdet från de förändrade behoven hos dina implementeringsdetaljer (t.ex. ändra / uppdatera ihållande bibliotek, utbyta biblioteksmeddelanden). krypteringsstrategier, etc.). Helst bör ändring av applikationsdetaljer för applikationen inte bryta komponenterna som inkapslar affärslogiken för applikationen.

Notera. Vissa kanske motsätter sig att beskriva detta andra fall som verklig återanvändning och tror att komponenter som affärslogikkomponenter som används i en enda utvecklande applikation endast representerar en användning. Tanken här är emellertid att varje förändring i tillämpningsdetaljerna för applikationen speglar ett nytt sammanhang och därför ett annat användningsfall, även om slutmålen kan särskiljas som isolering och bärbarhet.

Även om det kan vara till någon fördel att följa principen om beroende inversion i det senare fallet, bör det noteras att dess betydelse har minskat kraftigt på moderna språk som Java och C #, kanske till den punkt där det är irrelevant. Som diskuterats tidigare innebär DIP fullständig separation av implementeringsdetaljer i separata paket. När det gäller en applikation som utvecklas kommer dock helt enkelt att använda de gränssnitt som definieras i affärsområdet skydda mot behovet av att ändra komponenter på högre nivå på grund av de förändrade behoven hosna, även om implementeringsdetaljerna hamnar i samma paket .... Denna del av principen återspeglar aspekter som var relevanta för språket vid dess kodifiering (till exempel C ++) som inte är relevanta för nyare språk. Emellertid är betydelsen av principen för beroendeomvändning främst relaterad till utvecklingen av återanvändbara programvarukomponenter / bibliotek.

En mer detaljerad diskussion av denna princip, eftersom den handlar om enkel användning av gränssnitt, beroendeinsprutning och delat gränssnittsmönster, kan hittas.

När vi utvecklar program kan vi överväga klasser på låg nivå, klasser som implementerar grundläggande och primära operationer (hårddiskåtkomst, nätverksprotokoll och ...) och klasser på hög nivå som inkapslar komplex logik (affärsflöden, ...) .

De senare förlitar sig på lågnivåklasser. Det naturliga sättet att genomföra sådana strukturer skulle vara att skriva lågnivåklasser och så snart vi tvingas skriva komplexa klasser hög nivå. Eftersom klasser på hög nivå definieras i termer av andra verkar detta vara det logiska sättet att göra det. Men det här är inte en flexibel design. Vad händer om vi behöver byta ut en lågnivåklass?

Avhängighetsinversionsprincipen säger att:

  • Modeller på hög nivå bör inte bero på moduler på låg nivå. Båda bör bero på abstraktioner.

Denna princip syftar till att "invertera" den konventionella uppfattningen som högnivåmoduler i programvara måste bero på de lägre nivåmodulerna. Här äger moduler på hög nivå den abstraktion (till exempel att lösa gränssnittsmetoder) som implementeras av moduler på lägre nivå. Således är lägre nivåmoduler beroende av moduler på högre nivå.

Effektiv användning av Dependency Inversion ger flexibilitet och stabilitet i hela applikationens arkitektur. Detta gör att din ansökan kan utvecklas mer säkert och stabilt.

Traditionell skiktad arkitektur

Traditionellt har användargränssnittet för en skiktad arkitektur varit beroende av affärsskiktet, vilket i sin tur har berott på datatillgångsskiktet.

Du måste förstå ett lager, paket eller bibliotek. Låt oss se hur koden går.

Vi skulle ha ett bibliotek eller ett paket för datatillgångsskiktet.

// DataAccessLayer.dll publicDA ProductDAO ()

// BusinessLogicLayer.dll med DataAccessLayer; publicBO ProductBO (privat ProductDAO productDAO;)

Skiktad arkitektur med beroendeversion

Beroendeinversion indikerar följande:

Modeller på hög nivå bör inte bero på moduler på låg nivå. Båda bör bero på abstraktioner.

Abstraktioner bör inte bero på detaljer. Detaljerna bör bero på abstraktionen.

Vad är moduler på hög och låg nivå? Med tanke på moduler som bibliotek eller paket, skulle högnivåmoduler vara de som traditionellt har beroenden och lågnivåer som de är beroende av.

Med andra ord kommer modulens höga nivå att vara där åtgärden kallas och den låga nivån där åtgärden utförs.

Av denna princip kan en rimlig slutsats dras: det bör inte finnas något beroende mellan knölar, men det bör finnas beroende av abstraktion. Men enligt den metod vi använder kan vi missbruka investeringsberoendet, men detta är en abstraktion.

Tänk att vi anpassar vår kod så här:

Vi skulle ha ett bibliotek eller ett paket för ett dataåtkomstlager som definierar en abstraktion.

// DataAccessLayer.dll offentligt gränssnitt IPproductDAO public class ProductDAO: IProductDAO ()

Och annan affärslogik på biblioteks- eller paketnivå, som beror på datatillgångsskiktet.

// BusinessLogicLayer.dll med DataAccessLayer; publicBO -produktBO (privat IP -produkt DAO -produkt DAO;)

Även om vi är beroende av abstraktion förblir förhållandet mellan företag och datatillgång detsamma.

För att få beroendeomvändhet måste uthållighetsgränssnittet definieras i modulen eller paketet där logiken eller högnivådomänen finns, inte i modulen på låg nivå.

Definiera först vad domänskiktet är, och abstraktionen av dess relation bestäms av konstantitet.

// Domain.dll offentligt gränssnitt IProductRepository; med DataAccessLayer; publicBO ProductBO (privat IProductRepository productRepository;)

När uthållighetsnivån väl är domänberoende kan den nu inverteras om beroendet är definierat.

// Persistence.dll public class ProductDAO: IProductRepository ()

Fördjupa principen

Det är viktigt att förstå begreppet väl och fördjupa syftet och fördelarna. Om vi ​​stannar i mekanik och studerar ett typiskt förvar, kommer vi inte att kunna avgöra var vi kan tillämpa beroendeprincipen.

Men varför vänder vi på missbruket? Vad är huvudsyftet utanför konkreta exempel?

Detta är vanligtvis låter de mest stabila sakerna, som inte är beroende av de mindre stabila sakerna, förändras oftare.

Persistensstypen är lättare att ändra, antingen databasen eller tekniken för att komma åt samma databas, än domänlogik eller åtgärder som är utformade för att kommunicera ihållande. På grund av detta är förhållandet omvänt, eftersom det är lättare att ändra uthållighet om den förändringen inträffar. På så sätt behöver vi inte ändra domänen. Domänskiktet är det mest stabila av alla, så det borde inte bero på någonting.

Men det finns mer än bara det här exemplet på ett förråd. Det finns många scenarier som tillämpar denna princip, och det finns arkitekturer baserade på denna princip.

arkitektur

Det finns arkitekturer där beroendeversion är nyckeln till att definiera det. På alla domäner är detta viktigast, och det är abstraktionerna som kommer att specificera kommunikationsprotokollet mellan domänen och resten av paketen eller biblioteken.

Ren arkitektur

För mig, den beroende inversionsprincipen som beskrivs i den officiella artikeln

Problemet i C ++ är att rubrikfiler vanligtvis innehåller privata fält- och metoddeklarationer. Därför, om en C ++-modul på hög nivå innehåller en rubrikfil för en modul på låg nivå, beror den på den faktiska genomförande detaljer om denna modul. Och det här är uppenbarligen inte särskilt bra. Men detta är inte ett problem för fler moderna språk som ofta används idag.

Högnivåmoduler är i sig mindre återanvändbara än moduler på låg nivå eftersom de förra vanligtvis är mer applikations- / kontextspecifika än de senare. Till exempel är komponenten som implementerar UI-skärmen på högsta nivå och mycket (helt?) Applikationsspecifik. Att försöka återanvända en sådan komponent i en annan applikation är kontraproduktivt och kan bara leda till överutveckling.

Således kan en separat abstraktion på samma nivå av komponent A som är beroende av komponent B (som inte är beroende av A) bara göras om komponent A faktiskt är användbar för återanvändning i olika applikationer eller sammanhang. Om så inte är fallet skulle DIP -applikationen vara en dålig design.

Ett tydligare sätt att formulera principen om beroendeomvändning:

Dina moduler som inkapslar komplex affärslogik bör inte direkt bero på andra moduler som inkapslar affärslogik. Istället ska de bara vara beroende av gränssnitt ner till enkla data.

Dvs istället för att implementera din logikklass som folk vanligtvis gör:

Klassberoende (...) klasslogik (privat beroendeberoende; int doSomething () (// Affärslogik med dep här))

du borde göra något liknande:

Klassberoende (...) gränssnitt Data (...) klass DataFromDependency implementerar data (privat beroendeberoende; ...) klasslogik (int doSomething (datadata) (// beräkna något med data))

Data och DataFromDependency måste leva i samma modul som Logic, inte Dependency.

Varför är detta?

Bra svar och bra exempel redan gett av andra här.

Poängen med inversion av beroenden är att göra programvara återanvändbar.

Tanken är att de istället för att två kodbitar förlitar sig på varandra, förlitar sig på något abstrakt gränssnitt. Då kan du återanvända vilken del som helst utan den andra.

Detta uppnås vanligtvis genom att invertera en kontrollbehållare (IoC) som Spring in Java. I den här modellen konfigureras objektegenskaper via XML -konfiguration, inte objekt som lämnar och hittar deras beroenden.

Tänk dig denna pseudokod ...

Offentlig klass MyClass (public Service myService = ServiceLocator.service;)

MyClass är direkt beroende av både serviceklassen och ServiceLocator -klassen. Detta krävs för båda om du vill använda det i ett annat program. Tänk dig nu detta ...

Offentlig klass MyClass (public IService myService;)

MyClass använder nu ett gränssnitt, IService -gränssnittet. Vi skulle låta IoC -behållaren faktiskt ange värdet för denna variabel.

Låt det finnas ett hotell som ber mattillverkaren om hans förnödenheter. Hotellet ger namnet på maten (säg kyckling) till matgeneratorn, och generatorn skickar tillbaka den begärda maten till hotellet. Men hotellet bryr sig inte om vilken typ av mat det får och serverar. Således levererar Generatorn mat märkt "Mat" till hotellet.

Denna implementering i JAVA

FactoryClass med en fabriksmetod. Matgenerator

Public Class FoodGenerator (Matmat; offentlig mat getFood (strängnamn) (if (name.equals ("fisk")) (mat = ny fisk ();) annars om (name.equals ("kyckling")) (mat = ny kyckling ();) annars mat = null; returmat;))

Klassannotering / gränssnitt

Offentlig abstrakt klass Mat (// Ingen av barnklassen kommer att åsidosätta denna metod för att säkerställa kvalitet ... public void quality () (String fresh = "This is a fresh" + getName (); String delicious = "This is a läskande "+ getName (); System.out.println (färsk); System.out.println (välsmakande);) offentlig abstrakt String getName ();)

Kyckling implementerar mat (specifik klass)

Offentlig klass Kyckling utökar mat ( / * Alla livsmedel måste vara färska och välsmakande så * De kommer inte att åsidosätta superklassmetoden "egenskap ()" * / public String getName () (returnera "kyckling";) )

Fisk säljer mat (specifik klass)

Offentlig klass Fisk utökar mat ( / * Alla livsmedel måste vara färska och välsmakande så * De kommer inte att åsidosätta superklassmetoden "egenskap ()" * / public String getName () (returnera "Fisk";) )

Till sist

Hotell

Public class Hotel (public static void main (String args)) "kyckling"); food.quality ();))

Som du kunde se vet inte hotellet om det är kyckling eller fisk. Det är bara känt att det är ett livsmedelsobjekt, d.v.s. Hotellet beror på matklassen.

Du kanske också märker att klassen Fisk och kyckling implementerar matklassen och inte är direkt kopplad till hotellet. de där. kyckling och fisk beror också på matkvaliteten.

Det betyder att komponenten på hög nivå (hotell) och lågnivåkomponenten (fisk och kyckling) är beroende av abstraktion (mat).

Detta kallas beroendeversion.

Dependency Inversion Principle (DIP) säger att

i) Modeller på hög nivå bör inte bero på moduler på låg nivå. Båda bör bero på abstraktioner.

ii) Abstraktioner bör aldrig bero på detaljer. Detaljerna bör bero på abstraktionen.

Public interface ICustomer (string GetCustomerNameById (int id);) public class Customer: ICustomer (// ctor public Customer () () public string GetCustomerNameById (int id) (return "Dummy Customer Name";)) public class CustomerFactory (public static) ICustomer GetCustomerData () (returnera ny kund ();)) public class CustomerBLL (ICustomer _customer; public CustomerBLL () (_customer = CustomerFactory.GetCustomerData ();) public string GetCustomerNameById (int id) (return _customer.GetCustomerNameById ( public class Program (static void Main () (CustomerBLL customerBLL = new CustomerBLL (); int customerId = 25; string customerName = customerBLL.GetCustomerNameById (customerId); Console.WriteLine (customerName); Console.ReadKey ();))

Notera. En klass bör bero på abstraktioner som ett gränssnitt eller abstrakta klasser, inte på konkreta detaljer (gränssnittsimplementering).

att dela

, mångfald gränssnitt och beroendeomvändningar. Fem flexibla principer som hjälper dig varje gång du skriver kod.

Det vore orättvist att berätta att någon av SOLID -principerna är viktigare än den andra. Men kanske har ingen av de andra principerna en så omedelbar och djupgående inverkan på din kod som Dependency Inversion Principle, eller DIP kort sagt. Om du tycker att de andra principerna är svåra att förstå och tillämpa, bör du börja med det och sedan tillämpa resten på kod som redan följer principen för beroendeomvändning.

Definition

A. Moduler på hög nivå bör inte bero på lägre nivåmoduler. De måste alla vara beroende av abstraktioner.
B. Abstraktioner bör inte bero på detaljer. Detaljerna bör bero på abstraktionen.

Vi bör sträva efter att organisera vår kod baserat på dessa siffror, och här är några tekniker som kan hjälpa. Den maximala längden på funktioner bör inte vara mer än fyra rader (fem med rubriken), så att de helt kan passa in i vårt sinne. Inrycket ska vara högst fem nivåer djupt. Klasser med högst fem metoder. I designmönster sträcker sig vanligtvis antalet klasser från fem till nio. Vår arkitektur på hög nivå ovan innehåller fyra till fem koncept. Det finns fem SOLID -principer, var och en kräver fem till nio begrepp / moduler / klasser för exempel. Den idealiska utvecklingslagstorleken är mellan fem och nio. Det idealiska antalet lag i ett företag är också mellan fem och nio.

Som du kan se är det magiska talet sju, plus minus två, överallt, så varför skulle din kod vara annorlunda.

VARNING: Författaren till denna artikel har inte för avsikt att undergräva myndigheten eller på något sätt kränka en så respekterad kamrat som "farbror" Bob Martin. Vi talar här snarare om en mer noggrann övervägande av principen om beroendeomvändning och analys av exemplen som används i beskrivningen.

Under hela artikeln kommer jag att ge alla nödvändiga citat och exempel från ovanstående källor. Men så att det inte finns några "spoilers" och din åsikt förblir objektiv, skulle jag rekommendera att spendera 10-15 minuter och bekanta dig med den ursprungliga beskrivningen av denna princip i en artikel eller bok.

Beroendeinversionsprincipen låter så här:

A. Moduler på hög nivå bör inte bero på lägre nivåmoduler. Båda måste bero på abstraktioner.
F. Abstraktioner bör inte bero på detaljer. Detaljerna bör bero på abstraktionen.
Låt oss börja med den första punkten.

Skiktning

Lök har lager, tårta har lager, kannibaler har lager och mjukvarusystem har också lager! - Shrek (c)
Alla komplexa system är hierarkiska: varje lager är byggt ovanpå ett bevisat och välpresterande lägre lager. Detta låter dig fokusera på en begränsad uppsättning begrepp vid varje given tidpunkt utan att oroa dig för hur de underliggande lagren implementeras.
Som ett resultat får vi något liknande följande diagram:

Figur 1 - "Naivt" skiktningsschema

Från Bob Martins synvinkel är ett sådant system för att dela upp systemet i lager naiv... Nackdelen med denna design är den "lömska funktionen: lagret Politik beror på förändringar i alla lager på vägen till Verktyg. Detta beroende är transitivt.» .

Hmm ... Ganska ovanligt uttalande. Om vi ​​pratar om .NET-plattformen är beroendet endast transitivt om den nuvarande modulen "exponerar" lägre nivåmoduler i sitt öppna gränssnitt. Med andra ord, om i MekanismLager det finns en offentlig klass som tar en instans som argument StringUtil(från VerktygLager), sedan alla klienter på nivån MekanismLager bli beroende av VerktygLager... Annars finns det ingen transitivitet av ändringar: alla ändringar till den lägre nivån är begränsade till den nuvarande nivån och sprids inte ovan.

För att förstå Bob Martins idé måste du komma ihåg att för första gången beskrivs principen för beroendeomvändning 1996 och C ++ -språket användes som exempel. I originalartikeln skriver författaren själv det problemet med transitivitet är bara på språk utan en tydlig separation av klassgränssnittet från implementeringen... I C ++ är problemet med transitiva beroenden verkligen relevant: om filen PolicyLayer. h inkluderar via "inkludera" -direktivet MechanismLayer. h som i sin tur inkluderar UtilityLayer. h, sedan med någon ändring i rubrikfilen UtilityLayer. h(även i den "privata" delen av klasserna som deklareras i den här filen) måste vi kompilera om och distribuera alla klienter. Men i C ++ löses detta problem med hjälp av PIml -idiomet som föreslogs av Herb Sutter och är nu inte heller så relevant.

Lösningen på detta problem ur Bob Martins synvinkel är följande:

”Det högre lagret förklarar det abstrakta gränssnittet för de tjänster det behöver. De nedre skikten implementeras sedan för att tillfredsställa dessa gränssnitt. Varje klass som ligger på översta nivån kommer åt lagret på nästa nivå nedan genom ett abstrakt gränssnitt. Således är de övre skikten oberoende av de nedre. Omvänt beror de nedre lagren på det abstrakta servicegränssnittet, meddelat på en högre nivå ... Således, genom att vända beroenden, skapade vi en struktur som samtidigt är mer flexibel, hållbar och mobil.



Figur 2 - Inverterade lager

På ett sätt är en sådan partition rimlig. Så, till exempel, när du använder observatörsmönstret, är det det observerbara som definierar gränssnittet för interaktion med världen utanför, därför nej yttre förändringar kan inte påverka det.

Men å andra sidan, när det gäller lager, som vanligtvis representeras av sammansättningar (eller paket i UML -termer), kan den föreslagna metoden knappast kallas livskraftig. Per definition används lågnivåhjälparklasser i ett dussin olika högre nivåmoduler. Utility Layer kommer inte bara att användas i Mekanismskikt, men också i Data Access Layer, Transportskikt, Något annat lager... Ska den sedan implementera gränssnitten som definieras i alla högre nivåmoduler?

Uppenbarligen är en sådan lösning knappast idealisk, särskilt med tanke på att vi löser ett problem som inte finns på många plattformar, till exempel .NET eller Java.

Abstraktionskoncept

Många termer är så inarbetade i våra hjärnor att vi slutar uppmärksamma dem. För de flesta "objektorienterade" programmerare innebär det att vi slutar tänka på många hackiga termer som "abstraktion", "polymorfism", "inkapsling". Varför tänka på dem, för allt är klart ändå? ;)

För att exakt förstå innebörden av principen om beroendeomvändning och den andra delen av definitionen måste vi återgå till ett av dessa grundläggande begrepp. Låt oss ta en titt på definitionen av termen "abstraktion" från Grady Boochs bok:

Abstraktion framhäver de väsentliga egenskaperna hos ett objekt som skiljer det från alla andra typer av objekt och definierar därmed tydligt dess konceptuella gränser från observatörens synvinkel.

Med andra ord definierar abstraktion det objekts synliga beteende, som i programmeringsspråk definieras av objektets offentliga (och skyddade) gränssnitt. Mycket ofta modellerar vi abstraktioner med gränssnitt eller abstrakta klasser, även om det ur OOP -synvinkel inte behövs.

Låt oss gå tillbaka till definitionen: Abstraktioner bör inte bero på detaljer. Detaljerna bör bero på abstraktionen.

Vilket exempel uppstår i mitt huvud nu, efter att vi har kommit ihåg vad det är abstraktion? När börjar abstraktionen bero på detaljer? Ett exempel på ett brott mot denna princip är den abstrakta klassen GZipStream som tar MemoryStream, inte en abstrakt klass Ström:

Abstrakt klass GZipStream (// GZipStream -abstraktionen tar en specifik strömskyddad GZipStream (MemoryStream memoryStream) ())

Ett annat exempel på brott mot denna princip kan vara en abstrakt lagringsklass från datatillgångsskiktet, som tar in konstruktorn PostgreSqlConnection eller en anslutningssträng för SQL Server, vilket gör varje implementering av en sådan abstraktion knuten till en specifik implementering. Men är det vad Bob Martin menar? Av exemplen att döma i artikeln eller i boken förstår Bob Martin något helt annat med begreppet "abstraktion".

PrincipDOPPenligt Martin

För att förklara sin definition ger Bob Martin följande förklaring.

En något förenklad, men ändå mycket kraftfull tolkning av DIP -principen uttrycks med en enkel heuristisk regel: "Du måste vara beroende av abstraktioner." Den säger att det inte bör finnas några beroenden på specifika klasser; alla länkar i programmet måste leda till en abstrakt klass eller gränssnitt.

  • Det ska inte finnas några variabler som lagrar referenser till specifika klasser.
  • Det bör inte finnas några klasser som härrör från betongklasser.
  • Det bör inte finnas några metoder som åsidosätter en metod implementerad i en av basklasserna.

Som en illustration av kränkningen av DIP -principen i allmänhet och den första "förtydligande" punkten i synnerhet ges följande exempel:

Knapp för offentlig klass (privat Lamplampa; public void Poll () (om ( / * något villkor * /) lampa.TurnOn ();))

Låt oss nu komma ihåg igen vad det är abstraktion och svara på frågan: finns det en "abstraktion" här som beror på detaljerna? Medan du tänker på det här eller letar med ögonen efter det stycke där svaret på den här frågan ligger, vill jag göra en liten avvikelse.

Koden har en intressant funktion... Med sällsynta undantag kan själva koden inte vara korrekt eller felaktig. en bugg eller en funktion beror på vad som förväntas av den. Även om det inte finns någon formell specifikation (vilket är normen) är koden felaktig endast om den inte gör det som krävs eller antas av den. Det är denna princip som ligger till grund för kontraktsprogrammering, där specifikationen (avsikten) uttrycks direkt i koden i form av förutsättningar, eftervillkor och invarianter.

Tittar på klassen Knapp Jag kan inte säga om designen är fel eller inte. Jag kan säkert säga att klassens namn inte matchar dess implementering. Klassen måste byta namn till Lampknapp eller ta bort från klassen Knapp fält Lampa.

Bob Martin insisterar på att denna design är bristfällig eftersom ”applikationsstrategin på hög nivå inte är separat från implementeringen på låg nivå. Abstraktioner skiljs inte från detaljer. I avsaknad av denna separation beror toppnivåstrategin automatiskt på lägre nivåmoduler och abstraktionen beror automatiskt på detaljerna. "

I början, Jag ser inte in detta exempel"Strategier på högsta nivå" och "lägre nivåer": från min synvinkel, klasser Knapp och Lampaär på samma abstraktionsnivå (i alla fall ser jag inga argument för att bevisa något annat). Det faktum att klassen Knapp kan hantera någon gör det inte till högre nivå. För det andra finns det ingen "detaljberoende abstraktion", det finns "detaljberoende implementering av abstraktion", vilket inte alls är samma sak.

Martins lösning är:



Figur 3 - "Invertera beroenden"

Är det bättre detta beslut? Låt oss se…

Den största fördelen med Martin Dependency Inversion är ägarinversion. I den ursprungliga designen, när klassen ändras Lampa skulle behöva byta klass Knapp... Nu klassen Knapp"Äger" gränssnittet ButtonServer, men det kan inte förändras på grund av förändringar i de "lägre nivåerna", t.ex. Lampa... Tvärtom: byta klass ButtonServer endast möjligt under påverkan av förändringar i Button -klassen, vilket kommer att leda till en förändring i alla klasser ButonServer!

Senast uppdaterad: 2016-11-03

Beroendeinversionsprincip(Dependency Inversion Principle) används för att skapa löst kopplade enheter som är lätta att testa, modifiera och uppdatera. Denna princip kan formuleras enligt följande:

Moduler på toppnivå bör inte bero på lägre nivåmoduler. Båda måste bero på abstraktioner.

Abstraktioner bör inte bero på detaljer. Detaljerna bör bero på abstraktionen.

För att förstå principen, överväg följande exempel:

Klassbok (public string Text (get; set;) public ConsolePrinter Printer (get; set;) public void Print () (Printer.Print (Text);)) class ConsolePrinter (public void Print (string text) (Console.WriteLine (text);))

Klassen Bok, som representerar en bok, använder klassen ConsolePrinter för utskrift. Så här definieras beror bokklassen på ConsolePrinter -klassen. Dessutom har vi hårdkodat att boken bara kan skrivas ut till konsolen med ConsolePrinter-klassen. Andra alternativ, till exempel utmatning till en skrivare, utmatning till en fil eller användning av vissa element i det grafiska gränssnittet - allt detta är uteslutet i detta fall. Bokutskriftsabstraktionen är inte åtskild från detaljerna i ConsolePrinter -klassen. Allt detta är ett brott mot principen om beroendeomvändning.

Låt oss nu försöka få våra klasser i enlighet med principen om beroendeomvändning, separera abstraktioner från implementeringen på låg nivå:

Gränssnitt IPrinter (void Print (strängtext);) klass Book (public string Text (get; set;) public IPrinter Printer (get; set;) public Book (IPrinter printer) (this.Printer = printer;) public void Print ( ) (Printer.Print (Text);)) class ConsolePrinter: IPrinter (public void Print (string text) (Console.WriteLine ("Print to console");)) class HtmlPrinter: IPrinter (public void Print (string text) ( Console.WriteLine ("Skriv ut till html");))

Abstraktionen av att trycka boken är nu frikopplad från konkreta implementeringar. Som ett resultat beror både bokklassen och klassen ConsolePrinter på IPrinter -abstraktionen. Dessutom kan vi nu också skapa ytterligare lågnivåimplementeringar av IPrinter-abstraktionen och dynamiskt tillämpa dem i programmet:

Bokbok = ny bok (ny ConsolePrinter ()); bok.Print (); book.Printer = ny HtmlPrinter (); bok.Print ();