Princip inverze závislosti je implementován pomocí. Princip inverze závislosti. Vrstvená architektura s inverzí závislostí


ODMÍTNUTÍ ODPOVĚDNOSTI: Autor tohoto článku nemá za cíl podkopávat autoritu nebo jakkoli urážet tak váženého soudruha, jakým je „strýček“ Bob Martin. Jde spíše o to pečlivěji se zamyslet nad principem inverze závislosti a analyzovat příklady použité k jejímu popisu.

V celém článku uvedu všechny potřebné citace a příklady z výše uvedených zdrojů. Ale abyste se vyhnuli „spoilerům“ a váš názor zůstal objektivní, doporučil bych věnovat 10-15 minut a přečíst si originální popis tohoto principu v článku nebo knize.

Princip inverze závislosti vypadá takto:

A. Moduly nejvyšší úrovně by neměly záviset na modulech nižší úrovně. Obojí musí záviset na abstrakcích.
B. Abstrakce by neměly záviset na detailech. Podrobnosti musí záviset na abstrakcích.
Začněme prvním bodem.

Vrstvení

Cibule má vrstvy, koláče mají vrstvy, kanibalové mají vrstvy a softwarové systémy mají také vrstvy! – Shrek (c)
Každý komplexní systém je hierarchický: každá vrstva je postavena na osvědčené a dobře fungující vrstvě nižší úrovně. To vám umožní soustředit se na omezenou sadu konceptů najednou, aniž byste přemýšleli o tom, jak jsou implementovány základní vrstvy.
V důsledku toho dostaneme něco jako následující diagram:

Obrázek 1 – „Naivní“ schéma vrstvení

Z pohledu Boba Martina takové schéma rozdělení systému na vrstvy je naivní. Nevýhodou tohoto provedení je „zákeřná vlastnost: vrstva Politika závisí na změnách ve všech vrstvách na cestě k Utility. Tato závislost je tranzitivní.» .

Hmm... Velmi neobvyklé prohlášení. Pokud mluvíme o platformě .NET, pak bude závislost tranzitivní pouze v případě, že aktuální modul ve svém veřejném rozhraní „vystaví“ moduly nižších úrovní. Jinými slovy, pokud v MechanismusVrstva existuje veřejná třída, která bere instanci jako argument StringUtil(z UtilityVrstva), pak všichni klienti úrovně MechanismusVrstva stát se závislým na UtilityVrstva. Jinak nedochází k přechodnosti změn: všechny změny nižší úrovně jsou omezeny na aktuální úroveň a nešíří se výše.

Abyste pochopili myšlenku Boba Martina, musíte si pamatovat, že princip inverze závislostí byl poprvé popsán již v roce 1996 a jako příklady byl použit jazyk C++. V původním článku to píše sám autor problém tranzitivity existuje pouze v jazycích bez jasného oddělení rozhraní třídy od implementace. V C++ je problém tranzitivních závislostí skutečně relevantní: pokud soubor PolicyLayer. h zahrnuje prostřednictvím směrnice "zahrnout". Vrstva mechanismu. h, což zase zahrnuje UtilityLayer. h, pak pro jakoukoli změnu v záhlaví souboru UtilityLayer. h(i v „uzavřené“ sekci tříd deklarovaných v tomto souboru) budeme muset znovu zkompilovat a znovu nasadit všechny klienty. V C++ je však tento problém vyřešen použitím idiomu PIml, který navrhl Herb Sutter a nyní také není tak relevantní.

Řešení tohoto problému z pohledu Boba Martina je toto:

„Vrstva vyšší úrovně deklaruje abstraktní rozhraní pro služby, které potřebuje. K uspokojení těchto rozhraní jsou pak implementovány nižší vrstvy. Jakákoli třída umístěná na nejvyšší úrovni přistupuje k vrstvě vedle ní prostřednictvím abstraktního rozhraní. Horní vrstvy jsou tedy nezávislé na spodních. Naopak, nižší vrstvy závisí na rozhraní abstraktních služeb, oznámil na vyšší úrovni... Převrácením závislostí jsme tedy vytvořili strukturu, která je zároveň flexibilnější, odolnější a mobilnější



Obrázek 2 – Invertované vrstvy

V některých ohledech toto rozdělení dává smysl. Takže například při použití vzoru pozorovatele je to pozorovaný objekt (pozorovatelný), který definuje rozhraní pro interakci s vnějším světem, takže ho nemohou ovlivnit žádné vnější změny.

Ale na druhou stranu, pokud jde konkrétně o vrstvy, které jsou obvykle reprezentovány jako sestavy (nebo balíčky v podmínkách UML), navrhovaný přístup lze jen stěží označit za životaschopný. Podle definice se pomocné třídy nižší úrovně používají v tuctu různých modulů vyšší úrovně. Užitná vrstva se uplatní nejen v Vrstva mechanismu, ale také v Vrstva pro přístup k datům, Transportní vrstva, Nějaká další vrstva. Měla by pak implementovat rozhraní definovaná ve všech modulech vyšší úrovně?

Je zřejmé, že toto řešení není ideální, zvláště když řešíme problém, který na mnoha platformách, jako je .NET nebo Java, neexistuje.

