Problem inicjalizacji obiektów w aplikacjach OOP w PHP. Znalezienie rozwiązania przy użyciu rejestru, metody fabrycznej, lokalizatora usług i wzorców wstrzykiwania zależności. Wzorce OOP z przykładami i opisami Autorytatywny rejestr php

Problem inicjalizacji obiektów w aplikacjach OOP w PHP. Znalezienie rozwiązania przy użyciu rejestru, metody fabrycznej, lokalizatora usług i wzorców wstrzykiwania zależności

Tak się składa, że ​​programiści konsolidują udane rozwiązania w postaci wzorców projektowych. Literatury na temat wzorców jest mnóstwo. Książka Gangu Czterech „Wzory projektowe” Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa oraz być może „Patterns of Enterprise Application Architecture” Martina Fowlera są z pewnością uważane za klasykę. Najlepsza rzecz, jaką przeczytałem z przykładami w PHP - to.Tak się składa, że ​​cała ta literatura jest dość skomplikowana dla osób, które dopiero zaczynają opanowywać OOP.Więc wpadłem na pomysł przedstawienia niektórych wzorców, które uważam za najbardziej przydatne, w mocno uproszczonej formie.W innych Słowem, ten artykuł jest moją pierwszą próbą interpretacji wzorców projektowych w stylu KISS.
Dzisiaj porozmawiamy o tym, jakie problemy mogą pojawić się podczas inicjalizacji obiektu w aplikacji OOP i jak można wykorzystać popularne wzorce projektowe do rozwiązania tych problemów.

Przykład

Nowoczesna aplikacja OOP współpracuje z dziesiątkami, setkami, a czasami tysiącami obiektów. Cóż, przyjrzyjmy się bliżej, jak te obiekty są inicjowane w naszych aplikacjach. Inicjalizacja obiektu to jedyny aspekt, który nas interesuje w tym artykule, dlatego zdecydowałem się pominąć całą „dodatkową” implementację.
Załóżmy, że stworzyliśmy przydatną klasę superduperową, która może wysłać żądanie GET do określonego URI i zwrócić HTML z odpowiedzi serwera. Aby nasza klasa nie wydawała się zbyt prosta, niech również sprawdzi wynik i zgłosi wyjątek, jeśli serwer odpowie „niepoprawnie”.

Class Grabber (funkcja publiczna get($url) (/** zwraca kod HTML lub zgłasza wyjątek */) )

Stwórzmy kolejną klasę, której obiekty będą odpowiedzialne za filtrowanie otrzymanego kodu HTML. Metoda filter przyjmuje jako argumenty kod HTML i selektor CSS i zwraca tablicę elementów znalezionych dla danego selektora.

Klasa HtmlExtractor ( funkcja publiczna filter($html, $selector) (/** zwraca tablicę przefiltrowanych elementów */) )

Teraz wyobraźmy sobie, że musimy uzyskać wyniki wyszukiwania w Google dla podanych słów kluczowych. W tym celu wprowadzimy kolejną klasę, która będzie wykorzystywała klasę Grabber do wysłania żądania oraz klasę HtmlExtractor do wyodrębnienia potrzebnej treści. Będzie zawierał także logikę konstruowania identyfikatora URI, selektor służący do filtrowania otrzymanego kodu HTML i przetwarzania uzyskanych wyników.

Klasa GoogleFinder ( prywatny $grabber; prywatny $filter; funkcja publiczna __construct() ( $this->grabber = new Grabber(); $this->filter = new HtmlExtractor(); ) funkcja publiczna find($searchString) ( /* * zwraca tablicę ustalonych wyników */) )

Czy zauważyłeś, że inicjalizacja obiektów Grabber i HtmlExtractor odbywa się w konstruktorze klasy GoogleFinder? Zastanówmy się, jak dobra jest to decyzja.
Oczywiście kodowanie tworzenia obiektów w konstruktorze nie jest dobrym pomysłem. I własnie dlatego. Po pierwsze, nie będziemy w stanie łatwo zastąpić klasy Grabber w środowisku testowym, aby uniknąć wysyłania prawdziwego żądania. Aby być uczciwym, warto powiedzieć, że można to zrobić za pomocą Reflection API. Te. istnieje techniczna możliwość, ale nie jest to najwygodniejszy i najbardziej oczywisty sposób.
Po drugie, ten sam problem pojawi się, jeśli będziemy chcieli ponownie wykorzystać logikę GoogleFindera z innymi implementacjami Grabbera i HtmlExtractora. Tworzenie zależności jest zakodowane na stałe w konstruktorze klasy. A w najlepszym wypadku będziemy mogli odziedziczyć GoogleFindera i zastąpić jego konstruktora. Nawet wtedy, tylko jeśli zakres właściwości grabbera i filtru jest chroniony lub publiczny.
I ostatnia kwestia: za każdym razem, gdy tworzymy nowy obiekt GoogleFinder, w pamięci zostanie utworzona nowa para obiektów zależności, chociaż możemy dość łatwo użyć jednego obiektu Grabber i jednego obiektu HtmlExtractor w kilku obiektach GoogleFinder.
Myślę, że już rozumiesz, że inicjalizację zależności należy przenieść poza klasę. Możemy zażądać przekazania już przygotowanych zależności konstruktorowi klasy GoogleFinder.

Klasa GoogleFinder ( prywatny $grabber; prywatny $filter; funkcja publiczna __construct(Grabber $grabber, HtmlExtractor $filter) ( $this->grabber = $grabber; $this->filter = $filter; ) funkcja publiczna find($searchString) ( /** zwraca tablicę ustalonych wyników */) )

Jeśli chcemy dać innym programistom możliwość dodawania i używania własnych implementacji Grabbera i HtmlExtractora, to powinniśmy rozważyć wprowadzenie dla nich interfejsów. W tym przypadku jest to nie tylko przydatne, ale także konieczne. Uważam, że jeśli w projekcie wykorzystujemy tylko jedną implementację i nie spodziewamy się w przyszłości tworzyć nowych, to powinniśmy odmówić stworzenia interfejsu. Lepiej działać stosownie do sytuacji i przeprowadzić prostą refaktoryzację, gdy zaistnieje realna potrzeba.
Teraz mamy już wszystkie niezbędne klasy i możemy skorzystać z klasy GoogleFinder w kontrolerze.

