9.1 Inversie van afhankelijkheid

Weet je nog dat we ooit zeiden dat je in een servertoepassing niet zomaar streams kunt maken via new Thread().start()? Alleen de container mag threads maken. Dit idee gaan we nu nog verder uitwerken.

Alle objecten mogen ook alleen door de container worden gemaakt . We hebben het natuurlijk niet over alle objecten, maar over de zogenaamde bedrijfsobjecten. Ze worden ook vaak bakken genoemd. De benen van deze benadering groeien vanuit het vijfde principe van SOLID, dat vereist dat klassen worden verwijderd en naar interfaces wordt verplaatst:

  • Modules op het hoogste niveau mogen niet afhankelijk zijn van modules op een lager niveau. Zowel die als andere zouden afhankelijk moeten zijn van abstracties.
  • Abstracties mogen niet afhankelijk zijn van details. De uitvoering moet afhangen van de abstractie.

Modules mogen geen verwijzingen naar specifieke implementaties bevatten en alle afhankelijkheden en interacties daartussen moeten uitsluitend op basis van abstracties (dat wil zeggen interfaces) worden gebouwd. De essentie van deze regel kan in één zin worden geschreven: alle afhankelijkheden moeten de vorm hebben van interfaces .

Ondanks zijn fundamentele aard en schijnbare eenvoud, wordt deze regel het vaakst overtreden. Namelijk elke keer dat we de nieuwe operator in de code van het programma/de module gebruiken en een nieuw object van een specifiek type maken, wordt dus, in plaats van afhankelijk te zijn van de interface, de afhankelijkheid van de implementatie gevormd.

Het is duidelijk dat dit niet te vermijden is en dat er ergens objecten gecreëerd moeten worden. Maar u moet op zijn minst het aantal plaatsen minimaliseren waar dit wordt gedaan en waarin klassen expliciet worden gespecificeerd, en dergelijke plaatsen lokaliseren en isoleren zodat ze niet door de programmacode worden verspreid.

Een zeer goede oplossing is het gekke idee om de creatie van nieuwe objecten te concentreren binnen gespecialiseerde objecten en modules - fabrieken, servicelocators, IoC-containers.

In zekere zin volgt een dergelijke beslissing het Single Choice-principe, dat zegt: "Wanneer een softwaresysteem veel alternatieven moet ondersteunen, mag hun volledige lijst slechts bij één module van het systeem bekend zijn" .

Daarom, als het in de toekomst nodig is om nieuwe opties toe te voegen (of nieuwe implementaties, zoals in het geval van het maken van nieuwe objecten die we overwegen), dan is het voldoende om alleen de module bij te werken die deze informatie bevat, en alle andere modules blijven onaangetast en kunnen hun werk gewoon voortzetten.

voorbeeld 1

new ArrayList In plaats van iets als te schrijven , zou het logisch zijn List.new()als de JDK u de juiste implementatie van een blad geeft: ArrayList, LinkedList of zelfs ConcurrentList.

De compiler ziet bijvoorbeeld dat er aanroepen zijn naar het object vanuit verschillende threads en plaatst daar een thread-safe implementatie. Of teveel inserts in het midden van het blad, dan wordt de implementatie gebaseerd op LinkedList.

Voorbeeld 2

Dit is bijvoorbeeld al met soorten gebeurd. Wanneer heb je voor het laatst een sorteeralgoritme geschreven om een ​​collectie te sorteren? In plaats daarvan gebruikt nu iedereen de methode Collections.sort()en moeten de elementen van de collectie de Comparable-interface (comparable) ondersteunen.

Als sort()u een verzameling van minder dan 10 elementen aan de methode doorgeeft, is het heel goed mogelijk om deze te sorteren met een bubbelsortering (Bubble sort) en niet met Quicksort.

Voorbeeld 3

De compiler kijkt al hoe je strings samenvoegt en zal je code vervangen door StringBuilder.append().

9.2 Inversie van afhankelijkheid in de praktijk

Nu het meest interessante: laten we nadenken over hoe we theorie en praktijk kunnen combineren. Hoe kunnen modules hun "afhankelijkheden" correct creëren en ontvangen en Dependency Inversion niet schenden?

Om dit te doen, moet u bij het ontwerpen van een module zelf beslissen:

  • wat de module doet, welke functie hij vervult;
  • dan heeft de module van zijn omgeving nodig, dat wil zeggen met welke objecten / modules hij te maken zal krijgen;
  • En hoe komt hij eraan?

Om te voldoen aan de principes van Dependency Inversion, moet u zeker beslissen welke externe objecten uw module gebruikt en hoe deze er naar verwijst.

En hier zijn de volgende opties:

  • de module maakt zelf objecten aan;
  • de module haalt objecten uit de container;
  • de module heeft geen idee waar de objecten vandaan komen.

Het probleem is dat om een ​​object te maken, je een constructor van een specifiek type moet aanroepen, en als gevolg daarvan zal de module niet afhankelijk zijn van de interface, maar van de specifieke implementatie. Maar als we niet willen dat objecten expliciet in de modulecode worden gemaakt, kunnen we het Factory Method- patroon gebruiken .

