Magento 2: Najlepsze praktyki w projektowaniu API
Projektowanie API jest ważną częścią tworzenia oprogramowania. Interfejsy API mają bezpośredni wpływ na utrzymywanie systemu i jego rozszerzalności w zależności od punktów ekstrakcji i rozszerzeń wprowadzonych do systemu oraz sposobu ich organizacji w kodzie.
!!! Dzisiejsza notka stanowi tłumaczenie artykułu „Best Practices for API Design„, który autorem jest miniailo.
Ze względu na duży rozmiar Magento 2, wprowadzono warstwę Service Contracts (publicznych interfejsów API) w zakresie każdego Bounded Context (pod względem Domain Driven Design). Publiczne interfejsy API wyraźnie eksponują usługi świadczone przez każdą konkretną domenę biznesową i służą jako punkt wejścia do domeny, odkrywają rolę Fasady, która służy do maskowania złożonej logiki biznesowej za kulisami. W Magento 2 publiczne interfejsy API powinny być umieszczane w oddzielnych modułach. Wszystkie inne moduły zależą od modułu API, podczas gdy sama implementacja może być łatwo podmieniona za pomocą konfiguracji di.xml.
Na potrzeby tego artykułu, skorzystamy z projektu Multi-Source Inventory (MSI). Mamy przykład, jeśli moduł nosi nazwę Inventory, jego interfejsy API są zadeklarowane w module o nazwie InventoryApi.
Wskazówki
Interfejsy serwisów powinny być eksponowane jako Web API (REST/SOAP/GraphQL), są umieszczane w przestrzeni nazw Api. Podczas gdy wszystkie inne interfejsy API, w tym bezpośrednie punkty rozszerzeń, takie jako implementację Chain i Composite, są umieszczane w przestrzeni nazw Model.
W przestrzeni nazw Api\Data Magento przechowuje interfejsy Data Transfer Object (DTO), które reprezentują encję obsługiwane przez usługi zadeklarowanie na poziomie wyższym.
Wszystkie publiczne interfejsy API oznaczone są adnotacją PHP @api. Magento stosuje Semantyczne Wersjonowanie dla modułów zgodnie z założeniem, że moduły nie mają dostępu do prywatnej implementacji innych modułów, a cała integracja powinna być implementowana wyłącznie za pośrednictwem publicznych interfejsów API. Ważne jest, aby zewnętrzni programiści mieli zależności tylko od kodu oznaczonego adnotacją @api, aby ich instancja Magento była łatwa do aktualizacji. W miarę kolejnych wydań, Magento przestrzega kompatybilności wstecznej tylko dla interfejsów API, a tym samym gwarantuje, że publiczne interfejsy API nie zostaną zmienione. Oprócz tego zastrzega sobie prawo do zmiany wszystkich instancji nieoznaczonych jako @api, które są uznawane za implementację modułu prywatnego. W większości przypadków, modyfikacje nieoznaczonego kodu przez @api, spowodują skoki wersji modułu PATCH. Natomiast zmiany w kodzie publicznym wywołują skoki wersji MINOR lub MAJOR.
Takie zróżnicowanie powinno ułatwić życie zarówno sprzedawcom, jak i twórcom rozszerzeń. Obie strony chcą, aby wszystko było jak najbardziej opłacalne. Dla sprzedawców oznacza to, że aktualizacje powinny być tak proste, jak to tylko możliwe, tak samo jak developerzy chcą, aby ich rozszerzenia były kompatybilne wstecz, tak długo, jak to możliwe. Określając zależności tylko w publicznym kontrakcie modułu, sprzedawcy mogą być pewni, że nie napotkają niezgodności z kolejnymi wersjami poprawek, a ich ścieżka aktualizacji pozostanie stabilna.
Interfejs PHP w Magento może być użyty na wiele sposobów, przez twórców produktu podstawowego jak developerów tworzących rozszerzenia.
- Jako API: jest to zestaw interfejsów, jakie moduł udostępnia innym modułom, które mają być wywoływane i wykorzystane do osiągnięcia celu.
- Jako Service Provider Interface (SPI): jest to zestaw interfejsów, który moduł używa wewnętrznie i pozwala na ich rozszerzenie i implementację przez inne moduły.
- Zarówno oba: Interfejsy API jak i SPI nie wykluczają się wzajemnie.
Po wydaniu zarówno API jak i SPI mogą być rozwijane w sposób pozwalający na zachowanie kompatybilność wsteczną. Oba mają jednak swoje specyficzne ograniczenia.
Ograniczenia
Po tym jak API jest wydawane, można dodać nowe zachowanie, ale żadne nie może zostać usunięte. Istniejące zachowanie nie może być modyfikowane.
Przykłady API:
Po wydaniu SPI istniejące zachowanie nie może zostać usunięte, również nowe nie może zostać dodane. Istniejące zachowanie nie zostać zmodyfikowane.
Przykłady SPI:
Łączenie SPI i API
Kiedy interfejs jest zarówno częścią API jak i SPI, nie może być dalej rozwijany.
Aby zapobiec nadmiernemu komplikowaniu rozwoju dla zewnętrznych programistów, Magento nie rozróżnia interfejsów API i SPI. Dlatego jest nadal możliwe, że jedno dostosowań Magento będzie używało określonego interfejsu w sposób API, inne może zapewnić implementację istniejącego interfejsu (użycie SPI). W rezultacie SPI są opatrywane takimi samymi adnotacjami jak interfejsy API. Developerzy zewnętrzni są odpowiedzialny za zdecydowanie, w jaki sposób używają interfejsów zadeklarowanych w modułach zewnętrznych i, w zależności od tych deklaracji, określają zależność w pliku composer.json modułu.
- MAJOR zależności wersji modułu powinna być określona, jeśli programista używa/wywołuje zewnętrzne interfejsy w module (API)
- MAJOR + MINOR wersja powinna być określona, jeśli moduł implementuje interfejs zadeklarowany gdzie indziej.
Przykład pliku composer.json:
- Usuwanie interfejsów/klas,
- Usuwanie metod publicznych i chronionych,
- Wprowadzanie metody do klasy lub interfejsu,
- Usuwanie funkcji statycznych,
- Dodawanie parametrów w metodach publicznych,
- Dodawanie parametrów w metodach chronionych,
- Modyfikacja typu argumentu metody,
- Modyfikacja typów rzucanych wyjątków (chyba że nowy typ wyjątek jest podtypem starego),
- Modyfikacja konstruktora,
- Modyfikowanie domyślnych wartości opcjonalnych argumentów w metodach publicznych i chronionych,
- Usuwanie lub zmiana nazw stałych.
Zobacz DevDocs Backward compatible development, aby nauczyć się więcej o zabronionych zmianach w publicznym kodzie.
Nie ma rozwijania dla interfejsów @api w sposobie Kompatybilności Wstecznej. Każda zmiana naniesiona na interfejs, bez względu na dodanie czy usuwanie zachowania, prowadzi do zepsucia niektórych klientów. Najlepszym sposobem obejścia tych okoliczności jest uczynienie interfejsów tak atomowymi, jak to tylko możliwe. W ten sposób dochodzimy do idei obiektów funkcyjnych (Functors), interfejsu, który składa się z jednej metody.
W takim przypadku, jak interfejs powinien zostać zmodyfikowany lub usunięty, jest on po prostu oznaczany jako @deprecated i wprowadzana jest nowa wersja interfejsu, bez wpływu na inne usługi i metody. Im bardziej szczegółowo zaprojektowana jest usługa, tym mniejszy jest wpływ modyfikacji interfejsu na inne usługi. Dlatego dobre programowanie obiektowe w warstwie usługi jest w zasadzie programowaniem funkcjonalnym:
- Wstrzykiwanie konstruktora, gdy wszystkie zewnętrzne zależności są najpierw konstruowane, a następnie przekazywane jako argumenty konstruktora do inicjalizowanego obiektu.
Wstrzykiwanie konstruktora jest preferowane w stosunku do setter’ów i wstrzykiwanych interfejsów DI, ponieważ może być użyte do zapewnienia, że obiekt klienta jest zawsze w prawidłowym stanie, w przeciwieństwie do sytuacji, gdy niektóre z jego zależności odwołują się do wartości null (nie są ustawione). Może to być pierwszy krok, do stworzenia niezmiennych obiektów. - Stan niezmienny, kiedy stan wewnętrzny nie może zostać zmodyfikowany po utworzeniu obiektu.
- Zasada pojedynczej odpowiedzialności – „Klasa powinna mieć tylko jeden powód do zmiany” Robet C. Martin
- Jednolite interfejsy.
Podstawowy element projektu każdej usługi REST. Ponieważ upraszcza i oddziela architekturę, co umożliwia rozwój każdej części niezależnie, - Obiektu Transferu Danych (DTO) przekazywane pomiędzy usługami.
Obiekty DTO nie posiadają żadnego zachowania i reprezentują kontenery danych przesyłanych kanałami. Nie zawierają żadnej logiki biznesowej, która wymagałaby testowania. Wykorzystanie DTO w komunikacji pomiędzy usługami jest pierwszym krokiem w kierunku architektury mikro usług, która wspiera oddzielenie stosu technologicznego między różnymi kontekstami (Bounded Contexts).
Doprowadzenie tych koncepcji do skrajności, prowadzi do pojedynczych metod, niezmiennych usług, które manipulują za pomocą obiektów DTO.
Posiadając usługi z pojedynczymi metodami, bardzo ważne jest nadanie im odpowiednich nazw. W pożądanym stanie, developerzy powinni być w stanie szybko rzucić okiem na nazwę usługi, aby wiedzieć, za co ta usługa jest odpowiedzialna, bez otwierania listy usług w IDE. Jeszcze lepiej, gdy odpowiedzialność takich samowyjaśniających się interfejsów jest jasna nie tylko dla programistów, ale także dla innych interesariuszy zaangażowanych w projekt. To jest wszechobecny język projektowania oparty na domenie: Domain Driven Design.
Czasami jeśli nie jesteśmy pewni, jaka jest odpowiednia nazwa dla encji lub usługi, prosimy o feedback community:
Takie nazewnictwo może wydawać się nieco nietypowe, ponieważ istnieje wiele zaleceń dotyczących używania rzeczowników do nazw klas reprezentujących typy obiektów. Zasada ta ma zastosowanie dla klas, które reprezentują „rzeczy/encje”. Programiści chcą je nazywać rzeczownikami, a metody wewnątrz tych klas reprezentują działania nad encją, więc metody zwykle reprezentowane są przez czasowniki.
Jednak to podejście nie działa z Functional Objects, które reprezentują „akcję”, więc nazywanie ich czasownikiem jest bardziej odpowiednie.
Dyskutowano o bardziej radykalnym przejściu na obiekty funkcjonalne przy użyciu magicznej metody __invoke.
Poniżej znajdują się plusy i minusy podejścia z __invoke().
Plusy
- Głównym powodem zaproponowania użycia __invoke była eliminacja niepotrzebnych metod execute(), które wyglądają nieco sztucznie i nadmiarowo:
- Brak autouzupełniania w PHP Storm IDE przy dostępie przez $this:
- Testy jednostkowe wyglądają na frustrujące i nieintuicyjne:
- Będziemy mieszać podejścia, ponieważ Magento wciąż ma inne kontrakty serwisowe (repozytoria), które nie mogą być używane jako obiekty funkcyjne, ponieważ zawierają więcej niż jedną metodę.
Po wypełnieniu krótkiej ankiety ze społecznością zdecydowaliśmy się zastosować metodę execute.
Rzadkim wyjątkiem od reguły posiadania jednometodowych interfejsów są repozytoria Magento. Interfejsy repozytoriów są zwykle dostarczane do zarządzania jednostkami domeny, a metody wymienione w tych interfejsach powinny być zgodne z następującą semantyką:
Nie jest to jednak ścisła reguła, a lista metod może być krótsza, jeśli na podstawie wymagań biznesowych dana encja nie ma niektórych operacji. Ważne jest, aby zauważyć, że lista metod w interfejsie Repozytorium NIE może być szersza niż metody wymienione powyżej, ponieważ nie zaleca się dodawania innych metod z własną semantyką do interfejsu Repozytorium (metody zaleca się umieścić w dedykowanych Usługach).
Repozytoria w Magento 2 są uważane za implementację wzorca Fasada, która zapewnia uproszczony interfejs, dla większej części kodu, odpowiedzialny za zarządzanie Encją Domeny. Głównym założeniem było uczynienie API bardziej czytelnym i zmniejszenie zależności kodu logiki biznesowej od wewnętrznego działania modułu, ponieważ większość kodu korzysta z fasady, co pozwala na większą elastyczność w rozwoju systemu.
Jednak wewnętrznie Repozytoria nadal pośredniczą w połączeniach z dedykowanymi usługami poleceń.
Każda Usługa powinna być zgodna z semantyką CQRS i reprezentować polecenia (ang. Command) lub zapytanie (ang. Query), ale nie oba.
- Zapytania: Zwracają wynik i nie zmieniają obserwowanego stanu systemu (są wolne od efektów ubocznych).
- Polecenia: Zmieniają stan systemu, ale nie zwracają wartości (typ zwracany void).
Więcej informacji i dokumentacja
Więcej przykładów projektowania Service Contracts dla Magento 2 można znaleźć w zakresie projektu Multi-Source Inventory (MSI). Wiki projektu zawiera informacje i dyskusję na temat najciekawszych zmian architektonicznych wdrażających te najlepsze praktyki i Service Contracts.
Zalecamy zapoznanie się z dokumentem wzorca Magento Service Isolation, który opisuje podejście Modułowości wprowadzonej do bazy kodu Magento, które pomogłoby podzielić monolit na niezależny i odizolowany zestaw Usług. Zobacz DevDocs Service Contracts w Technical Guidelines, aby zapoznać się z wytycznymi i wymaganiami dotyczącymi rozwoju.