Pojem abstrakce

Mnoho pojmů se nám vryje do mozku natolik, že jim přestaneme věnovat pozornost. Pro většinu „objektově orientovaných“ programátorů to znamená, že přestáváme přemýšlet o mnoha nadužívaných termínech jako „abstrakce“, „polymorfismus“, „zapouzdření“. Proč o nich přemýšlet, když už je vše jasné? ;)

Abychom však přesně pochopili význam Principu inverze závislosti a druhé části definice, musíme se vrátit k jednomu z těchto základních pojmů. Podívejme se na definici pojmu „abstrakce“ z knihy Gradi Buchy:

Abstrakce identifikuje podstatné vlastnosti nějakého objektu, které jej odlišují od všech ostatních typů objektů, a tím jasně vymezuje jeho pojmové hranice z pohledu pozorovatele.

Jinými slovy, abstrakce definuje viditelné chování objektu, které je v programovacích jazycích určeno veřejným (a chráněným) rozhraním objektu. Velmi často modelujeme abstrakce pomocí rozhraní nebo abstraktních tříd, i když z pohledu OOP to není nutné.

Vraťme se k definici: Abstrakce by neměly záviset na detailech. Podrobnosti musí záviset na abstrakcích.

Jaký příklad nás nyní napadá, když si vzpomeneme, co to je? abstrakce? Kdy začíná abstrakce záviset na detailech? Příkladem porušení tohoto principu je abstraktní třída GZipStream, která trvá MemoryStream, nikoli abstraktní třída Proud:

Abstraktní třída GZipStream ( // Abstrakce GZipStream přijímá konkrétní proud chráněný GZipStream(MemoryStream memoryStream) () )

Dalším příkladem porušení tohoto principu by byla abstraktní třída úložiště z vrstvy pro přístup k datům, která přijímá konstruktor PostgreSqlConnection nebo připojovací řetězec pro SQL Server, díky kterému je jakákoli implementace takové abstrakce svázána s konkrétní implementací. Ale myslí tím Bob Martin? Soudě podle příkladů uvedených v článku nebo v knize Bob Martin chápe pod pojmem „abstrakce“ něco úplně jiného.

ZásadaDIPpodle Martina

K vysvětlení své definice uvádí Bob Martin následující vysvětlení.

Mírně zjednodušenou, ale přesto velmi účinnou interpretaci principu DIP vyjadřuje jednoduché heuristické pravidlo: „Musíte se spoléhat na abstrakce.“ Uvádí, že by neměly existovat žádné závislosti na konkrétních třídách; všechna připojení v programu musí vést k abstraktní třídě nebo rozhraní.

  • Neměly by existovat žádné proměnné, které ukládají odkazy na konkrétní třídy.
  • Neměly by existovat žádné třídy odvozené od konkrétních tříd.
  • Neměly by existovat žádné metody, které přepisují metodu implementovanou v jedné ze základních tříd.

Pro ilustraci porušení zásady DIP obecně a zejména prvního „vyjasňujícího“ bodu je uveden následující příklad:

Tlačítko veřejné třídy ( private Lamp lamp; public void Poll() ( if (/* nějaká podmínka */) lampa.TurnOn(); ) )

Nyní si ještě jednou připomeňme, co to je abstrakce a odpovězte na otázku: existuje zde „abstrakce“, která závisí na detailech? Zatímco o tom přemýšlíte nebo hledáte odstavec, který obsahuje odpověď na tuto otázku, chtěl bych udělat malou odbočku.

Kód má jednu zajímavou vlastnost. Až na vzácné výjimky nemůže být samotný kód správný nebo nesprávný; Zda se jedná o chybu nebo funkci, závisí na tom, co se od ní očekává. I když neexistuje žádná formální specifikace (což je norma), kód je nesprávný pouze tehdy, pokud dělá něco jiného, ​​než co je požadováno nebo zamýšleno dělat. Právě tento princip je základem smluvního programování, ve kterém jsou specifikace (záměry) vyjádřeny přímo v kódu ve formě předpokladů, postpodmínek a invariantů.

Při pohledu na třídu Knoflík Nemohu říci, zda je design vadný nebo ne. Rozhodně mohu říci, že název třídy neodpovídá její implementaci. Třídu je třeba přejmenovat na Tlačítko lampy nebo odstranit ze třídy Knoflík pole Svítilna.

Bob Martin trvá na tom, že tento návrh je chybný, protože „strategie aplikací na vysoké úrovni není oddělena od implementace na nízké úrovni. Abstrakce nejsou odděleny od detailů. Pokud takové oddělení neexistuje, strategie nejvyšší úrovně automaticky závisí na modulech nižší úrovně a abstrakce automaticky závisí na detailech."

Za prvé, V tomto příkladu nevidím „strategie nejvyšší úrovně“ a „moduly nižší úrovně“.: z mého pohledu tříd Knoflík A Svítilna jsou na stejné úrovni abstrakce (alespoň nevidím žádné argumenty pro opak). Skutečnost, že třída Knoflík To, že někoho může ovládat, ho nedělá na vyšší úroveň. Za druhé, neexistuje zde žádná „abstrakce závislá na detailech“, existuje „implementace abstrakce závislá na detailech“, což vůbec není totéž.

Martinovo řešení je:



Obrázek 3 – „Invertování závislostí“

Je toto řešení lepší? Pojďme se podívat…

Hlavní výhodou invertování závislostí „podle Martina“ je inverze vlastnictví. V původním provedení, při změně tř Svítilna třída by se musela změnit Knoflík. Nyní třída Knoflík„vlastní“ rozhraní ButtonServer, ale nemůže se změnit kvůli změnám v „nižších úrovních“, jako je např Svítilna. Je to přesně naopak: změna třídy ButtonServer možné pouze pod vlivem změn ve třídě Button, které povedou ke změnám u všech potomků třídy ButonServer!

Formulace principu inverze závislosti se skládá ze dvou pravidel, jejichž dodržování má mimořádně pozitivní vliv na strukturu kódu:

  • Moduly nejvyšší úrovně by neměly záviset na modulech nižší úrovně. Obojí musí záviset na abstrakci.
  • abstrakce by neměly záviset na detailech. Podrobnosti musí záviset na abstrakcích.

Zpočátku to nezní příliš lákavě a čtenář je pravděpodobně již připraven na velmi nudný článek se spoustou pojmů, složitých slovních obratů a příkladů, z nichž stejně není nic jasné. Ale marně, protože v zásadě, za předpokladu principu inverze závislostí, vše opět směřuje ke správnému použití a v kontextu hlavní myšlenky - opětovného použití kódu.

Dalším konceptem, který zde bude relevantní, je slabá vazba typů, to znamená snížení nebo odstranění jejich vzájemné závislosti, čehož je ve skutečnosti dosaženo pomocí abstrakce a polymorfismu. To je ve skutečnosti podstata principu inverze závislosti.

Nyní se podívejme na příklad, který názorně demonstruje, jak vypadá volná spojka v akci.

Řekněme, že se rozhodneme objednat narozeninový dort. Za tímto účelem jsme šli do nádherné pekárny na rohu ulice. Zjistili jsme, zda by nám mohli upéct dort pod hlasitým názvem „Luxus“, a po kladném ohlasu jsme ho objednali. Je to jednoduché.

Nyní se pojďme rozhodnout, jaké abstrakce je třeba zahrnout do tohoto kódu. Chcete-li to provést, položte si několik otázek:

  • Proč dort? Můžete si také objednat koláč nebo koláčky
  • Proč konkrétně k narozeninám? Co kdyby to byla svatba nebo promoce?
  • Proč „Luxus“, co když se mi víc líbí „Mraveniště“ nebo „Praha“?
  • Proč zrovna tato pekárna, a ne cukrárna v centru města nebo někde jinde?

A každé z těchto „co kdyby“ a „co kdyby“ jsou body, ve kterých je vyžadována rozšiřitelnost kódu, a tedy volné propojení a definice typů prostřednictvím abstrakcí.

Nyní začněme konstruovat samotné typy.

Definujeme rozhraní pro jakékoli cukrářské mistrovské dílo, které bychom si mohli objednat.

Rozhraní IPastry ( string Name ( get; set; ) )

A zde je konkrétní realizace, pro náš případ - narozeninový dort. Jak můžete vidět, tradičně narozeninový dort zahrnuje svíčky)))