"Het komt erop neer dat in plaats van een object direct te instantiëren via new, we de clientklasse een interface bieden om objecten te maken. Aangezien een dergelijke interface altijd kan worden overschreven met het juiste ontwerp, krijgen we enige flexibiliteit bij het gebruik van low-level modules in modules op hoog niveau" .

In gevallen waarin het nodig is om groepen of families van gerelateerde objecten te maken, wordt een abstracte fabriek gebruikt in plaats van een fabrieksmethode .

9.3 De servicezoeker gebruiken

De module neemt de benodigde objecten van degene die ze al heeft. Aangenomen wordt dat het systeem een ​​bepaalde repository met objecten heeft, waarin modules hun objecten kunnen "plaatsen" en objecten uit de repository kunnen "nemen".

Deze benadering wordt geïmplementeerd door het Service Locator-patroon , waarvan het belangrijkste idee is dat het programma een object heeft dat weet hoe het alle afhankelijkheden (services) kan krijgen die nodig kunnen zijn.

Het belangrijkste verschil met fabrieken is dat Service Locator geen objecten aanmaakt, maar eigenlijk al geconcretiseerde objecten bevat (of weet waar/hoe ze te krijgen, en als het aanmaakt, dan maar één keer bij de eerste aanroep). De fabriek maakt bij elke oproep een nieuw object waar je het volledige eigendom van krijgt en je kunt ermee doen wat je wilt.

Belangrijk ! De service locator produceert verwijzingen naar dezelfde reeds bestaande objecten . Daarom moet u zeer voorzichtig zijn met de objecten die door de Service Locator worden uitgegeven, aangezien iemand anders ze tegelijkertijd met u kan gebruiken.

Objecten in de Service Locator kunnen rechtstreeks via het configuratiebestand worden toegevoegd, en wel op elke manier die handig is voor de programmeur. De Service Locator zelf kan een statische klasse zijn met een set statische methoden, een singleton of een interface, en kan via een constructor of methode aan de vereiste klassen worden doorgegeven.

De Service Locator wordt wel eens een antipatroon genoemd en wordt afgeraden (omdat het impliciete verbanden legt en alleen de schijn wekt van een goed ontwerp). Je kunt meer lezen van Mark Seaman:

9.4 Injectie van afhankelijkheid

De module geeft helemaal niets om "mining"-afhankelijkheden. Het bepaalt alleen wat het nodig heeft om te werken, en alle benodigde afhankelijkheden worden van buitenaf door iemand anders aangeleverd (geïntroduceerd).

Dit is wat wordt genoemd - Dependency Injection. Meestal worden de vereiste afhankelijkheden doorgegeven als constructorparameters (Constructor Injection) of via klassemethoden (Setter-injectie).

Deze aanpak keert het proces van het creëren van afhankelijkheden om - in plaats van de module zelf wordt het maken van afhankelijkheden gecontroleerd door iemand van buitenaf. De module van de actieve zender van objecten wordt passief - niet hij creëert, maar anderen creëren voor hem.

Deze richtingsverandering wordt de Inversion of Control of het Hollywood-principe genoemd: "Bel ons niet, wij bellen u."

Dit is de meest flexibele oplossing, waardoor de modules de grootste autonomie krijgen . We kunnen zeggen dat alleen het het "Single Responsibility Principle" volledig implementeert - de module moet volledig gefocust zijn op het goed doen van zijn werk en zich nergens zorgen over maken.

De module voorzien van alles wat nodig is voor werk is een aparte taak, die moet worden afgehandeld door de juiste "specialist" (meestal is een bepaalde container, een IoC-container, verantwoordelijk voor het beheer van afhankelijkheden en de implementatie ervan).

Eigenlijk is alles hier zoals in het leven: in een goed georganiseerd bedrijf programmeren programmeurs, en de bureaus, computers en alles wat ze nodig hebben voor het werk worden gekocht en geleverd door de officemanager. Of, als je de metafoor van het programma als constructor gebruikt, de module moet niet aan draden denken, iemand anders is betrokken bij het samenstellen van de constructor, en niet de onderdelen zelf.

Het zou niet overdreven zijn om te zeggen dat het gebruik van interfaces om afhankelijkheden tussen modules te beschrijven (Dependency Inversion) + de juiste creatie en injectie van deze afhankelijkheden (voornamelijk Dependency Injection) sleuteltechnieken zijn voor ontkoppeling .

Ze dienen als de basis waarop de losse koppeling van de code, de flexibiliteit, weerstand tegen veranderingen, hergebruik en zonder welke alle andere technieken weinig zin hebben. Dit is de basis van losse koppeling en goede architectuur.

Het principe van Inversion of Control (samen met Dependency Injection en Service Locator) wordt in detail besproken door Martin Fowler. Er zijn vertalingen van zijn beide artikelen: "Inversion of Control Containers and the Dependency Injection pattern" en "Inversion of Control" .