Kontroler klasy ( funkcja publiczna akcja() ( /* Trochę rzeczy */ $finder = new GoogleFinder(new Grabber(), new HtmlExtractor()); $results = $finder->

Podsumujmy wyniki pośrednie. Napisaliśmy bardzo mało kodu i na pierwszy rzut oka nie zrobiliśmy nic złego. Ale... co jeśli będziemy musieli użyć obiektu takiego jak GoogleFinder w innym miejscu? Będziemy musieli zduplikować jego stworzenie. W naszym przykładzie jest to tylko jedna linia i problem nie jest tak zauważalny. W praktyce inicjowanie obiektów może być dość złożone i może zająć do 10 linii lub nawet więcej. Pojawiają się także inne problemy typowe dla powielania kodu. Jeśli podczas procesu refaktoryzacji zajdzie potrzeba zmiany nazwy używanej klasy lub logiki inicjalizacji obiektu, będziesz musiał ręcznie zmienić wszystkie miejsca. Myślę, że wiesz jak to się dzieje :)
Zwykle z twardym kodem radzi się sobie prosto. Zduplikowane wartości są zwykle uwzględniane w konfiguracji. Pozwala to na centralną zmianę wartości we wszystkich miejscach, w których są używane.

Szablon rejestru.

Postanowiliśmy więc przenieść tworzenie obiektów do konfiguracji. Zróbmy to.

$rejestr = nowy obiekt Array(); $rejestr["grabber"] = nowy Grabber(); $rejestr["filter"] = nowy HtmlExtractor(); $registry["google_finder"] = nowy GoogleFinder($registry["grabber"], $registry["filter"]);
Wystarczy, że przekażemy nasz obiekt ArrayObject do kontrolera i problem zostanie rozwiązany.

Kontroler klasy ( prywatny $rejestr; funkcja publiczna __construct(ArrayObject $rejestr) ( $this->registry = $rejestr; ) akcja funkcji publicznej() ( /* Niektóre rzeczy */ $results = $this->registry["google_finder" ]->find("szukany ciąg"); /* Zrób coś z wynikami */ ) )

Możemy dalej rozwijać ideę Rejestru. Dziedzicz ArrayObject, hermetyzuj tworzenie obiektów w nowej klasie, zabraniaj dodawania nowych obiektów po inicjalizacji itp. Jednak moim zdaniem podany kod w pełni wyjaśnia, czym jest szablon Rejestru. Ten wzorzec nie jest generatywny, ale w pewnym stopniu pomaga rozwiązać nasze problemy. Rejestr to po prostu kontener, w którym możemy przechowywać obiekty i przekazywać je w obrębie aplikacji. Aby obiekty stały się dostępne musimy je najpierw stworzyć i zarejestrować w tym kontenerze. Przyjrzyjmy się zaletom i wadom tego podejścia.
Na pierwszy rzut oka osiągnęliśmy swój cel. Przestaliśmy kodować nazwy klas na stałe i tworzymy obiekty w jednym miejscu. Obiekty tworzymy w jednym egzemplarzu, co gwarantuje ich ponowne wykorzystanie. Jeżeli zmieni się logika tworzenia obiektów, wówczas edycji będzie wymagało tylko jedno miejsce w aplikacji. Jako bonus otrzymaliśmy możliwość centralnego zarządzania obiektami w Rejestrze. Z łatwością możemy uzyskać listę wszystkich dostępnych obiektów i wykonać na nich pewne manipulacje. Przyjrzyjmy się teraz, co może nam się nie spodobać w tym szablonie.
W pierwszej kolejności musimy stworzyć obiekt przed zarejestrowaniem go w Rejestrze. W związku z tym istnieje duże prawdopodobieństwo powstania „przedmiotów niepotrzebnych”, tj. takie, które zostaną utworzone w pamięci, ale nie zostaną wykorzystane w aplikacji. Tak, możemy dodawać obiekty do Rejestru dynamicznie, tj. twórz tylko te obiekty, które są potrzebne do przetworzenia konkretnego żądania. Tak czy inaczej będziemy musieli kontrolować to ręcznie. W związku z tym z czasem stanie się bardzo trudne w utrzymaniu.
Po drugie, mamy nową zależność kontrolera. Tak, możemy odbierać obiekty metodą statyczną w Rejestrze, dzięki czemu nie musimy przekazywać Rejestru konstruktorowi. Jednak moim zdaniem nie powinnaś tego robić. Metody statyczne są jeszcze ściślejszym połączeniem niż tworzenie zależności wewnątrz obiektu i trudności w testowaniu (o tym temacie).
Po trzecie, interfejs kontrolera nie mówi nam nic o obiektach, których używa. Możemy uzyskać dowolny obiekt dostępny w Rejestrze w kontrolerze. Trudno nam będzie powiedzieć, z jakich obiektów korzysta kontroler, dopóki nie sprawdzimy całego jego kodu źródłowego.

Metoda fabryczna

Naszym największym problemem związanym z Rejestrem jest to, że obiekt musi zostać zainicjowany, zanim będzie można uzyskać do niego dostęp. Zamiast inicjować obiekt w konfiguracji, możemy rozdzielić logikę tworzenia obiektów na inną klasę, którą możemy „poprosić” o zbudowanie potrzebnego nam obiektu. Klasy odpowiedzialne za tworzenie obiektów nazywane są fabrykami. A wzorzec projektowy nazywa się Metodą fabryczną. Spójrzmy na przykładową fabrykę.

Class Factory ( funkcja publiczna getGoogleFinder() (zwróć nowy GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); ) funkcja prywatna getGrabber() (zwróć nowy Grabber(); ) funkcja prywatna getHtmlExtractor() ( zwróć nowy plik HtmlFiletr(); ) )

Z reguły powstają fabryki, które odpowiadają za stworzenie jednego rodzaju obiektu. Czasami fabryka może utworzyć grupę powiązanych obiektów. Możemy użyć buforowania we właściwości, aby uniknąć ponownego tworzenia obiektów.

Class Factory ( prywatna $finder; funkcja publiczna getGoogleFinder() ( if (null === $this->finder) ( $this->finder = new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor() ); ) zwróć $this->finder; ) )

Możemy sparametryzować metodę fabryczną i delegować inicjalizację innym fabrykom w zależności od przychodzącego parametru. Będzie to już szablon Abstract Factory.
Jeśli musimy zmodularyzować aplikację, możemy wymagać, aby każdy moduł udostępniał własne fabryki. Można dalej rozwijać temat fabryk, ale myślę, że istota tego szablonu jest jasna. Zobaczmy jak wykorzystamy fabrykę w kontrolerze.

Kontroler klasy ( prywatny $factory; funkcja publiczna __construct(Factory $factory) ( $this->factory = $factory; ) funkcja publiczna action() ( /* Niektóre rzeczy */ $results = $this->factory->getGoogleFinder( )->find("szukany ciąg"); /* Zrób coś z wynikami */ ) )

Zaletami tego podejścia jest jego prostota. Nasze obiekty są tworzone jawnie, a Twoje IDE z łatwością poprowadzi Cię do miejsca, w którym to się dzieje. Rozwiązaliśmy także problem rejestru, dzięki czemu obiekty w pamięci będą tworzone tylko wtedy, gdy „poprosimy” o to fabrykę. Ale nie zdecydowaliśmy jeszcze, w jaki sposób dostarczyć kontrolerom niezbędne fabryki. Istnieje kilka opcji tutaj. Możesz użyć metod statycznych. Możemy pozwolić kontrolerom samodzielnie stworzyć niezbędne fabryki i unieważnić wszystkie nasze próby pozbycia się kopiowania i wklejania. Możesz stworzyć fabrykę fabryk i przekazać ją tylko do kontrolera. Ale pobieranie obiektów do kontrolera stanie się nieco bardziej skomplikowane i będziesz musiał zarządzać zależnościami między fabrykami. Poza tym nie do końca jest jasne co zrobić jeśli chcemy wykorzystać w naszej aplikacji moduły, jak zarejestrować fabryki modułów, jak zarządzać połączeniami pomiędzy fabrykami z różnych modułów. Generalnie straciliśmy główną zaletę fabryki - jednoznaczne tworzenie obiektów. Nadal nie rozwiązaliśmy problemu „ukrytego” interfejsu kontrolera.

Lokalizator usług

Szablon Service Locator pozwala rozwiązać problem braku fragmentacji fabryk i automatycznie i centralnie zarządzać tworzeniem obiektów. Jeśli się nad tym zastanowimy, możemy wprowadzić dodatkową warstwę abstrakcji, która będzie odpowiedzialna za tworzenie obiektów w naszej aplikacji i zarządzanie relacjami pomiędzy tymi obiektami. Aby ta warstwa mogła tworzyć dla nas obiekty, będziemy musieli przekazać jej wiedzę, jak to zrobić.
Warunki wzorca lokalizatora usług:
  • Usługa to gotowy przedmiot, który można pozyskać z kontenera.
  • Definicja usługi – logika inicjalizacji usługi.
  • Kontener (Service Container) to centralny obiekt przechowujący wszystkie opisy i mogący na ich podstawie tworzyć usługi.