Třída BirthdayCake: IPastry ( public int NumberOfCandles ( get; set; ) public string Name ( get; set; ) public override string ToString() ( return String.Format("(0) with (1) candle nices"), Name, NumberOfCandles ) ;))

Nyní, pokud potřebujeme dort na svatbu nebo jen tak k čaji, nebo chceme koláčky nebo pudinkové koláče, máme základní rozhraní pro každého.

Další otázkou je, jak se od sebe cukrářské výrobky liší (samozřejmě kromě názvu). Samozřejmě s receptem!

A koncept receptu zahrnuje seznam ingrediencí a popis procesu vaření. Proto budeme mít samostatné rozhraní pro koncept přísad:

Rozhraní II Ingredient ( string IngredientName ( get;set;) double Quantity ( get; set; ) string Units ( get; set; ) )

A zde jsou ingredience pro náš dort: mouka, máslo, cukr a smetana:

Class Mouka:IIngredient ( public string IngredientName ( get; set; ) public double Quantity ( get; set; ) public string Units ( get; set; ) public string Quality ( get; set; ) ) class Butter: IIngredient ( public string IngredientName ( get; set; ) public double Quantity ( get; set; ) public string Units ( get; set; ) ) class Sugar: IIingredient ( public string IngredientName ( get; set; ) public double Quantity ( get; set; ) public string Jednotky ( get; set; ) public string Kind ( get; set; ) ) class Creme: IIngredient ( public string IngredientName ( get; set; ) public double Quantity ( get; set; ) public string Units ( get; set; ) public double Fat ( get; set; ) )

Seznam surovin se může v různých receptech a různých cukrářských výrobcích lišit, ale pro náš recept je tento seznam dostačující.

Nyní je čas přejít ke konceptu receptu. Ať vaříme cokoli, v každém případě víme, jak se tomu říká, co to je, jaké ingredience jsou v pokrmu obsaženy a jak ho vařit.

