De Schoonheid van Dependency Injection in Magento 2

Magento 2 heeft nog wat tekortkomingen, maar het Dependency Injection systeem hoort daar niet bij. Het werd ontworpen als een simpel, elegant en krachtig systeem. In dit artikel vertel ik er alles over en waarom ik vind dat het mooi is.

Click here for an English translation

Een voorbeeld

Als je niet bekend bent met Dependency Injection (DI), laat ik het zien met een voorbeeld uit Magento. Let vooral op de constructor:

Zoals je kunt zien in de constructor, is het niet mogelijk om het EmailSender object aan te maken zonder eerst een paar andere objecten aan te maken: een message manager (voor meldingen aan de gebruiker), een logger (voor meldingen aan de system log) en een order verzender (de actor in deze klasse). Dit zijn alle klassen die EmailSender nodig heeft om zijn functie te vervullen: zijn dependencies (afhankelijkheden). De EmailSender klasse maakt geen van zijn dependencies zelf aan, noch roept het de statische functies van een andere klasse aan. Alle klassen die het gebruikt worden geïnjecteerd (ingevoegd) via de constructor. Nu, als een nieuwe EmailSender nodig is, maakt de applicatie-code deze niet zelf aan; het is onderdeel van het framework om ze dynamisch aan te maken. Dit, in het kort, is Magento’s implementatie van dependency injection.

Hoe werkt het?

Alle dependencies van een klasse worden opgesomd als argumenten van de constructor. Als Magento een instantie van een klasse of interface nodig heeft, vraagt het ObjectManager om het te maken aan de hand van de fully qualified name. ObjectManager is een zogenaamde Dependency Injection Container. Het instantieert de dependencies voor een nieuw object, en zorgt er voor dat ieder dependency object slechts een keer wordt aangemaakt. Merk op dat de ObjectManager geen vaste lijst met dependencies voor iedere klasse heeft, die handmatig bijgewerkt moet worden zodra de klasse verandert. De dependencies worden dynamisch uit de klasse gelezen, via reflection, en dan gecachet.

ObjectManager controleert of er een preference (voorkeur) aanwezig is voor het gewenste type, en zo ja, vervangt het gevraagde type door het geprefereerde type. Het doet dit recursief. Nadat het een specifiek type heeft bepaald voor het gevraagde type, doet het dit ook voor alle beschikbare argumenten van de constructor van de klasse. Als alle argumenten geïnstantieerd zijn, wordt een nieuw object aangemaakt.

Merk op dat preferences optioneel zijn. Als er geen preference beschikbaar is voor een class name, zal de gevraagde klasse zelf geïnstantieerd worden. Als je probeert een interface te instantiëren zonder dat er een preference aanwezig is, leidt dit tot een fatal error. ObjectManager detecteert ook circulaire preference-afhankelijkheden.

Honderden preferences zijn voorgedefinieerd door Magento. Je kunt je eigen preferences toevoegen via di.xml bestanden. Elke module kan een globaal di.xml bestand hebben (in etc) of een bestand dat specifiek is voor een area (in etc/frontend of etc/adminhtml). Hier is een voorbeeld:

De originele klasse of interface wordt getoond als name in het type element. Het geprefereerde type wordt getoond as type in het plugin element.

Moet ik het gebruiken?”

DI is een Magento 2 idioom. Hoewel het waar is dat het aanmaken van objecten via DI meer werk is dan ze direct aan te maken, dien je jezelf toch aan te leren om DI te gebruiken.

Het is natuurlijk mogelijk om objecten aan te maken via PHP’s new operator. En het is mogelijk om ObjectManager’s create() methode direct aan te roepen. Beperk deze praktijken tot een minimum. Hier is een lijst met argumenten van Magento’s architect, Alan Kent, ter overweging. Het kan je opvallen dat Magento ObjectManager soms zelf aanroept zonder noodzaak. Dat betekent niet dat Magento dit goedkeurt.

Bovendien, het gebruik van dependency injection is mooier dan expliciete instantiatie. Meer hierover later in dit artikel.

Zou Magento het anders hebben kunnen doen?

Magento 1 maakte veel gebruik van een “God object” genaamd Mage. Het vormde de toegang naar een groot gedeelte van Magento’s functionaliteit.

Magento zou dit patroon gehandhaafd kunnen hebben. Het is een vorm van ambient context, een vaste centrale plaats die alle cross-cutting concerns van een system bedient: de functies die nodig zijn in bijna elke deel van het systeem. Het is zelfs waarschijnlijk dat de keuze om dit patroon los te laten één van de belangrijkste redenen was om van Magento 2 een geheel nieuw systeem te maken.

Magento zou ook een service locator object hebben kunnen maken, een centrale registry geïmplementeerd als een enkel object, dat het mogelijk maakt om dependencies toe te voegen en te verwijderen terwijl het programma wordt uitgevoerd. Dit locator object zou dan worden doorgegeven door de hele application. In Magento 2, worden alle objecten gebouwd en gelinkt voordat de application start.

In plaats van constructor injectie zou Magento ook setter injection of interface injection kunnen gebruiken. Constructor injectie is robuuster dan deze twee, omdat de dependencies zeker bestaan voordat de applicatie begint.

Een van de gevolgen van constructor injectie is dat het  circulaire afhankelijkheden onmogelijk maakt. Hoewel dit helpt om code te ontkoppelen (decouple), kan het problemen veroorzaken in de core van het framework. Dit zou een reden kunnen zijn dat DI niet consistent in het framework is doorgevoerd, en dat ObjectManager soms expliciet wordt aangeroepen.