Każdy moduł może zarejestrować swoje opisy usług. Aby uzyskać usługę z kontenera, będziemy musieli poprosić o nią za pomocą klucza. Możliwości implementacji Service Locatora jest wiele, w najprostszej wersji możemy wykorzystać ArrayObject jako kontener i zamknięcie jako opis usług.

Klasa ServiceContainer rozszerza ArrayObject ( funkcja publiczna get($key) ( if (is_callable($this[$key])) ( return call_user_func($this[$key]); ) rzut new \RuntimeException("Nie można znaleźć definicji usługi w klucz [ $klucz ]"); ) )

Następnie rejestracja definicji będzie wyglądać następująco:

$kontener = nowy kontener usług(); $kontener["grabber"] = funkcja () (zwróć nowy Grabber(); ); $container["html_filter"] = funkcja () (zwróć nowy HtmlExtractor(); ); $container["google_finder"] = funkcja() use ($container) (zwróć nowy GoogleFinder($container->get("grabber"), $container->get("html_filter")); );

A użycie w kontrolerze wygląda następująco:

Kontroler klasy ( prywatny $kontener; funkcja publiczna __construct(ServiceContainer $kontener) ( $this->container = $container; ) funkcja publiczna action() ( /* Niektóre rzeczy */ $results = $this->container->get( "google_finder")->find("search string"); /* Zrób coś z wynikami */ ) )

Kontener usług może być bardzo prosty lub bardzo złożony. Przykładowo Symfony Service Container udostępnia mnóstwo funkcjonalności: parametry, zakresy usług, wyszukiwanie usług po tagach, aliasach, usługi prywatne, możliwość wprowadzania zmian w kontenerze po dodaniu wszystkich usług (przejścia kompilatora) i wiele więcej. DIExtraBundle dodatkowo rozszerza możliwości standardowej implementacji.
Wróćmy jednak do naszego przykładu. Jak widać Service Locator nie tylko rozwiązuje te same problemy co poprzednie szablony, ale także ułatwia korzystanie z modułów posiadających własne definicje usług.
Dodatkowo na poziomie frameworku otrzymaliśmy dodatkowy poziom abstrakcji. Mianowicie zmieniając metodę ServiceContainer::get możemy np. zastąpić obiekt proxy. A zakres zastosowania obiektów zastępczych ograniczony jest jedynie wyobraźnią dewelopera. Tutaj możesz zaimplementować paradygmat AOP, LazyLoading itp.
Jednak większość programistów nadal uważa lokalizator usług za antywzorzec. Bo teoretycznie możemy mieć tyle tzw Klasy kontenerowe (tj. klasy zawierające odwołanie do kontenera). Przykładowo nasz Kontroler, w ramach którego możemy uzyskać dowolną usługę.
Zobaczmy, dlaczego to jest złe.
Najpierw ponowne testowanie. Zamiast tworzyć makiety tylko dla klas używanych w testach, będziesz musiał wyśmiewać cały kontener lub użyć prawdziwego kontenera. Pierwsza opcja Ci nie odpowiada, bo... musisz napisać dużo niepotrzebnego kodu w testach, po drugie, ponieważ jest to sprzeczne z zasadami testów jednostkowych i może prowadzić do dodatkowych kosztów utrzymania testów.
Po drugie, trudno będzie nam dokonać refaktoryzacji. Zmieniając dowolną usługę (lub ServiceDefinition) w kontenerze, będziemy zmuszeni sprawdzić także wszystkie usługi zależne. Tego problemu nie można rozwiązać za pomocą IDE. Znalezienie takich miejsc w całej aplikacji nie będzie takie proste. Oprócz usług zależnych konieczne będzie także sprawdzenie wszystkich miejsc, w których uzyskana jest usługa refaktoryzowana z kontenera.
Cóż, trzeci powód jest taki, że niekontrolowane wyciąganie usług z kontenera prędzej czy później doprowadzi do bałaganu w kodzie i niepotrzebnego zamieszania. Trudno to wyjaśnić, po prostu będziesz musiał spędzać coraz więcej czasu, aby zrozumieć, jak działa ta lub inna usługa, innymi słowy, możesz w pełni zrozumieć, co robi lub jak działa klasa, tylko czytając cały jej kod źródłowy.

Wstrzyknięcie zależności

Co jeszcze możesz zrobić, aby ograniczyć użycie kontenera w aplikacji? Możesz przenieść kontrolę nad tworzeniem wszystkich obiektów użytkownika, w tym kontrolerów, do frameworka. Innymi słowy, kod użytkownika nie powinien wywoływać metody get kontenera. W naszym przykładzie możemy dodać definicję kontrolera do kontenera:

$container["google_finder"] = funkcja() użyj ($kontener) (zwróć nowy kontroler(Grabber $grabber); );

I pozbądź się kontenera w kontrolerze:

Kontroler klasy ( prywatny $finder; funkcja publiczna __construct(GoogleFinder $finder) ( $this->finder = $finder; ) funkcja publiczna action() ( /* Niektóre rzeczy */ $results = $this->finder->find( "wyszukiwany ciąg"); /* Zrób coś z wynikami */ ) )

To podejście (kiedy dostęp do kontenera usług nie jest zapewniany klasom klienta) nazywa się wstrzykiwaniem zależności. Ale ten szablon ma również zalety i wady. Dopóki trzymamy się zasady pojedynczej odpowiedzialności, kod wygląda bardzo pięknie. Przede wszystkim pozbyliśmy się kontenera w klasach klienta, dzięki czemu jego kod jest znacznie jaśniejszy i prostszy. Kontroler możemy łatwo przetestować zastępując niezbędne zależności. Każdą klasę możemy tworzyć i testować niezależnie od innych (w tym klas kontrolerów) przy użyciu podejścia TDD lub BDD. Tworząc testy, możemy abstrahować od kontenera, a później dodać definicję, gdy zajdzie potrzeba użycia określonych instancji. Wszystko to sprawi, że nasz kod będzie prostszy i bardziej przejrzysty, a testowanie bardziej przejrzyste.
Ale trzeba wspomnieć o drugiej stronie medalu. Faktem jest, że kontrolery to bardzo specyficzne klasy. Zacznijmy od tego, że kontroler co do zasady zawiera zestaw działań, czyli narusza zasadę pojedynczej odpowiedzialności. W rezultacie klasa kontrolera może mieć o wiele więcej zależności, niż jest to konieczne do wykonania określonej akcji. Korzystanie z leniwej inicjalizacji (instancja obiektu jest tworzona w momencie jego pierwszego użycia, a wcześniej używany jest lekki serwer proxy) w pewnym stopniu rozwiązuje problem wydajności. Jednak z architektonicznego punktu widzenia tworzenie wielu zależności od kontrolera również nie jest całkowicie poprawne. Ponadto testowanie kontrolerów jest zwykle operacją niepotrzebną. Wszystko zależy oczywiście od tego, jak zorganizowane jest testowanie w Twojej aplikacji i od tego, jak sam się z tym czujesz.
Z poprzedniego akapitu zdałeś sobie sprawę, że użycie wstrzykiwania zależności nie eliminuje całkowicie problemów architektonicznych. Dlatego zastanów się, jak będzie dla Ciebie wygodniej, czy przechowywać link do kontenera w kontrolerach, czy nie. Nie ma tu jednego prawidłowego rozwiązania. Myślę, że oba podejścia są dobre, o ile kod kontrolera pozostaje prosty. Ale zdecydowanie nie powinieneś tworzyć usług Conatiner Aware oprócz kontrolerów.

wnioski

