Намиране на решение с помощта на регистър, фабричен метод, локатор на услуги и шаблони за инжектиране на зависимости. Проблемът с инициализацията на обекти в ООП приложения в PHP

Ще се опитам да ви разкажа за моята реализация на шаблона на регистъра в PHP. Регистърът е OOP заместител на глобалните променливи, предназначен да съхранява данни и да ги прехвърля между системните модули. Съответно, той е надарен със стандартни свойства - запис, четене, изтриване. Ето една типична реализация.

Е, по този начин получаваме глупава замяна на методите $key = $value - Registry::set($key, $value) $key - Registry::get($key) unset($key) - премахване на Registry::remove ($key ) Просто става неясно - защо този допълнителен код. И така, нека научим нашия клас да прави това, което глобалните променливи не могат да правят. Нека добавим черен пипер към него.

getMessage()); ) Amdy_Registry::unlock("test"); var_dump(Amdy_Registry::get("тест")); ?>

Към типичните задачи на шаблона добавих възможността да блокирам променлива от промени, това е много удобно при големи проекти, няма да вмъкнете нищо случайно. Например, удобен за работа с бази данни
define('DB_DNS', 'mysql:host=localhost;dbname= ’);
define('DB_USER', ' ’);
define('DB_PASSWORD', ' ’);
define('DB_HANDLE');

Amdy_Regisrtry::set(DB_HANDLE, нов PDO(DB_DNS, DB_USER, DB_PASSWORD));
Amdy_Registry::lock(DB_HANDLE);

Сега за обяснение на кода, за съхраняване на данни, използваме статичната променлива $data, променливата $lock съхранява данни за ключове, които са заключени за промяна. В мрежата проверяваме дали променливата е заключена и я променяме или добавяме към регистъра. При изтриване проверяваме и заключването; гетърът остава непроменен, с изключение на незадължителния параметър по подразбиране. Е, струва си да се обърне внимание на обработката на изключения, която по някаква причина се използва рядко.Между другото, вече имам чернова за изключения, изчакайте статията. По-долу е чернова на код за тестване, ето статия за тестване, няма да е зле да я напиша, въпреки че не съм фен на TDD.

В следващата статия ще разширим допълнително функционалността, като добавим инициализация на данни и внедрим „мързел“.

Този модел, подобно на Singleton, рядко предизвиква положителна реакция от разработчиците, тъй като поражда същите проблеми при тестване на приложения. Въпреки това те се карат, но активно използват. Подобно на Singleton, моделът на регистъра се намира в много приложения и по един или друг начин значително опростява решаването на определени проблеми.

Нека разгледаме и двата варианта по ред.

Това, което се нарича "чист регистър" или просто регистър, е имплементация на клас със статичен интерфейс. Основната разлика от модела Singleton е, че той блокира възможността за създаване на поне един екземпляр от клас. С оглед на това, няма смисъл да се крият магическите методи __clone() и __wakeup() зад модификатора private или protected.

Регистър кластрябва да има два статични метода - getter и setter. Установителят поставя предадения обект в хранилище със свързване към дадения ключ. Получателят съответно връща обект от магазина. Магазинът не е нищо повече от асоциативен масив ключ-стойност.

За пълен контрол върху регистъра е въведен още един интерфейсен елемент - метод, който ви позволява да изтриете обект от хранилището.

В допълнение към проблемите, идентични с модела на Singleton, има още два:

  • въвеждане на друг вид зависимост – от ключове в регистъра;
  • два различни ключа в регистъра могат да имат препратка към един и същ обект

В първия случай е невъзможно да се избегне допълнителна зависимост. До известна степен се привързваме към ключови имена.

Вторият проблем се решава чрез въвеждане на проверка в метода Registry::set():

Публична статична функция set($key, $item) ( if (!array_key_exists($key, self::$_registry)) ( foreach (self::$_registry as $val) ( if ($val === $item) ( хвърля ново изключение ("Елементът вече съществува"); ) ) self::$_registry[$key] = $item; ) )

« Шаблон за чист регистър"поражда друг проблем - увеличаване на зависимостта поради необходимостта от достъп до сетера и гетера чрез името на класа. Не можете да създадете препратка към обект и да работите с него, както беше в случая с модела Singleton, когато този подход беше наличен:

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

Тук имаме възможност да запазим препратка към екземпляр на Singleton, например в свойство на текущия клас, и да работим с него, както се изисква от OOP идеологията: да го предадем като параметър на агрегирани обекти или да го използваме в наследници.

За разрешаване на този проблем има Реализация на единичен регистър, което много хора не харесват, защото изглежда като излишен код. Мисля, че причината за това отношение е някакво неразбиране на принципите на ООП или съзнателното им незачитане.

_registry[$key] = $обект; ) статична публична функция get($key) ( return self::getInstance()->_registry[$key]; ) частна функция __wakeup() ( ) частна функция __construct() ( ) частна функция __clone() ( ) ) ?>

За да спестя пари, съзнателно пропуснах блоковете за коментари за методи и свойства. Не мисля, че са необходими.

Както вече казах, основната разлика е, че сега е възможно да запазите препратка към тома на системния регистър и да не използвате тромави извиквания към статични методи всеки път. Този вариант ми се струва малко по-правилен. Съгласието или несъгласието с моето мнение няма голямо значение, както и самото ми мнение. Никакви тънкости на изпълнение не могат да премахнат модела от редица от споменатите недостатъци.

Проблемът с инициализацията на обект в ООП приложения в PHP. Намиране на решение с помощта на регистър, фабричен метод, локатор на услуги и шаблони за инжектиране на зависимости

Просто се случва програмистите да консолидират успешни решения под формата на шаблони за проектиране. Има много литература за моделите. Книгата на The Gang of Four „Design Patterns“ от Erich Gamma, Richard Helm, Ralph Johnson и John Vlissides“ и, може би, „Patterns of Enterprise Application Architecture“ от Martin Fowler със сигурност се считат за класика. Най-доброто нещо, което съм чел с примери в PHP - това. Така се случи, че цялата тази литература е доста сложна за хора, които току-що са започнали да овладяват ООП. Така че имах идеята да представя някои от моделите, които намирам за най-полезни, в много опростена форма. В други думи, тази статия е първият ми опит да интерпретирам дизайнерски модели в стила KISS.
Днес ще говорим за това какви проблеми могат да възникнат при инициализацията на обект в ООП приложение и как можете да използвате някои популярни шаблони за проектиране, за да разрешите тези проблеми.

Пример

Модерното ООП приложение работи с десетки, стотици, а понякога и хиляди обекти. Е, нека да разгледаме по-отблизо как тези обекти се инициализират в нашите приложения. Инициализацията на обект е единственият аспект, който ни интересува в тази статия, така че реших да пропусна всички „допълнителни“ реализации.
Да кажем, че създадохме супер-дупер полезен клас, който може да изпрати GET заявка до конкретен URI и да върне HTML от отговора на сървъра. За да не изглежда нашият клас твърде прост, нека също да провери резултата и да хвърли изключение, ако сървърът отговори „неправилно“.

Class Grabber (публична функция get($url) (/** връща HTML код или хвърля изключение */))

Нека създадем друг клас, чиито обекти ще отговарят за филтрирането на получения HTML. Методът на филтъра приема HTML код и CSS селектор като аргументи и връща масив от елементи, намерени за дадения селектор.

Клас HtmlExtractor (публичен филтър за функция($html, $selector) (/** връща масив от филтрирани елементи */))

Сега си представете, че трябва да получим резултати от търсенето с Google за дадени ключови думи. За целта ще представим друг клас, който ще използва класа Grabber, за да изпрати заявка, и класа HtmlExtractor, за да извлече необходимото съдържание. Той също така ще съдържа логиката за конструиране на URI, селектор за филтриране на получения HTML и обработка на получените резултати.

Клас GoogleFinder ( private $grabber; private $filter; public function __construct() ( $this->grabber = new Grabber(); $this->filter = new HtmlExtractor(); ) public function find($searchString) ( /* * връща масив от намерени резултати */) )

Забелязахте ли, че инициализирането на обектите Grabber и HtmlExtractor е в конструктора на класа GoogleFinder? Нека помислим колко добро е това решение.
Разбира се, твърдото кодиране на създаването на обекти в конструктор не е добра идея. И ето защо. Първо, няма да можем лесно да заменим класа Grabber в тестовата среда, за да избегнем изпращането на истинска заявка. За да бъдем честни, струва си да кажем, че това може да се направи с помощта на Reflection API. Тези. техническата възможност съществува, но това далеч не е най-удобният и очевиден начин.
Второ, същият проблем ще възникне, ако искаме да използваме повторно логиката на GoogleFinder с други реализации на Grabber и HtmlExtractor. Създаването на зависимости е твърдо кодирано в конструктора на класа. И в най-добрия случай ще можем да наследим GoogleFinder и да заменим неговия конструктор. И дори тогава, само ако обхватът на свойствата на grabber и filter е защитен или публичен.
Последна точка, всеки път, когато създаваме нов обект на GoogleFinder, в паметта ще се създава нова двойка обекти на зависимост, въпреки че можем доста лесно да използваме един обект Grabber и един обект HtmlExtractor в няколко обекта на GoogleFinder.
Мисля, че вече разбирате, че инициализацията на зависимостта трябва да бъде преместена извън класа. Можем да изискаме вече подготвените зависимости да бъдат предадени на конструктора на класа GoogleFinder.

Клас GoogleFinder ( частен $grabber; частен $filter; публична функция __construct(Grabber $grabber, HtmlExtractor $filter) ( $this->grabber = $grabber; $this->filter = $filter; ) публична функция find($searchString) ( /** връща масив от намерени резултати */) )

Ако искаме да дадем възможност на други разработчици да добавят и използват свои собствени реализации на Grabber и HtmlExtractor, тогава трябва да обмислим въвеждането на интерфейси за тях. В този случай това е не само полезно, но и необходимо. Вярвам, че ако използваме само една имплементация в проект и не очакваме да създаваме нови в бъдеще, тогава трябва да откажем създаването на интерфейс. По-добре е да действате според ситуацията и да правите прост рефакторинг, когато има реална нужда от това.
Сега имаме всички необходими класове и можем да използваме класа GoogleFinder в контролера.

Class Controller ( public function action() ( /* Някои неща */ $finder = new GoogleFinder(new Grabber(), new HtmlExtractor()); $results = $finder->

Нека обобщим междинните резултати. Написахме много малко код и на пръв поглед не направихме нищо нередно. Но... какво ще стане, ако трябва да използваме обект като GoogleFinder на друго място? Ще трябва да дублираме създаването му. В нашия пример това е само един ред и проблемът не е толкова забележим. На практика инициализирането на обекти може да бъде доста сложно и може да отнеме до 10 реда или дори повече. Възникват и други проблеми, характерни за дублирането на код. Ако по време на процеса на рефакторинг трябва да промените името на използвания клас или логиката за инициализация на обекта, ще трябва ръчно да промените всички места. Мисля, че знаете как става :)
Обикновено хардкодът се обработва просто. Дублиращи се стойности обикновено са включени в конфигурацията. Това ви позволява да променяте стойностите централно на всички места, където се използват.

Шаблон за регистър.

И така, решихме да преместим създаването на обекти в конфигурацията. Нека го направим.

$registry = нов ArrayObject(); $registry["grabber"] = нов Grabber(); $registry["filter"] = нов HtmlExtractor(); $registry["google_finder"] = нов GoogleFinder($registry["grabber"], $registry["filter"]);
Всичко, което трябва да направим, е да предадем нашия ArrayObject на контролера и проблемът е решен.

Class Controller ( private $registry; public function __construct(ArrayObject $registry) ( $this->registry = $registry; ) public function action() ( /* Някои неща */ $results = $this->registry["google_finder" ]->find("низ за търсене"); /* Направете нещо с резултатите */ ) )

Можем да доразвием идеята за Регистър. Наследява ArrayObject, капсулира създаването на обекти в нов клас, забранява добавянето на нови обекти след инициализация и т.н. Но по мое мнение даденият код напълно изяснява какво представлява шаблонът на регистъра. Този модел не е генеративен, но помага донякъде да реши нашите проблеми. Регистърът е просто контейнер, в който можем да съхраняваме обекти и да ги предаваме в рамките на приложението. За да станат достъпни обектите, трябва първо да ги създадем и регистрираме в този контейнер. Нека да разгледаме предимствата и недостатъците на този подход.
На пръв поглед постигнахме целта си. Спряхме твърдото кодиране на имена на класове и създаваме обекти на едно място. Създаваме обекти в един екземпляр, което гарантира повторното им използване. Ако логиката за създаване на обекти се промени, тогава ще трябва да се редактира само едно място в приложението. Като бонус получихме възможност за централно управление на обекти в Регистъра. Лесно можем да получим списък с всички налични обекти и да извършим някои манипулации с тях. Нека сега да разгледаме какво може да не ни хареса в този шаблон.
Първо, трябва да създадем обекта, преди да го регистрираме в регистъра. Съответно има голяма вероятност за създаване на „ненужни обекти“, т.е. тези, които ще бъдат създадени в паметта, но няма да се използват в приложението. Да, можем да добавяме обекти към Регистъра динамично, т.е. създава само тези обекти, които са необходими за обработка на конкретна заявка. По един или друг начин ще трябва да контролираме това ръчно. Съответно с времето ще стане много трудно да се поддържа.
Второ, имаме нова зависимост на контролера. Да, можем да получаваме обекти чрез статичен метод в регистъра, така че да не се налага да предаваме регистъра на конструктора. Но според мен не трябва да правиш това. Статичните методи са дори по-тясна връзка от създаването на зависимости вътре в обект и трудности при тестване (по тази тема).
Трето, интерфейсът на контролера не ни казва нищо за това какви обекти използва. Можем да получим всеки обект, наличен в регистъра в контролера. Ще ни бъде трудно да кажем кои обекти използва контролерът, докато не проверим целия му изходен код.

Фабричен метод

Най-големият ни проблем с Registry е, че обектът трябва да бъде инициализиран, преди да може да бъде достъпен. Вместо да инициализираме обект в конфигурацията, можем да разделим логиката за създаване на обекти в друг клас, който можем да „помолим“ да изгради обекта, от който се нуждаем. Класовете, които отговарят за създаването на обекти, се наричат ​​фабрики. А шаблонът за проектиране се нарича Фабричен метод. Нека да разгледаме примерна фабрика.

Class Factory ( публична функция getGoogleFinder() ( връща нов GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); ) частна функция getGrabber() ( връща нов Grabber(); ) частна функция getHtmlExtractor() ( върне нов HtmlFiletr(); ))

По правило се правят фабрики, които отговарят за създаването на един тип обект. Понякога една фабрика може да създаде група от свързани обекти. Можем да използваме кеширане в свойство, за да избегнем повторното създаване на обекти.

Class Factory ( частен $finder; публична функция getGoogleFinder() ( if (null === $this->finder) ( $this->finder = new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor() ); ) върне $this->finder; ) )