Rozhraní IRecipe ( Type PastryType ( get; set; ) string Name ( get; set;) IList Ingredience ( get;set;) string Description ( get;set;) )

Konkrétně třída představující recept na narozeninový dort vypadá takto:

Třída BirthdayCakeRecipe: IRecipe ( public Type PastryType ( get; set; ) public string Name ( get; set;) public IList Ingredients ( get; set; ) public string Description ( get; set; ) public BirthdayCakeReipe() ( Ingredients = new List (); } }

Nyní se přesuneme do naší úžasné pekárny na rohu ulice.

Samozřejmě bychom mohli aplikovat na mnoho dalších pekáren, takže i pro to si nadefinujeme základní rozhraní. Co je pro pekárnu nejdůležitější? Schopnost péct výrobky.

Rozhraní IBakery (IPastry Bake (recept na recept);)

A zde je třída reprezentující naši pekárnu:

Třída NiceBakeryOnTheCornerOFMyStreet (Slovník menu = nový slovník (); public void AddToMenu(IRrecipe recept) ( if (!menu.ContainsKey(recipe.Name)) (menu.Add(recipe.Name, recept); ) else ( Console.WriteLine("Už je v nabídce"); ) ) public IRecipe FindInMenu(název řetězce) ( if (menu.ContainsKey(název)) (nabídka návratu; ) Console.WriteLine("Omlouváme se...momentálně nemáme" + jméno); return null; ) public IPastry Bake (IRecept recept) ( if (recipe != null) ( IPaster pastry = Activator.CreateInstance(recipe.PastryType) as IPCastry; if (pečivo != null) (pečivo.Name = recept.Name; vraťte pečivo jako IPaster; ) ) návrat null;))

Zbývá jen otestovat kód:

Program třídy ( static void Main() ( //vytvoření inctance třídy pekařství var bakery = new NiceBakeryOnTheCornerOFMyStreet(); //příprava ingrediencí pro recept var mouka = ​​new Flour() ( IngredientName = "Mouka", Množství = 1,5 , Units = "kg" ); var máslo = new Butter() ( IngredientName = "Máslo", Množství = 0,5, Units = "kg" ); var sugar = new Sugar() ( IngredientName = "Sugar", Množství = 0,7 , Units = "kg" ); var creme = new Creme() ( IngredientName = "Creme", Množství = 1,0, Units = "litry" ); //a toto je samotný recept var weddingCakeRecipe = new BirthdayCakeRecipe() ( PastryType = typeof(BirthdayCake), Name = "Narozeninový dort Luxus", Description = "popis, jak udělat krásný narozeninový dort" ); weddingCakeRecipe.Ingredients.Add(mouka); weddingCakeRecipe.Ingredients.Add(butter); weddingCakeRecipe.Ingredients .Add(cukr); weddingCakeRecipe.Ingredients.Add(creme); //přidání našeho receptu na dort do nabídky pekárny bakery.AddToMenu(weddingCakeRecipe); //teď si to objednejme!! BirthdayCake dort = bakery.Bake(bakery.FindInMenu("Birthday Cake Luxury")) jako BirthdayCake; //přidání nějakých svíček ;) dort.NumberOfCandles = 10; //a jsme tady !!! Console.WriteLine(cake); ) )

Nyní se znovu podíváme na celý kód a vyhodnotíme jej. Kód je poměrně jednoduchý, typy, jejich abstrakce, data a funkčnost jsou jasně vymezeny, kód umožňuje rozšíření a opětovné použití. Každý segment lze bezbolestně nahradit jiným, který odpovídá základnímu typu, a to nezničí zbytek kódu.

Můžete donekonečna přidávat druhy surovin, recepty na různé druhy cukrářských výrobků, vytvářet další třídy, které popisují pekárny, cukrárny a další podobná zařízení.

Není to špatný výsledek. A to vše díky tomu, že jsme se snažili, aby třídy na sebe minimálně navazovaly.

Nyní se zamysleme nad důsledky porušení principu inverze závislosti:

  1. rigidita (bylo by velmi obtížné v systému provádět nějaké změny, protože každá změna se týkala mnoha jeho různých částí).
  2. křehkost (když jsou provedeny jakékoli změny v jedné části systému, ostatní jeho části se stanou zranitelnými a někdy to není na první pohled příliš patrné).
  3. imobilita (na opětovné použití kódu v jiných systémech můžete zapomenout, protože moduly jsou silně propojeny).

Nyní si udělejte vlastní závěry o tom, jak užitečný je princip inverze závislosti v kódu. Myslím, že odpověď je zřejmá.

14 odpovědí

V zásadě se říká:

  • Abstrakce by nikdy neměly záviset na detailech. Podrobnosti musí záviset na abstrakcích.

Pokud jde o to, proč je to důležité, jedním slovem: změna je riskantní a v závislosti na koncepci, spíše než na implementaci, snížíte potřebu změn na stránkách volání.

DIP účinně snižuje vazbu mezi různými částmi kódu. Myšlenka je taková, že i když existuje mnoho způsobů, jak implementovat, řekněme, logger, způsob, jakým jej používáte, by měl být v průběhu času relativně stabilní. Pokud můžete extrahovat rozhraní, které představuje koncept protokolování, toto rozhraní by mělo být v průběhu času mnohem stabilnější než jeho implementace a volající weby by měly být mnohem méně náchylné ke změnám, které můžete provést údržbou nebo rozšířením tohoto mechanismu protokolování.

Protože implementace je specifická pro rozhraní, máte možnost si za běhu vybrat, která implementace je pro vaše konkrétní prostředí nejvhodnější. V závislosti na případu to může být také zajímavé.

Knihy Agile Software Development, Principles, Patterns and Practices a Agile Principles, Patterns and Practices in C# jsou nejlepšími zdroji pro plné pochopení původních cílů a motivací stojících za Principem Inversion Inversion. Článek „Princip obrácení závislosti“ je také dobrým zdrojem, ale vzhledem k tomu, že se jedná o zhuštěnou verzi návrhu, který se nakonec dostal do výše zmíněných knih, zanechává za sebou některé důležité diskuse o konceptu vlastnictví balíku a rozhraní, která jsou pro ně klíčová Tento princip se liší od obecnější rady „program pro rozhraní, nikoli implementaci“, kterou najdete v knize Design Patterns (Gamma a kol.).

Pro stručné shrnutí, princip inverze závislostí primárně směřuje k změna tradičně směřování závislostí z komponent „vyšší úrovně“ na komponenty „nižší úrovně“, takže komponenty „nižší úrovně“ závisejí na rozhraních, patřící ke komponentám „vyšší úrovně“. (Poznámka: Komponenta „vyšší úrovně“ zde odkazuje na komponentu vyžadující externí závislosti/služby, a nikoli nutně na její koncepční pozici ve vrstvené architektuře.) Vztah však není klesá stejně jako ona směny od komponent, které jsou teoreticky méně hodnotné, po komponenty, které jsou teoreticky hodnotnější.

Toho je dosaženo navržením komponent, jejichž externí závislosti jsou vyjádřeny jako rozhraní, pro které musí spotřebitel komponenty poskytnout implementaci. Jinými slovy, určitá rozhraní vyjadřují to, co komponenta potřebuje, nikoli to, jak komponentu používáte (například „INeedSomething“ spíše než „IDoSomething“).

Princip inverze závislostí neřeší jednoduchý postup abstrahování závislostí pomocí rozhraní (např. MyService -> ). Ačkoli to odděluje komponentu od konkrétního detailu implementace závislosti, neinvertuje to vztah mezi spotřebitelem a závislostí (například ⇐ Logger.

Význam principu inverze závislostí lze zredukovat na jediný cíl – schopnost opakovaně používat softwarové komponenty, které se pro část své funkčnosti (registrace, ověřování atd.) spoléhají na externí závislosti.

V rámci tohoto obecného cíle opětovného použití můžeme rozlišit dva podtypy opětovného použití:

    Používání softwarové komponenty ve více aplikacích s implementacemi závislostí (například jste vyvinuli DI kontejner a chcete poskytovat protokolování, ale nechcete svůj kontejner vázat na konkrétní protokolovací zařízení, takže každý, kdo používá váš kontejner, musí také používat protokolování knihovna, kterou si vyberete).

    Používání softwarových komponent ve vyvíjejícím se kontextu (například jste vyvinuli komponenty obchodní logiky, které zůstávají stejné v různých verzích aplikace, kde se vyvíjejí detaily implementace).

V prvním případě opakovaného použití komponent ve více aplikacích, jako je například knihovna infrastruktury, je cílem poskytnout zákazníkům základní infrastrukturu, aniž by byli spojeni se závislostmi vaší vlastní knihovny, protože získání závislostí z takových závislostí vyžaduje, aby spotřebitelé také vyžadovali stejné závislosti. To může být problematické, když se spotřebitelé vaší knihovny rozhodnou použít jinou knihovnu pro stejné potřeby infrastruktury (jako je NLog a log4net), nebo když se rozhodnou použít pozdější verzi požadované knihovny, která není zpětně kompatibilní s požadovanou verzí. ve vaší knihovně.

V druhém případě opětovného použití komponent obchodní logiky (tj. „komponenty vyšší úrovně“) je cílem izolovat implementaci aplikace v základní oblasti od měnících se potřeb podrobností vaší implementace (např. změna/aktualizace trvalých knihoven, zprávy výměnných knihoven) . šifrovací strategie atd.). V ideálním případě by změna podrobností implementace aplikace neměla narušit komponenty, které zapouzdřují obchodní logiku aplikace.

Poznámka. Někteří mohou mít námitky proti popisu tohoto druhého případu jako skutečného opětovného použití, protože se domnívají, že komponenty, jako jsou komponenty obchodní logiky použité v jedné vývojové aplikaci, představují pouze jedno použití. Myšlenka je zde však taková, že každá změna v detailech implementace aplikace představuje nový kontext, a tedy jiný případ použití, i když konečné cíle lze rozlišit jako izolaci a přenositelnost.

I když ve druhém případě může mít dodržování principu inverze závislostí určité výhody, je třeba poznamenat, že jeho význam při aplikaci na moderní jazyky, jako je Java a C#, byl značně snížen, možná do té míry, že je irelevantní. Jak bylo uvedeno výše, DIP zahrnuje úplné oddělení implementačních detailů do samostatných balíčků. V případě vyvíjející se aplikace však pouhé použití rozhraní definovaných v podmínkách obchodní domény ochrání před potřebou modifikovat komponenty vyšší úrovně kvůli měnícím se potřebám komponent detailů implementace, a to i v případě, že podrobnosti implementace jsou nakonec ve stejném balíčku. . Tato část principu odráží aspekty, které byly relevantní pro jazyk v době jeho kodifikace (například C++), které nejsou relevantní pro novější jazyky. Důležitost Principu inverze závislostí však primárně souvisí s vývojem opakovaně použitelných softwarových komponent/knihoven.

Lze nalézt podrobnější diskusi o tomto principu, pokud jde o jednoduché použití rozhraní, vkládání závislostí a vzor rozděleného rozhraní.

Když vyvíjíme softwarové aplikace, můžeme zvážit třídy nízké úrovně – třídy, které implementují základní a primární operace (přístup k disku, síťové protokoly atd.) a třídy vysoké úrovně – třídy, které zapouzdřují komplexní logiku (obchodní toky,... ) .

Ty druhé spoléhají na třídy nízké úrovně. Přirozeným způsobem implementace takových struktur by bylo psát nízkoúrovňové třídy a kdykoli jsme nuceni psát složité třídy na vysoké úrovni. Protože třídy na vysoké úrovni jsou definovány z pohledu ostatních, zdá se, že je to logický způsob, jak toho dosáhnout. To ale není responzivní design. Co se stane, když potřebujeme nahradit třídu nízké úrovně?

Princip inverze závislosti říká, že:

  • Moduly vysoké úrovně by neměly záviset na modulech nízké úrovně. Obojí musí záviset na abstrakcích.

Tento princip si klade za cíl „převrátit“ obvyklou myšlenku, že moduly vyšší úrovně v softwaru musí záviset na modulech nižší úrovně. Zde moduly na vysoké úrovni vlastní abstrakci (například metody rozhodování o rozhraní), které jsou implementovány moduly nižší úrovně. Moduly nižší úrovně tedy závisí na modulech vyšší úrovně.

Efektivní využití inverze závislostí poskytuje flexibilitu a stabilitu v celé architektuře vaší aplikace. To umožní vaší aplikaci vyvíjet se bezpečněji a stabilněji.

Tradiční vrstvená architektura

Uživatelské rozhraní vrstvené architektury tradičně záviselo na obchodní vrstvě, která zase závisela na vrstvě přístupu k datům.

Musíte rozumět vrstvě, balíčku nebo knihovně. Podívejme se, jak jde kód.

Měli bychom knihovnu nebo balíček pro vrstvu přístupu k datům.

// DataAccessLayer.dll veřejná třída ProductDAO ( )

// BusinessLogicLayer.dll pomocí DataAccessLayer; public class ProductBO ( private ProductDAO productDAO; )

Vrstvená architektura s inverzí závislostí

Inverze závislosti indikuje následující:

Moduly vysoké úrovně by neměly záviset na modulech nízké úrovně. Obojí musí záviset na abstrakcích.

Abstrakce by neměly záviset na detailech. Podrobnosti musí záviset na abstrakcích.

Co jsou moduly vysoké a nízké úrovně? Když přemýšlíme o modulech, jako jsou knihovny nebo balíčky, budou vysokoúrovňové moduly ty, které mají tradičně závislosti, a nízkoúrovňové, na kterých jsou závislé.

Jinými slovy, vysoká úroveň modulu bude tam, kde se akce volá, a nízká úroveň, kde se akce provede.

Z tohoto principu lze vyvodit rozumný závěr: mezi uzly by neměla být žádná závislost, ale měla by existovat závislost na abstrakci. Ale podle přístupu, který volíme, možná používáme investiční závislost nesprávně, ale to je abstrakce.

Představte si, že přizpůsobíme náš kód takto:

Měli bychom knihovnu nebo balíček pro vrstvu přístupu k datům, která definuje abstrakci.

// DataAccessLayer.dll veřejné rozhraní IProductDAO veřejná třída ProductDAO: IProductDAO( )

A další obchodní logika na úrovni knihovny nebo balíčku, která závisí na vrstvě přístupu k datům.

// BusinessLogicLayer.dll pomocí DataAccessLayer; public class ProductBO (soukromý produkt IProductDAO productDAO; )

Přestože jsme závislí na abstrakci, vztah mezi obchodem a přístupem k datům zůstává stejný.

K dosažení inverze závislosti musí být rozhraní persistence definováno v modulu nebo balíčku, kde je umístěna logika nebo doména vysoké úrovně, nikoli v modulu nízké úrovně.

Nejprve definujte, co je doménová vrstva a abstrakce její komunikace je definována perzistencí.

// Domain.dll veřejné rozhraní IProductRepository; pomocí DataAccessLayer; public class ProductBO ( private IProductRepository productRepository; )

Jakmile je úroveň perzistence závislá na doméně, je nyní možné invertovat, pokud je definována závislost.

// Persistence.dll veřejná třída ProductDAO: IProductRepository()

Prohloubení principu

Je důležité dobře porozumět konceptu, prohloubit účel a výhody. Pokud zůstaneme v mechanice a budeme studovat typický repozitář, nebudeme schopni určit, kde můžeme uplatnit princip závislosti.

Ale proč převracíme závislost? Co je hlavním cílem nad rámec konkrétních příkladů?

To je obvykle umožňuje, aby se ty nejstabilnější věci, které jsou nezávislé na věcech méně stabilních, častěji měnily.

Je snazší změnit typ persistence nebo databázi nebo technologii pro přístup ke stejné databázi než doménovou logiku nebo akce navržené pro komunikaci s persistencí. To způsobí, že závislost bude obrácena, protože je snazší změnit trvalost, pokud k této změně dojde. Nebudeme tak muset měnit doménu. Doménová vrstva je nejstabilnější ze všech, takže by neměla na ničem záviset.

Ale existuje více než jen tento příklad úložiště. Existuje mnoho scénářů, ve kterých je tento princip aplikován, a existují architektury založené na tomto principu.

architektura

Existují architektury, ve kterých je inverze závislostí klíčem k její definici. Ve všech doménách je to nejdůležitější a právě abstrakce budou specifikovat komunikační protokol mezi doménou a zbytkem balíčků nebo knihoven.

Čistá architektura

Pro mě princip inverze závislostí popsaný v oficiálním článku

Problém v C++ je, že hlavičkové soubory obvykle obsahují deklarace soukromých polí a metod. Pokud tedy modul C++ na vysoké úrovni obsahuje hlavičkový soubor pro modul nízké úrovně, bude to záviset na skutečnosti implementace podrobnosti o tomto modulu. A to evidentně není moc dobré. Ale to není problém v modernějších jazycích, které se dnes běžně používají.

Moduly na vysoké úrovni jsou ze své podstaty méně znovu použitelné než moduly nízké úrovně, protože moduly nižší úrovně jsou obvykle více specifické pro aplikaci/kontext než moduly nižší úrovně. Například komponenta, která implementuje obrazovku uživatelského rozhraní, je nejvyšší úrovně a také velmi (zcela?) specifická pro aplikaci. Pokus o opětovné použití takové součásti v jiné aplikaci je kontraproduktivní a může vést pouze k nadměrnému vývoji.

Vytvoření samostatné abstrakce na stejné úrovni komponenty A, která závisí na komponentě B (která nezávisí na A), lze tedy provést pouze v případě, že komponenta A bude skutečně užitečná pro opětovné použití v různých aplikacích nebo kontextech. Pokud tomu tak není, pak bude aplikace DIP špatným návrhem.

Jasnější způsob, jak uvést princip inverze závislosti:

Vaše moduly, které zapouzdřují komplexní obchodní logiku, by neměly přímo záviset na jiných modulech, které zapouzdřují obchodní logiku. Místo toho by měly záviset pouze na rozhraních k jednoduchým datům.

To znamená, že místo implementace vaší třídy Logic, jak to lidé obvykle dělají:

Class Dependency ( ... ) class Logic ( private Dependency dep; int doSomething() ( // Obchodní logika pomocí dep zde ) )

měli byste udělat něco jako:

Class Dependency ( ... ) rozhraní Data ( ... ) třída DataFromDependency implementuje Data ( private Dependency dep; ... ) class Logic ( int doSomething(Data data) ( // vypočítat něco s daty ) )

Data a DataFromDependency by měly žít ve stejném modulu jako Logic, nikoli se závislostí.

Proč je to?

Dobré odpovědi a dobré příklady zde již uvedli jiní.

Smyslem inverze závislosti je umožnit opětovné použití softwaru.

Myšlenka je taková, že místo toho, aby se dva kusy kódu spoléhaly na sebe, spoléhají na nějaké abstraktní rozhraní. Poté můžete znovu použít jakoukoli část bez druhé.

Toho je obvykle dosaženo invertováním řídicího kontejneru (IoC), jako je Spring v Javě. V tomto modelu se vlastnosti objektů nastavují pomocí konfigurace XML, spíše než objekty vycházejí a hledají svou závislost.

Představte si tento pseudokód...

Veřejná třída MyClass ( public Service myService = ServiceLocator.service; )

MyClass přímo závisí jak na třídě Service, tak na třídě ServiceLocator. To je vyžadováno pro oba, pokud jej chcete použít v jiné aplikaci. A teď si představ tohle...

Veřejná třída MyClass (veřejná IService myService; )

MyClass nyní používá jedno rozhraní, rozhraní IService. Nechali bychom kontejner IoC skutečně nastavit hodnotu této proměnné.

Ať existuje hotel, který žádá výrobce potravin o své zásoby. Hotel zadá název jídla (řekněme kuře) Generátoru jídla a Generátor vrátí požadované jídlo do hotelu. Ale hotel se nestará o typ jídla, které dostává a podává. Generátor tak dodává do hotelu jídlo označené jako „Jídlo“.

Tato implementace v JAVA

FactoryClass s tovární metodou. Generátor jídla

Public class FoodGenerator ( Food food; public Food getFood(String name)( if(name.equals("fish"))( food = new Fish(); )else if(name.equals("kuře"))( food = nové kuře(); )ostatní jídlo = null; vrátit jídlo; ) )

Anotace/rozhraní třídy

Veřejná abstraktní třída Jídlo ( //Žádný z podřízené třídy nepřepíše tuto metodu, aby byla zajištěna kvalita... public void quality())( String fresh = "Toto je čerstvé " + getName(); String tasty = "Toto je chutné " + getName(); System.out.println(fresh); System.out.println(tasty); ) public abstract String getName(); )

Kuře prodává jídlo (třída betonu)

Public class Chicken rozšiřuje Food ( /*Všechny druhy potravin musí být čerstvé a chutné, takže * Nepřepisují metodu super třídy "property()"*/ public String getName())( return "Kuře"; ))

Ryby prodávají jídlo (specifická třída)

Public class Fish rozšiřuje Food ( /*Všechny typy potravin musí být čerstvé a chutné, takže * Nepřepisují metodu super třídy "property()"*/ public String getName())( return "Fish"; ))

Konečně

Hotel

Public class Hotel ( public static void main(String args)( //Použití třídy Factory.... FoodGenerator foodGenerator = new FoodGenerator(); //Tovární metoda pro vytvoření instance potravin... Food food = foodGenerator.getFood( "kuře"); food.quality(); ) )

Jak vidíte, hotel neví, jestli je to kuře nebo ryba. Ví se pouze, že se jedná o potravinu, tzn. Hotel závisí na třídě jídla.

Můžete si také všimnout, že třída Ryby a kuře implementuje třídu Jídlo a není přímo spojena s hotelem. těch. kuře a ryby také závisí na potravinové třídě.

To znamená, že složka vysoké úrovně (hotel) a složka nízké úrovně (ryby a kuře) závisí na abstrakci (jídlo).

Tomu se říká inverze závislostí.

Princip inverze závislosti (DIP) to říká

i) Vysokoúrovňové moduly by neměly záviset na nízkoúrovňových modulech. Obojí musí záviset na abstrakcích.

ii) Abstrakce by nikdy neměly záviset na detailech. Podrobnosti musí záviset na abstrakcích.

Veřejné rozhraní ICustomer ( řetězec GetCustomerNameById(int id); ) veřejná třída Zákazník: ICustomer ( //ctor public Customer()() veřejný řetězec GetCustomerNameById(int id) ( return "Dummy Customer Name"; ) ) veřejná třída CustomerFactory ( public static ICustomer GetCustomerData() ( return new Customer(); ) ) public class CustomerBLL ( ICustomer _customer; public CustomerBLL() ( _customer = CustomerFactory.GetCustomerData(); ) public string GetCustomerNameById(int id) ( return _customer); ) ) public class Program ( static void Main() ( CustomerBLL customerBLL = new CustomerBLL(); int customerId = 25; řetězec customerName = customerBLL.GetCustomerNameById(customerId); Console.WriteLine(customerName); Console.ReadKey(); ) )

Poznámka. Třída by měla záviset na abstrakcích, jako je rozhraní nebo abstraktní třídy, spíše než na konkrétních detailech (implementace rozhraní).

podíl

Poslední aktualizace: 03/11/2016

Princip inverze závislosti Princip inverze závislosti se používá k vytvoření volně spojených entit, které lze snadno testovat, upravovat a aktualizovat. Tento princip lze formulovat následovně:

Moduly nejvyšší úrovně by neměly záviset na modulech nižší úrovně. Obojí musí záviset na abstrakcích.

Abstrakce by neměly záviset na detailech. Podrobnosti musí záviset na abstrakcích.

Chcete-li pochopit princip, zvažte následující příklad:

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

Třída Book, která představuje knihu, používá k tisku třídu ConsolePrinter. S touto definicí závisí třída Book na třídě ConsolePrinter. Navíc jsme striktně definovali, že tisk knihy lze provést pouze na konzole pomocí třídy ConsolePrinter. Další možnosti, například výstup na tiskárnu, výstup do souboru nebo použití některých prvků grafického rozhraní – to vše je v tomto případě vyloučeno. Knihtisková abstrakce není oddělena od detailů třídy ConsolePrinter. To vše je porušení principu inverze závislosti.

Nyní se pokusme uvést naše třídy do souladu s principem inverze závislostí a oddělit abstrakce od nízkoúrovňové implementace:

Rozhraní IPrinter ( void Print(string text); ) class 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(řetězcový text) ( Console.WriteLine("Print to console"); ) ) class HtmlPrinter: IPrinter ( public void Print(řetězcový text) ( Console.WriteLine("Tisk v html"); ))

Knihtisková abstrakce je nyní oddělena od konkrétních realizací. Výsledkem je, že jak třída Book, tak třída ConsolePrinter závisí na abstrakci IPrinter. Kromě toho nyní můžeme také vytvářet další nízkoúrovňové implementace abstrakce IPrinter a dynamicky je aplikovat v programu:

Kniha kniha = new Book(new ConsolePrinter()); kniha.Tisk(); book.Printer = new HtmlPrinter(); kniha.Tisk();