Ze zouden ontwikkelaars hebben kunnen dwingen om een di.xml item te maken voor iedere nieuwe klasse die wordt toegevoegd. En ze zouden class-level docblock informatie hebben kunnen gebruiken voor de dependency information.

Als laatste, Magento’s developers zouden een heel stricte version van DI kunnen gebruiken, die alleen interfaces zou toestaan voor constructor argument typen. Dat deden ze allemaal niet. Magento staat abstracte klassen toe en zelfs concrete klassen. Dit is een pragmatisch aanpak.

Het Dependency Inversion Principle

Dependency Injection is een implementatie van het Dependency Inversion Principle dat zegt:

A. Modules van een hoger niveau zouden niet afhankelijk moeten zijn van modulen van een lager niveau. Beide zouden afhankelijk moeten zijn van abstracties.
B. Abstracties zouden niet afhankelijk moeten zijn van details. Details zouden afhankelijk moeten zijn van abstracties.

Door de (lagere) dependencies te injecteren, en door interfaces of abstracte klassen als typen te gebruiken, open je iedere klasse voor een bredere reeks mogelijkheden, omdat ieder type vervangen kan worden door verschillende implementaties. Magento, een open framework, past dit als een handschoen.

DI in templates

Hoewel het redelijk duidelijk is hoe je DI in klassen gebruikt, is dit minder het geval op andere plaatsen. Templates (.phtml files), bijvoorbeeld. Om een extern object in een template te gebruiken, dien je het block te gebruiken dat aan de template gekoppeld is. Als de template is gekoppeld aan een Magento core block, maak dan een subklasse van dat block, verwijs hiernaar, en wijzig zijn klasse. Ja, het is veel werk, dat valt niet te ontkennen. 🙂 Hier volgt een voorbeeld, dat een method getCustomerSession levert.

In de template kun je dan gebruiken:

“Wat als ik echt een nieuw object wil?”

In sommige gevallen is het onwenselijk om een object te delen met andere delen van de code. Bijvoorbeeld, om producten te queryen, kun je een product collection gebruiken. Het wordt geïmplementeerd in \Magento\Catalog\Model\ResourceModel\Product\Collection. Je weet echter dat je dit collectie-object niet wilt delen met sommige andere delen van het request. Elk deel wil zijn eigen filters toevoegen. Je wilt een nieuw Collection object. Dit is de reden waarom factories zijn geïntroduceerd. Een \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory produceert Collections. Dus, in plaats van een Collection, injecteer je een CollectionFactory. Daarna vraag je de factory om een nieuwe collectie voor je te maken.

Contexten

Sommige klassen, door hun centrale positie in the framework, trekken veel dependencies aan. Als een group dependencies vaak samen gebruikt worden, worden ze gebundeld in een context. Zo’n context kan dan worden doorgegeven als een enkele entiteit via de constructor. Hier is een voorbeeld van het gebruik van de product context:

Om deze context te onderscheiden van de ambient context, kunnen we het een lokale context noemen. Het is geen service locator.

“Waar vind ik de dependencies die ik nodig heb?”

Als je een nieuwe klasse maakt en je hebt Magento’s functionaliteit nodig hebt, moet je er naar zoeken. Magento verschaft geen how-to’s voor specifieke use cases. Echter, als je een functie van een module nodig hebt, bekijk dat diens api directory. Deze directory bevat de klassen die de service contracts vormen van of the module. Deze klassen zijn speciaal ontworpen om het maken van een applicatie te vergemakkelijken, en ze zijn stabieler dan andere module-klassen.

Service contracts, echter, bieden nog geen toegang tot de volledige functionaliteit van Magento. Dit zal verbeteren, maar in tussentijd is Magento Stack Exchange is je beste kans op een antwoord.

Wat maakt het zo mooi?

Ik gebruik schoonheid in de zin van Beautiful Code, een boek met verhalen over programmeren, bewerkt door Andy Oram & Greg Wilson (2007)

“I saw that programs could be more than just instructions for computers. They could be as elegant as well-made kitchen cabinets, as graceful as a suspension bridge, or as eloquent as one of George Orwell’s essays”

“For some, it lives in the small details of elegantly crafted software. Others find beauty in the big picture – in how a program’s structure allows it to evolve gracefully over time, or in the techniques used to build it.”

Ik vind dat Magento 2’s Dependency Injection framework mooi is omdat het mogelijk maakt elke klasse van het framework als een onafhankelijke entiteit te beschouwen, onafhankelijk van de aanwezigheid van het hele framework. It schept uitbreidbaarheid voor iedere klasse en faciliteert object herbruik binnen ieder request. Het vergemakkelijkt parallele software ontwikkeling, verbetert de onderhoudbaarheid, en maakt het veel makkelijker om klassen te testen. Maar bovenal, het is consistent doorgevoerd in het hele framework, als een architectureel patroon. Daarin ligt de schoonheid.

Wil je meer weten?

Lees Mark Seemann’s boek over het onderwerp: Dependency Injection in .Net. Het biedt een grondig inzicht in DI. Het grootste deel hiervan heeft ook betrekking op PHP.

Je kunt ook bekijken Magento 2 developer documentation over het onderwerp. En, als geadvanceerd onderwerp, Alan Storm over virtual types.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.