Можем да параметризираме фабричен метод и да делегираме инициализация на други фабрики в зависимост от входящия параметър. Това вече ще бъде шаблон за абстрактна фабрика.
Ако трябва да модулираме приложението, можем да изискваме всеки модул да предоставя свои собствени фабрики. Можем да доразвием темата за фабриките, но мисля, че същността на този шаблон е ясна. Да видим как ще използваме фабриката в контролера.

Class Controller ( private $factory; public function __construct(Factory $factory) ( $this->factory = $factory; ) public function action() ( /* Някои неща */ $results = $this->factory->getGoogleFinder( )->find("низ за търсене"); /* Направете нещо с резултатите */ ) )

Предимствата на този подход включват неговата простота. Нашите обекти се създават изрично и вашата IDE лесно ще ви отведе до мястото, където това се случва. Решихме и проблема с регистъра, така че обектите в паметта да се създават само когато „помолим“ фабриката да го направи. Но все още не сме решили как да доставяме необходимите фабрики на контрольорите. Тук има няколко варианта. Можете да използвате статични методи. Можем да оставим контролерите сами да създадат необходимите фабрики и да анулираме всички наши опити да се отървем от copy-paste. Можете да създадете фабрика от фабрики и да предадете само това на контролера. Но получаването на обекти в контролера ще стане малко по-сложно и ще трябва да управлявате зависимостите между фабриките. Освен това не е съвсем ясно какво да правим, ако искаме да използваме модули в нашето приложение, как да регистрираме модулни фабрики, как да управляваме връзките между фабрики от различни модули. Като цяло загубихме основното предимство на фабриката - изричното създаване на обекти. И все още не сме решили проблема с „имплицитния“ интерфейс на контролера.