No cóż, przyszedł czas na podsumowanie wszystkiego, co zostało powiedziane. A powiedziano już wiele... :)
Aby więc uporządkować pracę tworzenia obiektów, możemy skorzystać z następujących wzorców:
  • Rejestr: Szablon ma oczywiste wady, z których najbardziej podstawową jest konieczność utworzenia obiektów przed umieszczeniem ich we wspólnym pojemniku. Oczywiście, będziemy mieli więcej problemów niż korzyści z jego stosowania. Zdecydowanie nie jest to najlepsze wykorzystanie szablonu.
  • Metoda fabryczna: Główna zaleta wzorca: obiekty są tworzone jawnie. Główna wada: kontrolerzy albo muszą martwić się o samodzielne tworzenie fabryk, co nie rozwiązuje całkowicie problemu zakodowania nazw klas, albo framework musi być odpowiedzialny za zapewnienie kontrolerom wszystkich niezbędnych fabryk, co nie będzie takie oczywiste. Nie ma możliwości centralnego zarządzania procesem tworzenia obiektów.
  • Lokalizator usług: Bardziej zaawansowany sposób kontrolowania tworzenia obiektów. Dodatkowy poziom abstrakcji można wykorzystać do automatyzacji typowych zadań napotykanych podczas tworzenia obiektów. Na przykład:
    klasa ServiceContainer rozszerza ArrayObject ( funkcja publiczna get($key) ( if (is_callable($this[$key])) ( $obj = call_user_func($this[$key]); if ($obj instancja RequestAwareInterface) ( $obj- >setRequest($this->get("request")); ) return $obj; ) rzut new \RuntimeException("Nie można znaleźć definicji usługi pod kluczem [ $key ]"); ) )
    Wadą Service Locatora jest to, że publiczne API klas przestaje mieć charakter informacyjny. Konieczne jest przeczytanie całego kodu klasy, aby zrozumieć, jakie usługi są w niej użyte. Klasa zawierająca odwołanie do kontenera jest trudniejsza do przetestowania.
  • Wstrzyknięcie zależności: Zasadniczo możemy użyć tego samego kontenera usług, co w poprzednim wzorcu. Różnica polega na sposobie wykorzystania tego pojemnika. Jeśli unikniemy uzależniania klas od kontenera, otrzymamy jasne i jednoznaczne API klas.
To nie wszystko, co chciałbym Państwu powiedzieć na temat problemu tworzenia obiektów w aplikacjach PHP. Istnieje również wzorzec Prototype, nie rozważaliśmy zastosowania Reflection API, odłożyliśmy na bok problem leniwego ładowania usług i wiele innych niuansów. Artykuł okazał się dość długi, więc kończę :)
Chciałem pokazać, że wstrzykiwanie zależności i inne wzorce nie są tak skomplikowane, jak się powszechnie uważa.
Jeśli mówimy o wstrzykiwaniu zależności, to istnieją na przykład implementacje tego wzorca w KISS

Dotykanie struktury przyszłej bazy danych. Początek został zrobiony i nie możemy się wycofać, nawet o tym nie myślę.

Do bazy danych wrócimy nieco później, ale na razie zajmiemy się pisaniem kodu dla naszego silnika. Ale najpierw trochę sprzętu. Zaczynać.

Początek czasu

Na chwilę obecną mamy jedynie pewne pomysły i zrozumienie działania systemu, który chcemy wdrożyć, natomiast samego wdrożenia jeszcze nie ma. Nie mamy z czym pracować: nie mamy żadnej funkcjonalności - i jak pamiętacie, podzieliliśmy to na 2 części: wewnętrzną i zewnętrzną. Alfabet wymaga liter, ale funkcjonalność zewnętrzna wymaga funkcjonalności wewnętrznej – od tego zaczniemy.

Ale nie tak szybko. Aby to zadziałało, musisz zejść trochę głębiej. Nasz system reprezentuje hierarchię, a każdy system hierarchiczny ma swój początek: punkt podłączenia w Linuksie, dysk lokalny w Windows, system stanu, firmy, instytucji edukacyjnej itp. Każdy element takiego systemu jest komuś podporządkowany i może mieć kilku podwładnych, a do zwracania się do swoich sąsiadów i ich podwładnych wykorzystuje przełożonych lub sam początek. Dobrym przykładem systemu hierarchicznego jest drzewo genealogiczne: wybierany jest punkt wyjścia – jakiś przodek – i zaczynamy. W naszym systemie potrzebujemy również punktu wyjścia, z którego będziemy rozwijać gałęzie - moduły, wtyczki itp. Potrzebujemy jakiegoś interfejsu, przez który wszystkie nasze moduły będą się „komunikować”. Do dalszej pracy musimy zapoznać się z koncepcją „ wzór projektowy” i kilka ich realizacji.

Wzorce projektowe

Artykułów o tym, co to jest i jakie są odmiany, jest bardzo dużo, temat jest dość oklepany i nie będę Wam opowiadać nic nowego. Na mojej ulubionej Wiki znajdują się informacje na ten temat: powóz ze zjeżdżalnią i trochę więcej.

Wzorce projektowe są często nazywane wzorami projektowymi lub po prostu wzorami (od angielskiego słowa „wzorzec”, przetłumaczonego oznaczającego „wzorzec”). W dalszej części artykułów, mówiąc o wzorach, będę miał na myśli wzorce projektowe.

Z ogromnej listy wszelkiego rodzaju strasznych (i mniej strasznych) nazw wzorców, jak dotąd interesują nas tylko dwa: rejestr i singleton.

Rejestr (lub zarejestruj się) to wzorzec działający na określonej tablicy, do której można dodawać i usuwać określony zestaw obiektów oraz uzyskać dostęp do dowolnego z nich i jego możliwości.

Samotnik (lub singleton) to wzorzec zapewniający, że może istnieć tylko jedna instancja klasy. Nie można go skopiować, uśpić ani obudzić (mowa o magii PHP: __clone(), __sleep(), __wakeup()). Singleton ma globalny punkt dostępu.

Definicje nie są pełne ani uogólnione, ale to wystarczy do zrozumienia. I tak nie potrzebujemy ich osobno. Nas interesują możliwości każdego z tych wzorców, ale w jednej klasie: taki wzór nazywa się rejestr singleton lub rejestr singleton.

Co nam to da?
  • Będziemy mieli gwarancję posiadania jednej instancji rejestru, do której w dowolnym momencie będziemy mogli dodawać obiekty i wykorzystywać je z dowolnego miejsca w kodzie;
  • nie będzie możliwe jego skopiowanie i wykorzystanie innej niechcianej (w tym przypadku) magii języka PHP.

Na tym etapie wystarczy zrozumieć, że pojedynczy rejestr pozwoli nam na wdrożenie modułowej struktury systemu, o którą nam chodziło przy omawianiu celów w , a resztę zrozumiecie w miarę postępu rozwoju.

No cóż, dość słów, twórzmy!

Pierwsze linie

Ponieważ ta klasa będzie powiązana z funkcjonalnością jądra, zaczniemy od utworzenia w katalogu głównym naszego projektu folderu o nazwie core, w którym umieścimy wszystkie klasy modułów jądra. Zaczynamy od rejestru, więc nazwijmy plik rejestr.php

Nie interesuje nas możliwość, że ciekawski użytkownik wpisze w wierszu przeglądarki bezpośredni adres do naszego pliku, dlatego musimy się przed tym zabezpieczyć. Aby osiągnąć ten cel wystarczy zdefiniować pewną stałą w głównym pliku wykonywalnym, co sprawdzimy. Pomysł nie jest nowy, o ile pamiętam, był stosowany w Joomla. Jest to metoda prosta i działająca, dlatego obejdziemy się tutaj bez rowerów.

Ponieważ chronimy coś, co jest połączone, nazwiemy stałą _PLUGSECURE_:

If (!definiowane("_PLUGSECURE_")) ( die("Bezpośrednie wywołanie modułu jest zabronione!"); )

Teraz, jeśli spróbujesz uzyskać bezpośredni dostęp do tego pliku, nie pojawi się nic przydatnego, co oznacza, że ​​cel został osiągnięty.

Następnie proponuję ustalić pewien standard dla wszystkich naszych modułów. Chcę zapewnić każdemu modułowi funkcję, która zwróci pewne informacje na jego temat, takie jak nazwa modułu, a ta funkcja musi być wymagana w klasie. Aby osiągnąć ten cel, piszemy, co następuje:

Interfejs StorableObject ( publiczna funkcja statyczna getClassName(); )

Lubię to. Teraz, jeśli połączymy dowolną klasę bez funkcji getClassName() zobaczymy komunikat o błędzie. Na razie nie będę się na tym skupiać, przyda się nam to później, przynajmniej do testowania i debugowania.

Czas na klasę samego naszego rejestru singli. Zaczniemy od zadeklarowania klasy i niektórych jej zmiennych:

Rejestr klasy implementuje StorableObject ( //nazwa modułu czytelna private static $className = "Registry"; //instancja rejestru private static $instance; //tablica obiektów private static $objects = array();

Jak na razie wszystko jest logiczne i zrozumiałe. Teraz, jak pamiętacie, mamy rejestr z właściwościami singletonu, więc od razu napiszmy funkcję, która pozwoli nam pracować z rejestrem w następujący sposób:

Publiczna funkcja statyczna singleton() ( if(!isset(self::$instance)) ( $obj = __CLASS__; self::$instance = new $obj; ) return self::$instance; )

Dosłownie: funkcja sprawdza, czy instancja naszego rejestru istnieje: jeśli nie, tworzy ją i zwraca, jeśli już istnieje, po prostu ją zwraca. W tym przypadku nie potrzebujemy magii, więc dla bezpieczeństwa zadeklarujemy, że jest to prywatne:

Funkcja prywatna __construct()() funkcja prywatna __clone()() funkcja prywatna __wakeup()() funkcja prywatna __sleep() ()

Teraz potrzebujemy funkcji dodania obiektu do naszego rejestru - ta funkcja nazywa się setter i zdecydowałem się ją zaimplementować na dwa sposoby, aby pokazać, jak możemy użyć magii i zapewnić alternatywny sposób dodania obiektu. Pierwsza metoda jest funkcją standardową, druga wykonuje pierwszą za pomocą magii __set().

//$object - ścieżka do podłączonego obiektu //$key - klucz dostępu do obiektu w rejestrze funkcja publiczna addObject($key, $object) ( require_once($object); //utwórz obiekt w tablicy obiektów self::$objects[ $key] = new $key(self::$instance); ) //alternatywna metoda poprzez magiczną funkcję publiczną __set($key, $object) ( $this->addObject($key, $ obiekt); )

Teraz, aby dodać obiekt do naszego rejestru, możemy skorzystać z dwóch typów wpisów (załóżmy, że stworzyliśmy już instancję rejestru $registry i chcemy dodać plik config.php):

$registry->addObject("config", "/core/config.php"); //zwykła metoda $registry->config = "/core/config.php"; //poprzez magiczną funkcję PHP __set()

Obydwa wpisy będą spełniać tę samą funkcję - połączą plik, utworzą instancję klasy i umieścią ją w rejestrze za pomocą klucza. Jest tu jedna ważna kwestia, nie możemy o niej zapominać w przyszłości: klucz obiektu w rejestrze musi odpowiadać nazwie klasy w podłączonym obiekcie. Jeśli spojrzysz na kod ponownie, zrozumiesz dlaczego.

To, którego nagrania użyjesz, zależy od Ciebie. Ja wolę nagrywać magiczną metodą – jest „ładniej” i krócej.

Więc poradziliśmy sobie z dodawaniem obiektu, teraz potrzebujemy funkcji umożliwiającej dostęp do połączonego obiektu za pomocą klucza - modułu pobierającego. Zaimplementowałem go również z dwiema funkcjami, podobnymi do settera:

//pobierz obiekt z rejestru //$key - klucz w tablicy funkcja publiczna getObject($key) ( //sprawdź, czy zmienna jest obiektem if (is_object(self::$objects[$key])) ( //jeśli tak, to zwracamy ten obiekt return self::$objects[$key]; ) ) //podobna metoda poprzez magiczną funkcję publiczną __get($key) ( if (is_object(self::$objects[$ klucz])) (zwróć self: :$objects[$key]; ) )

Podobnie jak w przypadku setera, aby uzyskać dostęp do obiektu będziemy mieli 2 równoważne wpisy:

$rejestr->getObject("config"); //zwykła metoda $registry->config; //poprzez magiczną funkcję PHP __get()

Uważny czytelnik natychmiast zada pytanie: dlaczego w magicznej funkcji __set() po prostu wywołuję zwykłą (nie magiczną) funkcję dodawania obiektów, ale w geterze __get() kopiuję kod funkcji getObject() zamiast tego samego wywołania? Szczerze mówiąc, nie potrafię wystarczająco dokładnie odpowiedzieć na to pytanie, powiem tylko, że miałem problemy podczas pracy z magią __get() w innych modułach, ale przy przepisaniu kodu „od razu” nie ma takich problemów.

Być może dlatego często spotykałem się w artykułach z zarzutami wobec magicznych metod PHP i radami, aby ich nie używać.

„Wszelka magia ma swoją cenę.” © Rumpelsztyk

Na tym etapie główna funkcjonalność naszego rejestru jest już gotowa: możemy stworzyć pojedynczą instancję rejestru, dodawać obiekty i uzyskiwać do nich dostęp zarówno metodami konwencjonalnymi, jak i magicznymi metodami języka PHP. „A co z usuwaniem?”— na razie nie będziemy potrzebować tej funkcji i nie jestem pewien, czy coś się zmieni w przyszłości. Na koniec zawsze możemy dodać potrzebną funkcjonalność. Ale jeśli teraz spróbujemy utworzyć instancję naszego rejestru,

$rejestr = Rejestr::singleton();

otrzymamy błąd:

Błąd krytyczny: Rejestr klas zawiera 1 metodę abstrakcyjną i dlatego należy ją zadeklarować jako abstrakcyjną lub zaimplementować pozostałe metody (StorableObject::getClassName) w ...

Wszystko dlatego, że zapomnieliśmy napisać wymaganą funkcję. Pamiętacie, że na samym początku mówiłem o funkcji zwracającej nazwę modułu? To jest to, co pozostaje do dodania, aby uzyskać pełną funkcjonalność. To proste:

Publiczna funkcja statyczna getClassName() (zwróć self::$className; )

Teraz nie powinno być żadnych błędów. Proponuję dodać jeszcze jedną funkcję, nie jest to wymagane, ale prędzej czy później może się przydać, będziemy z niej korzystać w przyszłości do sprawdzania i debugowania. Funkcja zwróci nazwy wszystkich obiektów (modułów) dodanych do naszego rejestru:

Funkcja publiczna getObjectsList() ( //tablica, którą zwrócimy $names = array(); //pobierz nazwę każdego obiektu z tablicy obiektów foreach(self::$objects as $obj) ( $names = $ obj->getClassName() ; ) //dodaj nazwę modułu rejestru do tablicy array_push($names, self::getClassName()); //i zwróć return $names; )

To wszystko. To kończy rejestr. Sprawdźmy jego pracę? Podczas sprawdzania będziemy musieli coś podłączyć - niech będzie plik konfiguracyjny. Utwórz nowy plik core/config.php i dodaj minimalną zawartość wymaganą przez nasz rejestr:

//nie zapomnij sprawdzić stałej if (!definiowane("_PLUGSECURE_")) ( die("Bezpośrednie wywołanie modułu jest zabronione!"); ) class Config ( //nazwa modułu, czytelna prywatna statyczna $className = "Config "; publiczna funkcja statyczna getClassName() (zwróć self::$className; ) )

Coś w tym stylu. Przejdźmy teraz do samej weryfikacji. W katalogu głównym naszego projektu utwórz plik indeks.php i wpisz w nim następujący kod:

Zdefiniuj("_PLUGSECURE_", prawda); //zdefiniowano stałą chroniącą przed bezpośrednim dostępem do obiektów require_once "/core/registry.php"; //podłączono rejestr $registry = Registry::singleton(); //utworzono instancję pojedynczego rejestru $registry->config = "/core/config.php"; //podłącz naszą, dotychczas bezużyteczną, konfigurację //wyświetl nazwy podłączonych modułów echo " Połączony"; foreach ($rejestr->

  • " . $nazwy . "
  • "; }

    Lub, jeśli nadal unikasz magii, piątą linię można zastąpić alternatywną metodą:

    Zdefiniuj("_PLUGSECURE_", prawda); //zdefiniowano stałą chroniącą przed bezpośrednim dostępem do obiektów require_once "/core/registry.php"; //podłączono rejestr $registry = Registry::singleton(); //utworzono instancję pojedynczego rejestru $registry->addObject("config", "/core/config.php"); //podłącz naszą, dotychczas bezużyteczną, konfigurację //wyświetl nazwy podłączonych modułów echo " Połączony"; foreach ($registry->getObjectsList() jako $names) ( echo "

  • " . $nazwy . "
  • "; }

    Teraz otwórz przeglądarkę i wpisz http://localhost/index.php lub po prostu http://localhost/ w pasku adresu (istotne, jeśli używasz standardowych ustawień serwera Open Server lub podobnych)

    W rezultacie powinniśmy zobaczyć coś takiego:

    Jak widać błędów nie ma, czyli wszystko działa, czego serdecznie gratuluję :)

    Dziś na tym poprzestaniemy. W kolejnym artykule wrócimy do bazy danych i napiszemy klasę do pracy z MySQL SUDB, podłączymy ją do rejestru i przetestujemy działanie w praktyce. Do zobaczenia!

    Wzorzec ten, podobnie jak Singleton, rzadko wywołuje pozytywną reakcję programistów, gdyż powoduje te same problemy podczas testowania aplikacji. Niemniej jednak besztają, ale aktywnie korzystają. Podobnie jak Singleton, wzorzec Rejestru można znaleźć w wielu aplikacjach i w taki czy inny sposób znacznie upraszcza rozwiązywanie niektórych problemów.

    Rozważmy obie opcje w kolejności.

    To, co nazywa się „czystym rejestrem” lub po prostu Rejestrem, jest implementacją klasy ze statycznym interfejsem. Główną różnicą w stosunku do wzorca Singleton jest to, że blokuje on możliwość utworzenia przynajmniej jednej instancji klasy. W związku z tym nie ma sensu ukrywać magicznych metod __clone() i __wakeup() za modyfikatorem private lub chronionym.

    Klasa rejestru musi mieć dwie metody statyczne - getter i setter. Osoba ustawiająca umieszcza przekazany obiekt w pamięci z powiązaniem z danym kluczem. Getter odpowiednio zwraca obiekt ze sklepu. Sklep to nic innego jak asocjacyjna tablica klucz-wartość.

    Dla pełnej kontroli nad rejestrem wprowadzony został kolejny element interfejsu - metoda pozwalająca na usunięcie obiektu z magazynu.

    Oprócz problemów identycznych ze wzorcem Singletona są jeszcze dwa:

    • wprowadzenie innego rodzaju zależności - od kluczy rejestru;
    • dwa różne klucze rejestru mogą mieć odniesienie do tego samego obiektu

    W pierwszym przypadku nie da się uniknąć dodatkowego uzależnienia. W pewnym stopniu przywiązujemy się do kluczowych nazw.

    Drugi problem rozwiązuje się poprzez wprowadzenie sprawdzenia do metody Registry::set():

    Publiczny zestaw funkcji statycznych ($key, $item) ( if (!array_key_exists($key, self::$_registry)) ( foreach (self::$_registry as $val) ( if ($val === $item) (wrzuć nowy wyjątek("Element już istnieje"); ) ) self::$_registry[$key] = $item; ) )

    « Wyczyść wzór rejestru„powoduje kolejny problem - zwiększenie zależności ze względu na konieczność dostępu do settera i gettera poprzez nazwę klasy. Nie można utworzyć odniesienia do obiektu i pracować z nim, jak miało to miejsce w przypadku wzorca Singleton, gdy dostępne było takie podejście:

    $instancja = Singleton::getInstance(); $instancja->Foo();

    Tutaj mamy możliwość zapisania referencji do instancji Singletona np. we właściwości bieżącej klasy i pracy z nią zgodnie z wymogami ideologii OOP: przekazania jej jako parametru do zagregowanych obiektów lub użycia jej w potomkach.

    Aby rozwiązać ten problem, istnieje Implementacja rejestru Singleton, którego wiele osób nie lubi, ponieważ wydaje się zbędnym kodem. Myślę, że powodem takiej postawy jest niezrozumienie zasad OOP lub celowe ich lekceważenie.

    _rejestr[$klucz] = $obiekt; ) statyczna funkcja publiczna get($key) ( return self::getInstance()->_registry[$key]; ) funkcja prywatna __wakeup() ( ) funkcja prywatna __construct() ( ) funkcja prywatna __clone() ( ) ) ?>

    Aby zaoszczędzić pieniądze, celowo pominąłem bloki komentarzy dla metod i właściwości. Nie sądzę, że są konieczne.

    Jak już mówiłem, zasadnicza różnica polega na tym, że teraz można zapisać odwołanie do woluminu rejestru i nie używać za każdym razem uciążliwych wywołań metod statycznych. Ta opcja wydaje mi się nieco bardziej poprawna. Zgadzanie się lub nie z moją opinią nie ma większego znaczenia, tak samo jak moja opinia. Żadne subtelności implementacyjne nie są w stanie wyeliminować wzorca z szeregu wymienionych wad.

    Postanowiłam napisać krótko o wzorcach często stosowanych w naszym życiu, więcej przykładów, mniej wody, jedziemy.

    Singel

    Główną cechą „singla” jest to, że kiedy powiesz „Potrzebuję centrali telefonicznej”, powiedzą ci „Już tam zbudowano”, a nie „Zbudujmy to od nowa”. „Samotnik” jest zawsze sam.

    Klasa Singleton ( private static $instance = null; prywatna funkcja __construct())( /* ... @return Singleton */ ) // Ochrona przed utworzeniem za pomocą nowej prywatnej funkcji Singleton __clone() ( /* ... @return Singleton * / ) // Ochrona przed utworzeniem poprzez klonowanie prywatnej funkcji __wakeup() ( /* ... @return Singleton */ ) // Ochrona przed utworzeniem poprzez unserializację publicznej funkcji statycznej getInstance() ( if (is_null(self::$instance ) ) ( self::$instance = nowe ja; ) return self::$instance; ) )

    Rejestr (rejestr, dziennik wpisów)

    Jak sama nazwa wskazuje, wzorzec ten przeznaczony jest do przechowywania umieszczonych w nim rekordów i w związku z tym zwracania tych rekordów (po nazwie), jeśli są wymagane. W przykładzie centrali telefonicznej jest to rejestr w odniesieniu do numerów telefonów mieszkańców.

    Rejestr klasy ( private $registry = array(); publiczny zestaw funkcji ($key, $object) ( $this->registry[$key] = $object; ) funkcja publiczna get($key) ( return $this->registry [$klucz]; ) )

    Rejestr Singletona- nie mylić z)

    „Rejestr” często jest „samotnikiem”, ale nie zawsze musi tak być. Przykładowo w dziale księgowości możemy utworzyć kilka dzienników, w jednym pracują pracownicy od „A” do „M”, w drugim od „N” do „Z”. Każde takie czasopismo będzie „rejestrem”, a nie „jednym”, bo są już 2 czasopisma.

    Klasa SingletonRegistry ( private static $instance = null; private $registry = array(); prywatna funkcja __construct() ( /* ... @return Singleton */ ) // Ochrona przed utworzeniem za pomocą nowej prywatnej funkcji Singleton __clone() ( / * ... @return Singleton */ ) // Ochrona przed utworzeniem poprzez klonowanie prywatnej funkcji __wakeup() ( /* ... @return Singleton */ ) // Ochrona przed utworzeniem poprzez unserializację publicznej funkcji statycznej getInstance() ( if ( is_null(self::$instance)) ( self::$instance = nowe self; ) return self::$instance; ) zestaw funkcji publicznych($key, $object) ( $this->registry[$key] = $ obiekt; ) funkcja publiczna get($key) ( return $this->registry[$key]; ) )

    Multiton (pula „singli”), czyli innymi słowyRejestr Singletona ) - nie mylić z Rejestrem Singleton

    Często „rejestr” jest używany specjalnie do przechowywania „singli”. Ale ponieważ wzorzec „rejestru” nie jest „wzorcem generatywnym”, ale chciałbym rozważyć „rejestr” w połączeniu z „singletonem”.Dlatego opracowaliśmy wzór Multitona, które wgW swojej istocie jest to „rejestr” zawierający kilka „singli”, z których każdy ma swoją własną „nazwę”, za pomocą której można uzyskać do niego dostęp.

    Krótki: umożliwia tworzenie obiektów tej klasy, ale tylko wtedy, gdy nadasz obiektowi nazwę. Nie ma przykładu z życia wziętego, ale znalazłem następujący przykład w Internecie:

    Baza danych klas ( prywatna statyczna $instances = array(); prywatna funkcja __construct() ( ) prywatna funkcja __clone() ( ) publiczna funkcja statyczna getInstance($key) ( if(!array_key_exists($key, self::$instances)) ( self::$instances[$key] = nowe self(); ) return self::$instances[$key]; ) ) $master = Database::getInstance("master"); var_dump($master); // obiekt(Baza danych)#1 (0) ( ) $logger = Database::getInstance("logger"); var_dump($rejestrator); // obiekt(Baza danych)#2 (0) ( ) $masterDupe = Database::getInstance("master"); var_dump($masterDupe); // obiekt(Database)#1 (0) ( ) // Błąd krytyczny: wywołanie prywatnej bazy danych::__construct() z nieprawidłowego kontekstu $dbFatalError = new Database(); // PHP Błąd krytyczny: wywołanie prywatnej bazy danych::__clone() $dbCloneError = clone $masterDupe;

    Pula obiektów

    Zasadniczo taki jest wzór „rejestr”, w którym przechowywane są tylko obiekty, bez ciągów znaków, tablic itp. typy danych.

    Fabryka

    Istotę wzoru niemal całkowicie opisuje jego nazwa. Kiedy chcesz zdobyć jakieś przedmioty, takie jak pudełka po sokach, nie musisz wiedzieć, jak są one produkowane w fabryce. Mówisz po prostu: „daj mi karton soku pomarańczowego”, a „fabryka” zwraca Ci żądane opakowanie. Jak? O tym wszystkim decyduje sama fabryka, na przykład „kopiuje” już istniejący standard. Głównym celem „fabryki” jest umożliwienie, w razie potrzeby, zmiany procesu „wyglądu” opakowania soku, a samemu konsumentowi nie trzeba o tym nic mówić, aby mógł o to poprosić jak wcześniej. Z reguły jedna fabryka zajmuje się „produkcją” tylko jednego rodzaju „produktu”. Nie zaleca się tworzenia „fabryki soków” biorąc pod uwagę produkcję opon samochodowych. Jak w życiu, wzór fabryczny często tworzy jedna osoba.

    Klasa abstrakcyjna AnimalAbstract ( chroniony $gatunek; funkcja publiczna getSpecies() ( return $this->gatunek; ) ) klasa Kot rozszerza AnimalAbstract ( chroniony $gatunek = "kot"; ) klasa Pies rozszerza AnimalAbstract ( chroniony $gatunek = "pies"; ) klasa AnimalFactory ( publiczna funkcja statyczna fabryka($zwierzę) ( przełącznik ($zwierzę) ( case "cat": $obj = new Cat(); break; case "dog": $obj = new Dog(); break; domyślnie : rzucaj nowy wyjątek("Fabryka zwierząt nie mogła stworzyć zwierzęcia gatunku "" . $animal . """, 1000); ) return $obj; ) ) $cat = AnimalFactory::factory("cat"); // obiekt(Kot)#1 echo $cat->getSpecies(); // kot $pies = AnimalFactory::factory("pies"); // obiekt(Pies)#1 echo $dog->getSpecies(); // pies $hippo = AnimalFactory::factory("hipopotam"); // Spowoduje to zgłoszenie wyjątku

    Pragnę zwrócić uwagę na fakt, że metoda fabryczna jest również wzorcem, nazywa się to metodą fabryczną.

    Konstruktor (budowniczy)

    Więc już zrozumieliśmy, że „Factory” to automat z napojami, ma już wszystko gotowe, a ty po prostu mówisz, czego potrzebujesz. „Konstruktor” to zakład produkujący te napoje i obejmujący wszystkie złożone operacje oraz potrafiący złożyć złożone przedmioty z prostszych (opakowanie, etykieta, woda, aromaty itp.) w zależności od zamówienia.

    Klasa Bottle ( public $name; public $liters; ) /** * wszyscy konstruktorzy muszą */ interfejs BottleBuilderInterface ( funkcja publiczna setName(); funkcja publiczna setLiters(); funkcja publiczna getResult(); ) klasa CocaColaBuilder implementuje interfejs BottleBuilderInterface ( private $ butelka; funkcja publiczna __construct() ( $this->bottle = new Bottle(); ) funkcja publiczna setName($value) ( ​​\this->bottle->name = $value; ) publiczna funkcja setLiters($value) ( ​​$ this->bottle->liters = $value; ) publiczna funkcja getResult() ( return $this->bottle; ) ) $juice = new CocaColaBuilder(); $juice->setName("Coca-Cola Light"); $sok->setLiters(2); $sok->getResult();

    Prototyp

    Przypominając fabrykę, służy również do tworzenia obiektów, ale z nieco innym podejściem. Wyobraź sobie, że jesteś w barze, pijesz piwo i kończy Ci się, mówisz barmanowi - zrób mi kolejne tego samego rodzaju. Barman z kolei patrzy na piwo, które pijesz i robi kopię zgodnie z Twoją prośbą. PHP ma już implementację tego wzorca, nazywa się to .

    $nowyJuice = klon $soku;

    Leniwa inicjalizacja

    Przykładowo szef widzi listę raportów dla różnych typów działań i myśli, że te raporty już istnieją, a tak naprawdę wyświetlają się tylko nazwy raportów, a same raporty nie zostały jeszcze wygenerowane i dopiero zostaną wygenerowane na zamówienie (na przykład poprzez kliknięcie przycisku Zobacz raport). Szczególnym przypadkiem leniwej inicjalizacji jest utworzenie obiektu w momencie uzyskania do niego dostępu. Ciekawą informację można znaleźć na Wikipedii, ale... zgodnie z teorią poprawnym przykładem w php byłaby na przykład funkcja

    Adapter lub owijka (adapter, owijka)

    Wzór ten w pełni odpowiada jego nazwie. Aby wtyczka „radziecka” działała przez gniazdo euro, wymagany jest adapter. To jest dokładnie to, co robi „adapter” - służy jako obiekt pośredni między dwoma innymi, które nie mogą bezpośrednio ze sobą współpracować. Pomimo definicji, w praktyce nadal widzę różnicę pomiędzy Adapterem a Wrapperem.

    Klasa MojaKlasa ( funkcja publiczna metodaA() () ) klasa MojaKlasaWrapper ( funkcja publiczna __construct())( $this->myClass = nowa MojaKlasa(); ) funkcja publiczna __call($nazwa, $argumenty)( Log::info(" Za chwilę wywołasz metodę $name."); return call_user_func_array(array($this->myClass, $name), $arguments); ) ) $obj = new MyClassWrapper(); $obj->metodaA();

    Zastrzyk zależności

    Wstrzykiwanie zależności pozwala na przeniesienie części odpowiedzialności za jakąś funkcjonalność na inne obiekty. Przykładowo, jeśli potrzebujemy zatrudnić nowy personel, to nie możemy stworzyć własnego działu HR, ale uzależnić się od firmy rekrutacyjnej, która z kolei na nasze pierwsze żądanie „potrzebujemy osoby” albo będzie działać jako pracownik sam dział HR, albo znajdzie inną firmę (za pomocą „lokatora usług”), która będzie świadczyć te usługi.
    „Wstrzykiwanie zależności” pozwala na przesuwanie i zamianę poszczególnych części firmy bez utraty ogólnej funkcjonalności.

    Klasa AppleJuice () // ta metoda jest prymitywną implementacją wzorca wstrzykiwania zależności, a dalej zobaczysz tę funkcję getBottleJuice())( $obj = new Sok jabłkowy Sok jabłkowy)(zwróć $obj; ) ) $bottleJuice = getBottleJuice();

    Teraz wyobraźcie sobie, że nie chcemy już soku jabłkowego, chcemy soku pomarańczowego.

    Klasa Klasa AppleJuice(). Sok pomarańczowy() // ta metoda implementuje funkcję wstrzykiwania zależności getBottleJuice())( $obj = new Sok pomarańczowy; // sprawdź obiekt, na wypadek, gdyby nie podali nam piwa (piwo to nie sok) if($obj instancja Sok pomarańczowy)(zwróć $obj; ) )

    Jak widać musieliśmy zmienić nie tylko rodzaj soku, ale także sprawdzenie rodzaju soku, co nie jest zbyt wygodne. O wiele bardziej poprawne jest użycie zasady inwersji zależności:

    Interfejs Juice () Klasa AppleJuice implementuje Juice () Klasa OrangeJuice implementuje funkcję Juice () getBottleJuice())( $obj = new OrangeJuice; // sprawdzamy obiekt, na wypadek, gdyby podali nam piwo (piwo to nie sok) if($obj wystąpienie Sok)(zwróć $obj; ) )

    Inwersja zależności jest czasami mylona z wstrzykiwaniem zależności, ale nie ma potrzeby ich mylić, ponieważ Odwrócenie zależności jest zasadą, a nie wzorcem.

    Lokalizator usług

    „Lokator usług” to metoda implementacji „Wstrzykiwania zależności”. Zwraca różne typy obiektów w zależności od kodu inicjującego. Niech zadaniem będzie dostarczenie naszego opakowania soku, stworzonego przez konstruktora, fabrykę lub coś innego, gdziekolwiek chce kupujący. Mówimy lokalizatorowi „zapewnij nam dostawę” i prosimy obsługę o dostarczenie soku pod wskazany adres. Dziś jest jedno nabożeństwo, jutro może być drugie. Nie ma dla nas znaczenia, jaka to konkretna usługa, ważne jest, abyśmy wiedzieli, że ta usługa dostarczy to, co jej powiemy i gdzie to powiemy. Z kolei serwisy wdrażają usługę „Deliver<предмет>NA<адрес>».

    Jeśli mówimy o prawdziwym życiu, prawdopodobnie dobrym przykładem lokalizatora usług byłoby rozszerzenie PDO PHP, ponieważ Dziś pracujemy z bazą danych MySQL, a jutro będziemy mogli pracować z PostgreSQL. Jak już zrozumiałeś, dla naszej klasy nie ma znaczenia, do której bazy danych wyśle ​​swoje dane, ważne, aby potrafiła to zrobić.

    $db = nowe PDO(" mysql:dbname=test;host=localhost", $użytkownik, $pass); $db = nowe PDO(" pgsql:dbname=host testowy=localhost", $użytkownik, $pass);

    Różnica między wstrzykiwaniem zależności a lokalizatorem usług

    Jeśli jeszcze nie zauważyłeś, chciałbym to wyjaśnić. Zastrzyk zależności W rezultacie zwraca nie usługę (która może coś gdzieś dostarczyć), ale obiekt, którego dane wykorzystuje.

    Postaram się opowiedzieć o mojej implementacji wzorca Rejestru w PHP. Rejestr jest zamiennikiem OOP dla zmiennych globalnych, przeznaczonym do przechowywania danych i przesyłania ich pomiędzy modułami systemu. W związku z tym jest wyposażony w standardowe właściwości - zapis, odczyt, usuwanie. Oto typowa implementacja.

    Cóż, w ten sposób otrzymujemy głupią zamianę metod $key = $value - Registry::set($key, $value) $key - Registry::get($key) unset($key) - usuń Registry::remove ($key ) Po prostu staje się niejasne - dlaczego ten dodatkowy kod. Nauczmy więc naszą klasę robić to, czego nie potrafią zmienne globalne. Dodajmy do tego pieprzu.

    pobierzWiadomość()); ) Amdy_Registry::unlock("test"); var_dump(Amdy_Registry::get("test")); ?>

    Do typowych zadań wzorca dodałem możliwość zablokowania zmiennej przed zmianami, jest to bardzo wygodne przy dużych projektach, nic przez przypadek nie wstawisz. Na przykład wygodny do pracy z bazami danych
    zdefiniuj('DB_DNS', 'mysql:host=localhost;nazwa bazy danych= ’);
    zdefiniuj('DB_USER', ' ’);
    zdefiniuj('DB_HASŁO', ' ’);
    zdefiniuj('UCHWYT_DB');

    Amdy_Regisrtry::set(DB_HANDLE, nowe PDO(DB_DNS, DB_USER, DB_PASSWORD));
    Amdy_Registry::lock(DB_HANDLE);

    Teraz, w celu wyjaśnienia kodu, do przechowywania danych używamy zmiennej statycznej $data, zmienna $lock przechowuje dane o kluczach, które są zablokowane na zmianę. W sieci sprawdzamy czy zmienna jest zablokowana i zmieniamy ją lub dodajemy do rejestru. Podczas usuwania sprawdzamy również blokadę, getter pozostaje niezmieniony, z wyjątkiem domyślnego parametru opcjonalnego. No cóż, warto zwrócić uwagę na obsługę wyjątków, która z jakiegoś powodu jest rzadko stosowana.Swoją drogą mam już wersję roboczą na temat wyjątków, poczekajcie na artykuł. Poniżej projekt kodu do testów, tutaj artykuł o testowaniu, nie zaszkodzi też go napisać, chociaż nie jestem fanem TDD.

    W następnym artykule będziemy dalej rozszerzać funkcjonalność, dodając inicjalizację danych i implementując „lenistwo”.