Локатор на услуги

Шаблонът Service Locator ви позволява да разрешите липсата на фрагментация на фабрики и да управлявате създаването на обекти автоматично и централизирано. Ако се замислим, можем да въведем допълнителен абстракционен слой, който ще отговаря за създаването на обекти в нашето приложение и управлението на връзките между тези обекти. За да може този слой да създава обекти за нас, ще трябва да му дадем знания как да прави това.
Условия на модела на локатора на услуги:
  • Услугата е готов обект, който може да се получи от контейнер.
  • Дефиниране на услугата – логика за инициализация на услугата.
  • Контейнерът (Service Container) е централен обект, който съхранява всички описания и може да създава услуги въз основа на тях.
Всеки модул може да регистрира своите описания на услуги. За да получим услуга от контейнера, ще трябва да я поискаме с ключ. Има много опции за внедряване на Service Locator; в най-простата версия можем да използваме ArrayObject като контейнер и затваряне като описание на услугите.

Class ServiceContainer разширява ArrayObject ( публична функция get($key) ( if (is_callable($this[$key])) ( return call_user_func($this[$key]); ) хвърля нов \RuntimeException("Не може да се намери дефиниция на услуга под ключът [ $key ]"); ) )

Тогава регистрацията на дефинициите ще изглежда така:

$контейнер = нов ServiceContainer(); $container["grabber"] = функция () (връща нов Grabber();); $container["html_filter"] = функция () (връща нов HtmlExtractor();); $container["google_finder"] = function() use ($container) ( return new GoogleFinder($container->get("grabber"), $container->get("html_filter")); );

А употребата в контролера е следната:

Class Controller ( private $container; public function __construct(ServiceContainer $container) ( $this->container = $container; ) public function action() ( /* Някои неща */ $results = $this->container->get( "google_finder")->find("низ за търсене"); /* Направете нещо с резултати */ ) )

Сервизният контейнер може да бъде много прост или може да бъде много сложен. Например Symfony Service Container предоставя много функции: параметри, обхвати на услуги, търсене на услуги по тагове, псевдоними, частни услуги, възможност за извършване на промени в контейнера след добавяне на всички услуги (пропуски на компилатор) и много други. DIExtraBundle допълнително разширява възможностите на стандартната реализация.
Но да се върнем към нашия пример. Както можете да видите, Service Locator не само решава всички същите проблеми като предишните шаблони, но също така улеснява използването на модули със собствени дефиниции на услуги.
Освен това на ниво рамка получихме допълнително ниво на абстракция. А именно, чрез промяна на метода ServiceContainer::get можем например да заменим обекта с прокси. А обхватът на приложение на прокси обекти е ограничен само от въображението на разработчика. Тук можете да приложите AOP парадигмата, LazyLoading и т.н.
Но повечето разработчици все още смятат Service Locator за анти-модел. Защото на теория можем да имаме толкова много т.нар Класове, съобразени с контейнера (т.е. класове, които съдържат препратка към контейнера). Например нашия контролер, в който можем да получим всяка услуга.
Нека видим защо това е лошо.
Първо, тестване отново. Вместо да създавате макети само за класовете, използвани в тестовете, ще трябва да имиктирате целия контейнер или да използвате истински контейнер. Първият вариант не ви подхожда, защото... трябва да пишете много ненужен код в тестовете, второ, защото това противоречи на принципите на модулното тестване и може да доведе до допълнителни разходи за поддържане на тестове.
Второ, ще ни бъде трудно да преработим. Като променим която и да е услуга (или ServiceDefinition) в контейнера, ще бъдем принудени да проверим и всички зависими услуги. И този проблем не може да бъде решен с помощта на IDE. Намирането на такива места в приложението няма да е толкова лесно. В допълнение към зависимите услуги, вие също ще трябва да проверите всички места, където преработената услуга се получава от контейнера.
Е, третата причина е, че неконтролираното изтегляне на услуги от контейнера рано или късно ще доведе до бъркотия в кода и ненужно объркване. Това е трудно за обяснение, просто ще трябва да отделяте повече и повече време, за да разберете как работи тази или онази услуга, с други думи, можете да разберете напълно какво прави или как работи даден клас само като прочетете целия му изходен код.

Инжектиране на зависимост

Какво друго можете да направите, за да ограничите използването на контейнер в приложение? Можете да прехвърлите контрола върху създаването на всички потребителски обекти, включително контролери, към рамката. С други думи, потребителският код не трябва да извиква метода get на контейнера. В нашия пример можем да добавим дефиниция за контролера към контейнера:

$container["google_finder"] = function() use ($container) ( return new Controller(Grabber $grabber); );

И се отървете от контейнера в контролера:

Class Controller ( private $finder; public function __construct(GoogleFinder $finder) ( $this->finder = $finder; ) public function action() ( /* Някои неща */ $results = $this->finder->find( "низ за търсене"); /* Направете нещо с резултати */ ) )

Този подход (когато достъпът до сервизния контейнер не е предоставен на клиентските класове) се нарича Инжектиране на зависимост. Но този шаблон има както предимства, така и недостатъци. Докато се придържаме към принципа на единична отговорност, кодът изглежда много красив. На първо място, ние се отървахме от контейнера в клиентските класове, правейки техния код много по-ясен и прост. Можем лесно да тестваме контролера, като заменим необходимите зависимости. Можем да създадем и тестваме всеки клас независимо от другите (включително класове на контролери), използвайки подход TDD или BDD. Когато създаваме тестове, можем да се абстрахираме от контейнера и по-късно да добавим дефиниция, когато трябва да използваме конкретни екземпляри. Всичко това ще направи нашия код по-опростен и ясен, а тестването по-прозрачно.
Но е необходимо да се спомене и другата страна на монетата. Факт е, че контролерите са много специфични класове. Нека започнем с факта, че администраторът, като правило, съдържа набор от действия, което означава, че нарушава принципа на единична отговорност. В резултат на това класът на контролера може да има много повече зависимости, отколкото са необходими за изпълнение на конкретно действие. Използването на мързелива инициализация (обектът се инстанцира в момента на първото му използване и преди това се използва олекотен прокси) решава проблема с производителността до известна степен. Но от архитектурна гледна точка създаването на много зависимости от контролера също не е напълно правилно. В допълнение, тестването на контролери обикновено е ненужна операция. Всичко, разбира се, зависи от това как е организирано тестването във вашето приложение и от това как вие самите го чувствате.
От предишния параграф разбрахте, че използването на Dependency Injection не елиминира напълно архитектурните проблеми. Затова помислете как ще ви бъде по-удобно дали да съхранявате връзка към контейнера в контролери или не. Тук няма едно правилно решение. Мисля, че и двата подхода са добри, стига кодът на контролера да остане прост. Но определено не трябва да създавате Conatiner Aware услуги в допълнение към контролерите.

заключения

Е, дойде моментът да обобщим всичко казано. И много се каза... :)
И така, за да структурираме работата по създаване на обекти, можем да използваме следните модели:
  • Регистър: Шаблонът има очевидни недостатъци, най-основният от които е необходимостта от създаване на обекти, преди да ги поставите в общ контейнер. Очевидно ще имаме повече проблеми, отколкото ползи от използването му. Това очевидно не е най-доброто използване на шаблона.
  • Фабричен метод: Основното предимство на модела: обектите се създават изрично. Основният недостатък: контролерите или трябва да се тревожат за създаването на фабрики сами, което не решава напълно проблема с имената на класовете, които са твърдо кодирани, или рамката трябва да отговаря за осигуряването на контролерите с всички необходими фабрики, което няма да е толкова очевидно. Няма възможност за централизирано управление на процеса на създаване на обекти.
  • Локатор на услуги: По-усъвършенстван начин за контрол на създаването на обекти. Допълнително ниво на абстракция може да се използва за автоматизиране на общи задачи, срещани при създаване на обекти. Например:
    клас ServiceContainer разширява ArrayObject ( публична функция get($key) ( if (is_callable($this[$key])) ( $obj = call_user_func($this[$key]); if ($obj instanceof RequestAwareInterface) ( $obj- >setRequest($this->get("request")); ) return $obj; ) хвърля нов \RuntimeException("Не може да се намери дефиниция на услуга под ключа [ $key ]"); ) )
    Недостатъкът на Service Locator е, че публичният API на класовете престава да бъде информативен. Необходимо е да прочетете целия код на класа, за да разберете какви услуги се използват в него. Клас, който съдържа препратка към контейнер, е по-труден за тестване.
  • Инжектиране на зависимост: По същество можем да използваме същия сервизен контейнер като за предишния модел. Разликата е как се използва този контейнер. Ако избягваме да правим класове зависими от контейнера, ще получим ясен и изричен API за клас.
Това не е всичко, което бих искал да ви кажа за проблема със създаването на обекти в PHP приложения. Съществува и моделът на прототипа, не разгледахме използването на API за отражение, оставихме настрана проблема с мързеливото зареждане на услуги и много други нюанси. Статията се оказа доста дълга, така че ще я приключа :)
Исках да покажа, че инжектирането на зависимост и други модели не са толкова сложни, колкото обикновено се смята.
Ако говорим за Dependency Injection, тогава има KISS реализации на този модел, например

Докосване до структурата на бъдещата база данни. Началото е поставено и не можем да отстъпим и дори не мисля за това.

Ще се върнем към базата данни малко по-късно, но засега ще започнем да пишем кода за нашия двигател. Но първо, малко хардуер. Започнете.

Началото на времето

В момента имаме само някакви идеи и разбиране за работата на системата, която искаме да внедрим, но все още няма самото внедряване. Нямаме с какво да работим: нямаме никаква функционалност - и, както си спомняте, го разделихме на 2 части: вътрешна и външна. Азбуката изисква букви, но външната функционалност изисква вътрешна функционалност – оттам ще започнем.

Но не толкова бързо. За да работи, трябва да отидете малко по-дълбоко. Нашата система представлява йерархия, а всяка йерархична система има начало: точка на монтиране в Linux, локален диск в Windows, система на държава, компания, образователна институция и т.н. Всеки елемент от такава система е подчинен на някого и може да има няколко подчинени, а за адресиране на своите съседи и техните подчинени използва началници или самото начало. Добър пример за йерархична система е родословното дърво: избрана е отправна точка - някакъв предшественик - и тръгваме. В нашата система също се нуждаем от начална точка, от която ще развиваме разклонения - модули, плъгини и т.н. Имаме нужда от някакъв интерфейс, чрез който всички наши модули ще „комуникират“. За по-нататъшна работа трябва да се запознаем с концепцията „ дизайн модел" и няколко техни реализации.

Дизайнерски модели

Има много статии за това какво е и какви разновидности има, темата е доста изтъркана и няма да ви кажа нищо ново. В любимата ми Wiki има информация по тази тема: карета с пързалка и още малко.

Шаблоните за проектиране също често се наричат ​​шаблони за проектиране или просто шаблони (от английската дума pattern, преведена като „модел“). По-нататък в статиите, когато говоря за шаблони, ще имам предвид дизайнерски шаблони.

От огромния списък с всякакви страшни (и не толкова страшни) имена на шаблони, засега се интересуваме само от две: регистър и сингълтън.

Регистър (или се регистрирайте) е шаблон, който работи с определен масив, в който можете да добавяте и премахвате определен набор от обекти и да получавате достъп до всеки от тях и неговите възможности.

Самотник (или сингълтън) е модел, който гарантира, че може да съществува само един екземпляр от клас. Не може да бъде копиран, заспиван или събуден (говорейки за магията на PHP: __clone(), __sleep(), __wakeup()). Singleton има глобална точка за достъп.

Дефинициите не са пълни или обобщени, но това е достатъчно за разбиране. Така или иначе не ни трябват отделно. Ние се интересуваме от възможностите на всеки от тези шаблони, но в един клас: такъв модел се нарича единичен регистър или единичен регистър.

Какво ще ни даде това?
  • Ще ни бъде гарантирано, че имаме един екземпляр на регистъра, в който можем да добавяме обекти по всяко време и да ги използваме от всяко място в кода;
  • ще бъде невъзможно да го копирате и да използвате други нежелани (в този случай) магии на езика PHP.

На този етап е достатъчно да разберем, че един единствен регистър ще ни позволи да внедрим модулна структура на системата, което искахме, когато обсъждахме целите в , а останалото ще разберете с напредването на разработката.

Е, стига думи, да творим!

Първи редове

Тъй като този клас ще се отнася до функционалността на ядрото, ще започнем със създаване на папка в корена на нашия проект, наречена ядро, в която ще поставим всички класове модули на ядрото. Започваме с регистъра, така че нека наречем файла registry.php

Не се интересуваме от възможността любопитен потребител да въведе директен адрес на нашия файл в реда на браузъра, така че трябва да се предпазим от това. За да постигнем тази цел, просто трябва да дефинираме определена константа в главния изпълним файл, който ще проверим. Идеята не е нова, доколкото си спомням е използвана в Joomla. Това е прост и работещ метод, така че тук можем да се справим без велосипеди.

Тъй като защитаваме нещо, което е свързано, ще извикаме константата _PLUGSECURE_:

If (!defined("_PLUGSECURE_")) ( die("Директното извикване на модул е ​​забранено!"); )

Сега, ако се опитате да получите директен достъп до този файл, няма да излезе нищо полезно, което означава, че целта е постигната.

След това предлагам да определим определен стандарт за всички наши модули. Искам да осигуря на всеки модул функция, която ще върне някаква информация за него, като например името на модула, и тази функция трябва да се изисква в класа. За да постигнем тази цел, пишем следното:

Интерфейс StorableObject (публична статична функция getClassName();)

Като този. Сега, ако свържем който и да е клас без функция getClassName()ще видим съобщение за грешка. Засега няма да се фокусирам върху това, ще ни бъде полезно по-късно, поне за тестване и отстраняване на грешки.

Време е за самия клас на нашия регистър за необвързани. Ще започнем с деклариране на класа и някои от неговите променливи:

Class Registry имплементира StorableObject ( //името на модула може да се чете private static $className = "Registry"; //instance на регистъра private static $instance; //масив от обекти private static $objects = array();

До тук всичко е логично и разбираемо. Сега, както си спомняте, имаме регистър с единични свойства, така че нека незабавно напишем функция, която ще ни позволи да работим с регистъра по този начин:

Публична статична функция singleton() ( if(!isset(self::$instance)) ( $obj = __CLASS__; self::$instance = new $obj; ) return self::$instance; )

Буквално: функцията проверява дали екземпляр от нашия регистър съществува: ако не, тя го създава и го връща; ако вече съществува, просто го връща. В този случай нямаме нужда от магия, така че за защита ще го обявим за частно:

Частна функция __construct()() частна функция __clone()() частна функция __wakeup()() частна функция __sleep() ()

Сега се нуждаем от функция за добавяне на обект към нашия регистър - тази функция се нарича сетер и реших да я внедря по два начина, за да покажа как можем да използваме магия и да предоставим алтернативен начин за добавяне на обект. Първият метод е стандартна функция, вторият изпълнява първия чрез магията на __set().

//$object - път до свързания обект //$key - ключ за достъп до обекта в публичната функция addObject($key, $object) ( require_once($object); //създайте обект в масив от обекти self::$objects[ $key] = new $key(self::$instance); ) //алтернативен метод чрез магическа публична функция __set($key, $object) ( $this->addObject($key, $ обект);)

Сега, за да добавим обект към нашия регистър, можем да използваме два типа записи (да кажем, че вече сме създали екземпляр на регистър $registry и искаме да добавим файла config.php):

$registry->addObject("config", "/core/config.php"); //обикновен метод $registry->config = "/core/config.php"; //чрез PHP магическата функция __set()

И двата записа ще изпълняват една и съща функция - ще свържат файла, ще създадат екземпляр на класа и ще го поставят в регистъра с ключа. Тук има един важен момент, който не трябва да забравяме в бъдеще: ключът на обекта в регистъра трябва да съответства на името на класа в свързания обект. Ако погледнете отново кода, ще разберете защо.

Кой запис да използвате зависи от вас. Предпочитам да записвам чрез магически метод - той е "по-красив" и по-кратък.

И така, решихме добавянето на обект, сега имаме нужда от функция за достъп до свързан обект чрез ключ - getter. Също така го внедрих с две функции, подобни на настройката:

//вземете обекта от регистъра //$key - ключът в публичната функция на масива getObject($key) ( //проверете дали променливата е обект if (is_object(self::$objects[$key])) ( //ако е така, тогава връщаме този обект return self::$objects[$key]; ) ) //подобен метод чрез магическа публична функция __get($key) ( if (is_object(self::$objects[$) ключ])) (връща себе си: :$objects[$key]; ))

Както при настройката, за да получим достъп до обекта, ще имаме 2 еквивалентни записа:

$registry->getObject("config"); //обикновен метод $registry->config; //чрез PHP магическата функция __get()

Внимателният читател веднага ще зададе въпроса: защо в магическата функция __set() просто извиквам обикновена (не-магическа) функция за добавяне на обект, но в __get() geter копирам кода на функцията getObject() вместо същото извикване?Честно казано, не мога да отговоря достатъчно точно на този въпрос, просто ще кажа, че имах проблеми при работа с магията __get() в други модули, но при пренаписване на кода „челно“ няма такива проблеми.

Може би затова често виждах в статии упреци към PHP магически методи и съвети да избягвам използването им.

"Всяка магия си има цена." © Rumplestiltskin

На този етап основната функционалност на нашия регистър вече е готова: можем да създадем единичен екземпляр на регистъра, да добавяме обекти и да осъществяваме достъп до тях както с помощта на конвенционални методи, така и чрез магическите методи на езика PHP. „Ами изтриването?“— засега няма да имаме нужда от тази функция и не съм сигурен, че нещо ще се промени в бъдеще. В крайна сметка винаги можем да добавим необходимата функционалност. Но ако сега се опитаме да създадем екземпляр на нашия регистър,

$registry = Регистър::singleton();

ще получим грешка:

Фатална грешка: Class Registry съдържа 1 абстрактен метод и следователно трябва да бъде обявен за абстрактен или да внедри останалите методи (StorableObject::getClassName) в ...

Всичко, защото сме забравили да напишем необходимата функция. Помните ли в самото начало, че говорих за функция, която връща името на модула? Това е, което остава да се добави за пълна функционалност. Просто е:

Публична статична функция getClassName() ( return self::$className; )

Сега не трябва да има грешки. Предлагам да добавим още една функция, не е задължителна, но рано или късно може да ни бъде полезна; ще я използваме в бъдеще за проверка и отстраняване на грешки. Функцията ще върне имената на всички обекти (модули), добавени към нашия регистър:

Публична функция getObjectsList() ( //масивът, който ще върнем $names = array(); //получаваме името на всеки обект от масива от обекти foreach(self::$objects като $obj) ( $names = $ obj->getClassName() ; ) //добавяне на името на регистърния модул към масива array_push($names, self::getClassName()); //и връщане return $names; )

Това е всичко. Това завършва регистъра. Да проверим работата му? Когато проверяваме, ще трябва да свържем нещо - нека има конфигурационен файл. Създайте нов файл core/config.php и добавете минималното съдържание, което нашият регистър изисква:

//не забравяйте да проверите константата if (!defined("_PLUGSECURE_")) ( die("Директното извикване на модул е ​​забранено!"); ) class Config ( //име на модул, четливо частно статично $className = "Config "; публична статична функция getClassName() ( return self::$className; ) )

Нещо такова. Сега да преминем към самата проверка. В основата на нашия проект създайте файл index.php и напишете следния код в него:

Define("_PLUGSECURE_", true); //дефинира константа за защита срещу директен достъп до обекти require_once "/core/registry.php"; //свърза регистъра $registry = Registry::singleton(); //създаде единичен екземпляр на регистър $registry->config = "/core/config.php"; //свържете нашата, досега безполезна, конфигурация //покажете имената на свързаните модули echo " Свързан"; foreach ($registry->

  • ". $names."
  • "; }

    Или, ако все пак избягвате магия, тогава 5-ти ред може да бъде заменен с алтернативен метод:

    Define("_PLUGSECURE_", true); //дефинира константа за защита срещу директен достъп до обекти require_once "/core/registry.php"; //свърза регистъра $registry = Registry::singleton(); //създаде единичен екземпляр на регистър $registry->addObject("config", "/core/config.php"); //свържете нашата, досега безполезна, конфигурация //покажете имената на свързаните модули echo " Свързан"; foreach ($registry->getObjectsList() като $names) ( echo "

  • ". $names."
  • "; }

    Сега отворете браузъра и напишете http://localhost/index.php или просто http://localhost/ в адресната лента (уместно, ако използвате стандартен отворен сървър или подобни настройки на уеб сървъра)

    В резултат на това трябва да видим нещо подобно:

    Както виждате, няма грешки, което означава, че всичко работи, за което ви поздравявам :)

    Днес ще спрем на това. В следващата статия ще се върнем към базата данни и ще напишем клас за работа с MySQL SUDB, ще го свържем с регистъра и ще тестваме работата на практика. Ще се видим!

    Реших да напиша накратко за моделите, които често се използват в живота ни, повече примери, по-малко вода, да тръгваме.

    Сингълтън

    Основният смисъл на „единицата“ е, че когато кажеш „Трябва ми телефонна централа“, те ще ти кажат „Там вече е построена“, а не „Да я направим отново“. „Самотникът“ винаги е сам.

    Клас Singleton ( private static $instance = null; private function __construct())( /* ... @return Singleton */ ) // Защита срещу създаване чрез нова частна функция Singleton __clone() ( /* ... @return Singleton * / ) // Защита срещу създаване чрез клониране на частна функция __wakeup() ( /* ... @return Singleton */ ) // Защита срещу създаване чрез десериализиране на публична статична функция getInstance() ( if (is_null(self::$instance) ) ) ( self::$instance = new self; ) return self::$instance; ) )

    Регистър (регистър, дневник на вписванията)

    Както подсказва името, този модел е предназначен да съхранява записи, които са поставени в него и съответно да връща тези записи (по име), ако са необходими. В примера с телефонна централа това е регистър по отношение на телефонните номера на жителите.

    Регистър на класа ( private $registry = array(); public function set($key, $object) ( $this->registry[$key] = $object; ) public function get($key) ( return $this->registry [$key]; ) )

    Регистър на Singleton- не бъркайте с)

    „Регистърът“ често е „самотник“, но не винаги трябва да е така. Например, можем да създадем няколко журнала в счетоводния отдел, в единия има служители от „A” до „M”, в другия от „N” до „Z”. Всеки такъв журнал ще бъде „регистър“, но не и „единичен“, защото вече има 2 журнала.

    Клас SingletonRegistry ( private static $instance = null; private $registry = array(); private function __construct() ( /* ... @return Singleton */) // Защита от създаване чрез нова частна функция на Singleton __clone() ( / * ... @return Singleton */ ) // Защита срещу създаване чрез клониране на частна функция __wakeup() ( /* ... @return Singleton */ ) // Защита срещу създаване чрез десериализация на публична статична функция getInstance() ( if ( is_null(self::$instance)) ( self::$instance = new self; ) return self::$instance; ) public function set($key, $object) ( $this->registry[$key] = $ обект; ) публична функция get($key) (връща $this->registry[$key]; ) )

    Multiton (пул от „единични“) или с други думиРегистър Singleton ) - не бъркайте със Singleton Registry

    Често „регистърът“ се използва специално за съхраняване на „единични“. Но защото моделът „регистър“ не е „генеративен модел“, но бих искал да разгледам „регистъра“ във връзка с „singleton“.Ето защо измислихме модел Многотонен, което споредВ основата си това е „регистър“, съдържащ няколко „единични“, всеки от които има свое собствено „име“, с което може да бъде достъпен.

    Къс: ви позволява да създавате обекти от този клас, но само ако наименувате обекта. Няма пример от реалния живот, но намерих следния пример в интернет:

    Класова база данни ( частни статични $instances = array(); частна функция __construct() ( ) частна функция __clone() ( ) публична статична функция getInstance($key) ( if(!array_key_exists($key, self::$instances)) ( self::$instances[$key] = new self(); ) return self::$instances[$key]; ) ) $master = Database::getInstance("master"); var_dump($master); // object(Database)#1 (0) ( ) $logger = Database::getInstance("logger"); var_dump($logger); // object(Database)#2 (0) ( ) $masterDupe = Database::getInstance("master"); var_dump($masterDupe); // object(Database)#1 (0) ( ) // Фатална грешка: Извикване на частна база данни::__construct() от невалиден контекст $dbFatalError = нова база данни(); // PHP Фатална грешка: Извикване на частна база данни::__clone() $dbCloneError = клониране $masterDupe;

    Пул от обекти

    По същество този модел е „регистър“, който съхранява само обекти, без низове, масиви и т.н. типове данни.

    Фабрика

    Същността на модела е почти напълно описана от името му. Когато трябва да получите някои предмети, като кутии за сок, не е нужно да знаете как се правят във фабрика. Просто казвате „дайте ми кашон портокалов сок“ и „фабриката“ ви връща необходимия пакет. как? Всичко това се решава от самата фабрика, например, тя „копира“ вече съществуващ стандарт. Основната цел на „фабриката“ е да даде възможност, ако е необходимо, да промени процеса на „поява“ на опаковката на сока, като на самия потребител не е необходимо да се казва нищо за това, за да може да го поиска по старому. По правило една фабрика се занимава с „производство“ само на един вид „продукт“. Не се препоръчва създаването на „фабрика за сокове“, като се има предвид производството на автомобилни гуми. Както в живота, фабричният модел често се създава от един човек.

    Abstract class AnimalAbstract ( protected $species; public function getSpecies() ( return $this->species; ) ) class Cat extends AnimalAbstract ( protected $species = "cat"; ) class Dog extends AnimalAbstract ( protected $species = "dog"; ) клас AnimalFactory ( публична статична функция factory($animal) ( switch ($animal) ( case "cat": $obj = new Cat(); break; case "dog": $obj = new Dog(); break; default : throw new Exception("Animal factory не можа да създаде животно от вид "" . $animal . """, 1000); ) return $obj; ) ) $cat = AnimalFactory::factory("cat"); // object(Cat)#1 echo $cat->getSpecies(); // котка $куче = AnimalFactory::factory("куче"); // object(Dog)#1 echo $dog->getSpecies(); // куче $хипопотам = AnimalFactory::factory("хипопотам"); // Това ще предизвика изключение

    Бих искал да насоча вниманието ви към факта, че фабричният метод също е модел, той се нарича Фабричен метод.

    Строител (строител)

    И така, вече разбрахме, че „Фабрика“ е автомат за напитки, вече има всичко готово, а вие просто казвате какво ви трябва. „Builder” е завод, който произвежда тези напитки и съдържа всички сложни операции и може да сглобява сложни обекти от по-прости (опаковки, етикети, вода, аромати и др.) в зависимост от заявката.

    Class Bottle ( public $name; public $liters; ) /** * всички създатели трябва */ интерфейс BottleBuilderInterface ( public function setName(); public function setLiters(); public function getResult(); ) class CocaColaBuilder implements BottleBuilderInterface ( private $ бутилка; публична функция __construct() ( $this->bottle = new Bottle(); ) публична функция setName($value) ( ​​​​$this->bottle->name = $value; ) публична функция setLiters($value) ( ​​$ this->bottle->liters = $value; ) public function getResult() ( return $this->bottle; ) ) $juice = new CocaColaBuilder(); $juice->setName("Coca-Cola Light"); $juice->setLiters(2); $juice->getResult();

    Прототип

    Наподобявайки фабрика, той също служи за създаване на обекти, но с малко по-различен подход. Представете си себе си в бар, пиете бира и ви свършва, казвате на бармана - направи ми още една от същия вид. Барманът от своя страна гледа бирата, която пиете, и прави копие, както сте поискали. PHP вече има имплементация на този модел, той се нарича.

    $newJuice = клонинг $juice;

    Мързелива инициализация

    Например, шефът вижда списък с отчети за различни видове дейности и мисли, че тези отчети вече съществуват, но всъщност се показват само имената на отчетите, а самите отчети все още не са генерирани и тепърва ще се генерират при поръчка (например чрез натискане на бутона Преглед на отчета). Специален случай на мързелива инициализация е създаването на обект в момента на достъп до него.Можете да намерите интересен в Уикипедия, но... според теорията правилният пример в php би бил например функция

    Адаптер или обвивка (адаптер, обвивка)

    Този модел напълно отговаря на името си. За да накарате „съветския“ щепсел да работи през еврогнездо, е необходим адаптер. Точно това прави един "адаптер" - той служи като междинен обект между два други, които не могат да работят директно един с друг. Въпреки определението, на практика все още виждам разликата между адаптер и обвивка.

    Клас MyClass ( public function methodA() () ) class MyClassWrapper ( public function __construct())( $this->myClass = new MyClass(); ) public function __call($name, $arguments)( Log::info(" На път сте да извикате метод $name."); return call_user_func_array(array($this->myClass, $name), $arguments); ) ) $obj = new MyClassWrapper(); $obj->methodA();

    Инжектиране на зависимост

    Инжектирането на зависимости ви позволява да прехвърлите част от отговорността за някои функции към други обекти. Например, ако трябва да наемем нов персонал, тогава можем да не създаваме собствен отдел по човешки ресурси, а да въведем зависимост от компания за подбор на персонал, която от своя страна при първото ни искане „имаме нужда от човек“ ще работи или като Самият отдел по човешки ресурси или ще намери друга компания (използвайки „локатор на услуги“), която ще предостави тези услуги.
    „Инжектирането на зависимост“ ви позволява да премествате и разменяте отделни части на компанията, без да губите цялостната функционалност.

    Клас AppleJuice () // този метод е примитивна реализация на шаблона за инжектиране на зависимост и по-нататък ще видите тази функция getBottleJuice())( $obj = new Ябълков сок Ябълков сок)( return $obj; ) ) $bottleJuice = getBottleJuice();

    Сега си представете, че вече не искаме ябълков сок, искаме портокалов сок.

    Клас AppleJuice() Клас Портокалов сок() // този метод имплементира функция за инжектиране на зависимости getBottleJuice())( $obj = new Портокалов сок; // проверка на обекта, в случай че са ни подхлъзнали бира (бирата не е сок) if($obj instanceof Портокалов сок)( върне $obj; ) )

    Както можете да видите, трябваше да променим не само вида на сока, но и проверката за вида на сока, което не е много удобно. Много по-правилно е да се използва принципът на инверсия на зависимостта:

    Интерфейс Juice () Клас AppleJuice имплементира Juice () Клас OrangeJuice имплементира Juice () функция getBottleJuice())( $obj = нов OrangeJuice; // проверка на обекта, в случай че са ни подхлъзнали бира (бирата не е сок) if($obj instanceof Сок)( върне $obj; ) )

    Инверсията на зависимостта понякога се бърка с инжекцията на зависимостта, но няма нужда да ги бъркате, т.к. Инверсията на зависимостта е принцип, а не модел.

    Локатор на услуги

    "Service Locator" е метод за внедряване на "Dependency Injection". Той връща различни типове обекти в зависимост от кода за инициализация. Нека задачата е да доставим нашия пакет сок, създаден от строител, завод или нещо друго, където купувачът пожелае. Казваме на локатора „дайте ни услуга за доставка“ и молим услугата да достави сока до желания адрес. Днес има една услуга, а утре може да има друга. За нас няма значение каква конкретна услуга е, важно е да знаем, че тази услуга ще достави това, което ѝ кажем и къде ѝ кажем. От своя страна услугите прилагат „Deliver<предмет>На<адрес>».

    Ако говорим за реалния живот, тогава вероятно добър пример за локатор на услуги би бил PDO PHP разширението, защото Днес работим с MySQL база данни, а утре можем да работим с PostgreSQL. Както вече разбрахте, за нашия клас няма значение към коя база данни изпраща данните си, важно е да може да го направи.

    $db = нов PDO(" mysql:dbname=test;host=localhost", $user, $pass); $db = new PDO(" pgsql:dbname=test host=localhost", $user, $pass);

    Разликата между инжектиране на зависимости и локатор на услуги

    Ако все още не сте забелязали, бих искал да обясня. Инжектиране на зависимостВ резултат на това той връща не услуга (която може да достави нещо някъде), а обект, чиито данни използва.



    Свързани публикации