Проблема ініціалізації об'єктів в ООП додатках на PHP. Пошук рішення за допомогою шаблонів Registry, Factory Method, Service Locator та Dependency Injection. Паттерни ооп з прикладами та описом Авторитетний registro php

Проблема ініціалізації об'єктів в ООП додатках на PHP. Пошук рішення за допомогою шаблонів Registry, Factory Method, Service Locator та Dependency Injection

Так повелося, що програмісти закріплюють вдалі рішення у вигляді шаблонів проектування. За шаблонами існує багато літератури. Класикою безумовно вважається книга Банди чотирьох " Design Patterns " by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides" і ще, мабуть, "Patterns of Enterprise Application Architecture" by Martin Fowler. Найкраще з того, що я читав з прикладами на PHP Це вже вийшло, що вся ця література досить складна для людей, які тільки почали освоювати ОВП, тому в мене з'явилася ідея викласти деякі патерни, які я вважаю найбільш корисними, в дуже спрощеному вигляді. спроба інтерпретувати шаблони проектування в стилі KISS.
Сьогодні мова піде про те, які проблеми можуть виникнути з ініціалізацією об'єктів в ОПП додатку та про те, як можна використовувати деякі популярні шаблони проектування для вирішення цих проблем.

приклад

Сучасне ООП програма працює з десятками, сотнями, а іноді й тисячами об'єктів. Що ж, уважно подивимося на те, яким чином відбувається ініціалізація цих об'єктів у наших додатках. Ініціалізація об'єктів – це єдиний аспект, який нас цікавить у цій статті, тому я вирішив опустити всю зайву реалізацію.
Допустимо, ми створили супер-пупер корисний клас, який вміє відправляти GET запит на певний URI та повертати HTML із відповіді сервера. Щоб наш клас не здавався надто простим, нехай він також перевіряє результат і кидає виняток у разі неправильної відповіді сервера.

Class Grabber ( public function get($url) (/** returns HTML code or throws an exception */) )

Створимо ще один клас, об'єкти якого відповідатимуть за фільтрацію отриманого HTML. Метод filter приймає як аргумент HTML код і CSS селектор, а повертає він нехай масив знайдених елементів по заданому селектору.

Class HtmlExtractor ( public function filter($html, $selector) (/** returns array of filtered elements */) )

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

Class GoogleFinder ( private $grabber; private $filter; public function __construct() ( $this->grabber = new Grabber(); $this->filter = new HtmlExtractor(); ) public function find($searchString) ( /* * returns array of founded results */) )

Ви помітили, що ініціалізація об'єктів Grabber та HtmlExtractor знаходиться у конструкторі класу GoogleFinder? Давайте подумаємо, наскільки це вдале рішення.
Звичайно ж, хардкодити створення об'єктів у конструкторі не найкраща ідея. І ось чому. По-перше, ми не зможемо легко підмінити клас Grabber у тестовому середовищі, щоб уникнути надсилання реального запиту. Заради справедливості, варто сказати, що це можна зробити за допомогою Reflection API. Тобто. технічна можливість існує, але це далеко не найзручніший і очевидний спосіб.
По-друге, та сама проблема виникне, якщо ми захочемо повторно використовувати логіку GoogleFinder з іншими реалізаціями Grabber та HtmlExtractor. Створення залежностей жорстко прописано у конструкторі класу. І в найкращому випадку ми зможемо успадкувати GoogleFinder і перевизначити його конструктор. Та й то тільки якщо область видимості властивостей grabber і filter буде protected або public.
І останній момент, щоразу при створенні нового об'єкта GoogleFinder у пам'яті буде створюватись нова пара об'єктів-залежностей, хоча ми цілком можемо використовувати один об'єкт типу Grabber та один об'єкт типу HtmlExtractor у кількох об'єктах типу GoogleFinder.
Я думаю, що ви вже зрозуміли, що ініціалізацію залежностей потрібно винести за межі класу. Ми можемо зажадати, щоб конструктор класу GoogleFinder передавали вже підготовлені залежності.

Class GoogleFinder ( private $grabber; private $filter; public function __construct(Grabber $grabber, HtmlExtractor $filter) ( $this->grabber = $grabber; $this->filter = $filter; ) public function find($searchString) ( /** returns array of founded results */) )

Якщо ми хочемо надати іншим розробникам можливість додавати та використовувати свої реалізації Grabber та HtmlExtractor, варто подумати про введення інтерфейсів для них. У разі це корисно, а й необхідно. Я вважаю, що якщо у проекті ми використовуємо лише одну реалізацію і не припускаємо створення нових у майбутньому, то варто відмовитися від створення інтерфейсу. Краще діяти за ситуацією та зробити простий рефакторинг, коли в ньому з'явиться реальна потреба.
Тепер у нас є всі необхідні класи і ми можемо використовувати клас GoogleFinder в контролері.

Class Controller ( public function action() ( /* Some stuff */ $finder = New GoogleFinder(New Grabber(), New HtmlExtractor()); $results = $finder->

Підіб'ємо проміжний підсумок. Ми написали зовсім небагато коду, і на перший погляд не зробили нічого поганого. Але… а якщо нам знадобиться використовувати об'єкт типу GoogleFinder в іншому місці? Нам доведеться продублювати його створення. У нашому прикладі це лише один рядок і проблема не така помітна. Насправді ж ініціалізація об'єктів може бути досить складною і може займати до 10 рядків, а то й більше. Також виникають інші проблеми типові для дублювання коду. Якщо в процесі рефакторингу знадобиться змінити ім'я класу, що використовується, або логіку ініціалізації об'єктів, то доведеться вручну поміняти всі місця. Я думаю, ви знаєте як це буває:)
Зазвичай з хардкод надходять просто. Значення, що дублюються, як правило, виносяться в конфігурацію. Це дозволяє централізовано змінювати значення у всіх місцях, де вони використовуються.

Шаблон Registry.

Отже, ми вирішили винести створення об'єктів у конфігурацію. Давайте зробимо це.

$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() ( /* Some stuff */ $results = $this->registry["google_finder" ]->find("search string"); /* Do something with results */ ) )

Можна далі розвинути ідею Registry. Успадкувати ArrayObject, інкапсулювати створення об'єктів усередині нового класу, заборонити додавати нові об'єкти після ініціалізації тощо. Але на мій погляд наведений код повною мірою дає зрозуміти, що собою являє шаблон Registry. Цей шаблон не відноситься до тих, хто породжує, але він в деякій мірі дозволяє вирішити наші проблеми. Registry – це лише контейнер, у якому ми можемо зберігати об'єкти і передавати їх усередині додатка. Щоб об'єкти стали доступними, нам необхідно їх заздалегідь створити та зареєструвати у цьому контейнері. Давайте розберемо переваги та недоліки цього підходу.
На перший погляд, ми досягли своєї мети. Ми перестали хардкодити імена класів та створюємо об'єкти в одному місці. Ми створюємо об'єкти в єдиному екземплярі, що гарантує їхнє повторне використання. Якщо зміниться логіка створення об'єктів, відредагувати потрібно лише одне місце в додатку. Як бонус ми отримали можливість централізовано керувати об'єктами в Registry. Ми легко можемо отримати список усіх доступних об'єктів і провести з ними якісь маніпуляції. Давайте подивимося, що нас може не влаштувати в цьому шаблоні.
По-перше, ми повинні створити об'єкт перед тим, як зареєструвати його в Registry. Відповідно, висока можливість створення «непотрібних об'єктів», тобто. тих, які будуть створюватися в пам'яті, але не будуть використовуватися в програмі. Так, ми можемо додавати об'єкти до Registry динамічно, тобто. створювати ті об'єкти, які необхідні обробки конкретного запиту. Так чи інакше, контролювати це нам доведеться вручну. Відповідно, згодом підтримувати це стане дуже важко.
По-друге, у нас з'явилася нова залежність у контролера. Так, ми можемо отримувати об'єкти через статичний метод Registry, щоб не передавати Registry в конструктор. Але, на мій погляд, не варто цього робити. Статичні методи це навіть більш жорсткий зв'язок, ніж створення залежностей всередині об'єкта, і складності в тестуванні (ось на цю тему).
По-третє, інтерфейс контролера нічого не говорить нам про те, які об'єкти у ньому використовуються. Ми можемо отримати в контролері будь-який об'єкт доступний у Registry. Нам важко буде сказати, які саме об'єкти використовує контролер, доки ми не перевіримо весь вихідний код.

Factory Method

У Registry нас найбільше не влаштовує те, що об'єкт необхідно попередньо ініціалізувати, щоб він став доступним. Замість ініціалізації об'єкта у конфігурації ми можемо виділити логіку створення об'єктів в інший клас, у якого можна буде «попросити» побудувати необхідний нам об'єкт. Класи, які відповідають за створення об'єктів, називають фабриками. А шаблон проектування називається Factory Method. Погляньмо на приклад фабрики.

Class Factory ( public function getGoogleFinder() ( return new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); ) private function getGrabber() ( return new Grabber(); ) private function getHtmlExtractor return new HtmlFiletr(); ) )

Як правило роблять фабрики, які відповідають за створення одного типу об'єктів. Іноді завод може створювати групу пов'язаних об'єктів. Ми можемо використовувати кешування як властивість, щоб уникнути повторного створення об'єктів.

Class Factory ( private $finder; public function getGoogleFinder() ( if (null === $this->finder) ( $this->finder = new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor() ); ) return $this->finder; ) )

Ми можемо параметризувати метод фабрики та делегувати ініціалізацію іншим фабрикам залежно від вхідного параметра. Це вже буде шаблон Abstract Factory.
Якщо з'явиться необхідність розбити програму на модулі, ми можемо вимагати, щоб кожен модуль надавав свої заводи. Ми можемо й надалі розвивати тему фабрик, але думаю, що суть цього шаблону зрозуміла. Давайте подивимося, як ми будемо використовувати фабрику в контролері.

Class Controller ( private $factory; public function __construct(Factory $factory) ( $this->factory = $factory; ) public function action() ( /* Some stuff */ $results = $this->factory->getGoogleFinder( )->find("search string"); /* Do something with results */ ) )

До переваг даного підходу віднесемо його простоту. Наші об'єкти створюються явно і Ваша IDE легко приведе Вас до місця, в якому це відбувається. Ми також вирішили проблему Registry і об'єкти в пам'яті будуть створюватися лише тоді, коли ми попросимо фабрику про це. Але ми поки що не вирішили, як постачати контролерам потрібні фабрики. Тут є кілька варіантів. Можна використати статичні методи. Можна надати контролерам самим створювати потрібні фабрики і звести нанівець усі наші спроби позбутися копіпасту. Можна створити фабрику і передавати в контролер тільки її. Але отримання об'єктів у контролері стане трохи складнішим, та й треба буде керувати залежностями між фабриками. Крім того, не зовсім зрозуміло, що робити, якщо ми хочемо використовувати модулі в нашому додатку, як реєструвати фабрики модулів, як керувати зв'язками між фабриками з різних модулів. Загалом ми втратили головну перевагу фабрики – явне створення об'єктів. І поки що все ще не вирішили проблему «неявного» інтерфейсу контролера.

Service Locator

Шаблон Service Locator дозволяє вирішити нестачу розрізненості фабрик та керувати створенням об'єктів автоматично та централізовано. Якщо подумати, ми можемо запровадити додатковий шар абстракції, який буде відповідати за створення об'єктів у нашому додатку та керувати зв'язками між цими об'єктами. Для того, щоб цей шар зміг створювати об'єкти для нас, ми повинні будемо наділити його знаннями, як це робити.
Терміни шаблону Service Locator:
  • Сервіс (Service) – готовий об'єкт, який можна отримати з контейнера.
  • Опис сервісу (Service Definition) – логіка ініціалізації сервісу.
  • Контейнер (Service Container) – центральний об'єкт, який зберігає всі описи та вміє за ними створювати сервіси.
Будь-який модуль може зареєструвати опис сервісів. Щоб отримати якийсь сервіс з конейнера, ми повинні будемо запросити його по ключу. Існує маса варіантів реалізації Service Locator, у найпростішому варіанті ми можемо використовувати ArrayObject як контейнер і замикання, як опис сервісів.

Class ServiceContainer extends ArrayObject ( public function get($key) ( if (is_callable($this[$key]))) ( return call_user_func($this[$key]); ) throw new \RuntimeException("Can not find service definition under the key [ $key ]"); ) )

Тоді реєстрація Definitions виглядатиме так:

$container = новий ServiceContainer(); $container["grabber"] = function () ( return new Grabber(); ); $container["html_filter"] = function () ( return new 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() ( /* Some stuff */ $results = $this->container->get( "google_finder")->find("search string"); /* Do something with results */ ) )

Service Container може бути дуже простим, а може бути складним. Наприклад, Symfony Service Container надає масу можливостей: параметри (parameters), області видимості сервісів (scopes), пошук сервісів за тегами (tags), псевдоніми (aliases), закриті сервіси (private services), можливість внести зміни до контейнера після додавання всіх сервісів (compiller passes) та ще багато чого. DIExtraBundle ще більше розширює можливості стандартної реалізації.
Але повернемося до нашого прикладу. Як бачимо, Service Locator не тільки вирішує всі ті проблеми, що й попередні шаблони, а й дозволяє легко використовувати модулі із власними визначеннями сервісів.
Крім того, на рівні фреймворку ми одержали додатковий рівень абстракції. А саме, змінюючи метод ServiceContainer::get, ми зможемо, наприклад, підмінити об'єкт на проксі. А сфера застосування проксі-об'єктів обмежена лише фантазією розробника. Тут можна і AOP парадигму реалізувати, і LazyLoading і т.д.
Але, більшість розробників, все ж таки вважають Service Locator анти-патерном. Тому що, теоретично, ми можемо мати скільки завгодно т.зв. Container Aware класів (тобто таких класів, які містять посилання на контейнер). Наприклад, наш Controller, у якому ми можемо отримати будь-який сервіс.
Давайте подивимося, чому це погано.
По-перше, знову ж таки тестування. Замість того, щоб створювати моки тільки для класів, що використовуються в тестах доведеться робити мок всьому контейнеру або використовувати реальний контейнер. Перший варіант не задовольняє, т.к. доводиться писати багато непотрібного коду тестах, другий, т.к. він суперечить принципам модульного тестування, і може призвести до додаткових витрат за підтримку тестів.
По-друге, нам буде важко рефакторити. Змінивши будь-який сервіс (або ServiceDefinition) у контейнері, ми будемо змушені перевірити також усі залежні сервіси. І це завдання не вирішується за допомогою IDE. Знайти такі місця по всьому додатку буде не так просто. Крім залежних сервісів, потрібно буде ще перевірити всі місця, де відрефакторений сервіс виходить із контейнера.
Ну і третя причина в тому, що безконтрольне смикання сервісів із контейнера рано чи пізно призведе до каші в коді та зайвій плутанині. Це складно пояснити, просто Вам потрібно буде витрачати все більше і більше часу, щоб зрозуміти як працює той чи інший сервіс, тобто повністю зрозуміти що робить або як працює клас можна буде тільки прочитавши весь його вихідний код.

Dependency Injection

Що ж можна зробити, щоб обмежити використання контейнера в додатку? Можна передати у фреймворк управління створенням всіх об'єктів користувача, включаючи контролери. Іншими словами, код користувача не повинен викликати метод get у контейнера. У нашому прикладі ми зможемо додати до контейнера Definition для контролера:

$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() ( /* Some stuff */ $results = $this->finder->find( "search string"); /* Do something with results */ ) )

Такий підхід (коли доступ до Service Container не надається клієнтським класам) називають Dependency Injection. Але й цей шаблон має як переваги, і недоліки. Поки у нас дотримується принципу єдиної відповідальності, то код виглядає дуже красиво. По-перше, ми позбулися контейнера в клієнтських класах, завдяки чому їх код став набагато зрозумілішим і простішим. Ми легко можемо протестувати контролер, замінивши необхідні залежності. Ми можемо створювати і тестувати кожен клас незалежно від інших (у тому числі класи контролерів) використовуючи TDD або BDD підхід. При створенні тестів ми зможемо абстрагуватися від контейнера, а потім додати Definition, коли нам знадобиться використовувати конкретні екземпляри. Все це зробить наш код простішим і зрозумілішим, а тестування прозорішим.
Але, необхідно згадати і про зворотний бік медалі. Справа в тому, що контролери – це вельми специфічні класи. Почнемо з того, що контролер, як правило, містить набір екшенів, отже, порушує принцип єдиної відповідальності. У результаті клас контролера може з'явитися набагато більше залежностей, ніж необхідно для виконання конкретного екшену. Використання відкладеної ініціалізації (об'єкт інстанціонується в момент першого використання, а до цього використовується легковажний проксі) певною мірою вирішує питання з продуктивністю. Але з погляду архітектури створювати безліч залежностей у контролера теж зовсім правильно. Крім того, тестування контролерів, як правило зайва операція. Все, звичайно, залежить від того, як тестування організовано у Вашому додатку і від того, як ви самі до цього ставитеся.
З попереднього абзацу Ви зрозуміли, що використання Dependency Injection повністю не позбавляє проблем з архітектурою. Тому, подумайте, як Вам буде зручніше, зберігати в контролерах посилання на контейнер чи ні. Тут немає єдино правильного рішення. Я вважаю, що обидва підходи хороші до тих пір, поки код контролера залишається простим. Але, однозначно, не варто створювати Conatiner Aware сервіси, крім контролерів.

Висновки

Ну от і настав час підбити все сказане. А сказано було чимало… :)
Отже, щоб структурувати роботу зі створення об'єктів ми можемо використовувати такі патерни:
  • Registry: Шаблон має явні недоліки, основний з яких, це необхідність створювати об'єкти перед тим як покласти їх в загальний контейнер. Очевидно, що ми отримаємо більше проблем, ніж вигоди від його використання. Це явно не найкраще застосування шаблону.
  • Factory Method: Основна перевага патерну: об'єкти створюються явно Основний недолік: контролери повинні або самі турбуватися про створення фабрик, що не вирішує проблему хардкода імен класів повністю, або фреймворк повинен відповідати за постачання контролерів усіма необхідними фабриками, що вже не так очевидно. Відсутня можливість централізованого керування процесом створення об'єктів.
  • Service Locator: Більш «просунутий» спосіб керувати створенням об'єктів Додатковий рівень абстракції може бути використаний, щоб автоматизувати типові завдання, що зустрічаються при створенні об'єктів. Наприклад:
    class ServiceContainer extends ArrayObject ( public function get($key) ( if (is_callable($this[$key]))) ( $obj = call_user_func($this[$key])); if ($obj instanceof RequestAwareInterface) ( $obj- >setRequest($this->get("request")); ) return $ obj;
    Недолік Service Locator у тому, що громадський API класів перестає бути інформативним. Необхідно прочитати весь код класу, щоб зрозуміти, які послуги у ньому використовуються. Клас, який містить посилання на контейнер, складніше протестувати.
  • Dependency Injection: По суті ми можемо використовувати той же Service Container, що і для попереднього патерну Різниця у тому, як цей контейнер використовується. Якщо ми уникатимемо створення класів залежних від контейнера, ми отримаємо чіткий і явний API класів.
Це не все, що я хотів би розповісти про проблему створення об'єктів у програмах PHP. Є ще патерн Prototype, ми не розглянули використання Reflection API, залишили осторонь проблему лінивого завантаження сервісів та ще багато інших нюансів. Стаття вийшла не маленька, тому закруглююсь:)
Я хотів показати, що Dependency Injection та інші патерни не такі вже й складні, як прийнято вважати.
Якщо говорити про Dependency Injection, то існують і KISS реалізації цього патерну, наприклад

Торкнувшись структури майбутньої бази даних. Початок належить, і відступати не можна, та я й не думаю про це.

До бази даних ми повернемося трохи пізніше, а поки почнемо писати код нашого двигуна. Але для початку трохи «матчастини». Починаємо.

Початок початків

На даний момент ми маємо лише деякі ідеї та розуміння роботи тієї системи, яку ми хочемо реалізувати, а самої реалізації поки що немає. Нам нема з чим працювати: у нас відсутній якийсь функціонал — а, як пам'ятаєте, ми розділили його на 2 частини: внутрішній і зовнішній. Для алфавіту потрібні літери, а для зовнішнього функціоналу потрібен внутрішній — з нього й почнемо.

Але не так швидко. Для його роботи потрібно крапнути ще трохи глибше. Наша система представляє ієрархію, а будь-яка ієрархічна система має початок: точка монтування в Linux, локальний диск у Windows, державна система, компанія, навчальний заклад і т.д. Кожен елемент такої системи комусь підпорядкований і може мати кілька підлеглих, а для звернення до своїх сусідів та їх підлеглих використовує вищих чи сам початок. Хорошим прикладом ієрархічної системи є генеалогічне дерево: вибирається точка відліку - якийсь предок і помчала. У нашій системі нам також потрібна точка відліку, з якої ми вирощуватимемо гілки — модулі, плагіни тощо. Нам потрібен якийсь інтерфейс, через який будуть спілкуватися всі наші модулі. Для подальшої роботи нам потрібно познайомитися з поняттям. шаблон проектування» та парочка їх реалізацій.

Шаблони проектування

Про те, що це таке і які є різновиди існує безліч статей, тема досить заїжджена і нічого нового я не розповім. На улюбленій Вікі інформації з цієї теми вагон із гіркою та ще трішки.

Шаблони проектування також часто називають патернами проектування або просто патернами (від англійського слова pattern, що в перекладі означає «шаблон»). Далі в статтях, говорячи про патерни, я маю на увазі саме шаблони проектування.

З величезного списку всіляких страшних (і не дуже) назв патернів нас цікавлять поки що всього два: реєстр (registry) та одинак ​​(singleton).

Реєстр (або регістр) - Патерн, який оперує деяким масивом, в який можна додати і видалити кілька об'єктів і отримувати доступ до будь-якого з них і його можливостей.

Одинак (або синглтон) - Патерн, який гарантує, що може існувати тільки один екземпляр класу. Його неможливо скопіювати, приспати або розбудити (мова про магію PHP: __clone(), __sleep(), __wakeup()). Сінглтон має глобальну точку доступу.

Визначення не повні, узагальнені, але розуміння цього достатньо. Окремо вони нам однаково не потрібні. Нас цікавлять можливості кожного з цих патернів, але в одному класі: такий патерн називають реєстр-одинак ​​або Singleton Registry.

Що нам це дасть
  • У нас буде гарантований єдиний екземпляр реєстру, в який ми в будь-який момент можемо додавати об'єкти та використовувати їх з будь-якого місця у коді;
  • його неможливо буде скопіювати та скористатися іншою небажаною (у цьому випадку) магією мови PHP.

На даному етапі достатньо розуміти, що реєстр-одинак ​​дозволить реалізувати модульну структуру системи, чого ми і хотіли, обговорюючи цілі в , а решту зрозумієте по ходу розробки.

Ну, вистачить уже слів, давайте творити!

Перші рядки

Так як цей клас буде у нас ставитись до функціоналу ядра, почнемо ми з того, що в корені нашого проекту створимо папку з ім'ям core, в яку ми поміщатимемо всі класи модулів ядра. Починаємо ми з реєстру, тому назвемо файл registry.php

Нам не цікавий варіант, коли допитливий користувач впише в рядок браузера пряму адресу до нашого файлу, тому нам потрібно захиститися від цього. Для досягнення такої мети нам достатньо визначити в головному файлі якусь константу, яку ми і перевірятимемо. Ідея не нова, її використовували, наскільки я пам'ятаю, у джумлі. Це простий і робочий метод, тому тут обійдемося без велосипедів.

Так як ми захищаємо щось, що підключається, константу обзовемо _PLUGSECURE_ :

If (!defined("_PLUGSECURE_")) ( die("Прямий виклик модуля заборонено!"); )

Тепер при спробі звернутися до цього файлу нічого корисного не вийде, а значить мети досягнуто.

Далі пропоную обговорити стандарт для всіх наших модулів. Я хочу кожен модуль забезпечити функцією, яка повертатиме деяку інформацію про нього, наприклад ім'я модуля, і ця функція повинна бути обов'язковою в класі. Для досягнення цієї мети пишемо наступне:

Interface StorableObject ( public static function getClassName(); )

Ось так. Тепер, якщо ми підключатимемо якийсь клас без функції getClassName()ми побачимо повідомлення про помилку. На цьому не буду поки що загострювати уваги, це нам знадобиться пізніше, для тестів та налагодження як мінімум.

Настав час самого класу нашого реєстру-одинака. Почнемо ми з оголошення класу та деяких його змінних:

Class Registry implements StorableObject ( //ім'я модуля, що читається private static $className = "Реєстр"; //примірник реєстру private static $instance; //масив об'єктів private static $objects = array();

Поки що все логічно і зрозуміло. Тепер, як ви пам'ятаєте, у нас реєстр з властивостями одинаки, тому одразу напишемо функцію, яка дозволить нам працювати з реєстром таким чином:

Public static function singleton() ( if(!isset(self::$instance)) ( $obj = __CLASS__; self::$instance = new $obj; ) return self::$instance; )

Буквально: функція перевіряє, чи існує екземпляр нашого реєстру: якщо ні, створює його та повертає, якщо вже існує – просто повертає. У такому разі, деяка магія нам непотрібна, для захисту оголосимо її приватною:

Private function __construct()() private function __clone()() private function __wakeup()() private function __sleep() ()

Тепер нам потрібна функція додавання об'єкта до нашого реєстру - така функція називається сеттер (setter) і я вирішив реалізувати її двома способами, щоб показати як можна використовувати магію і надати альтернативний спосіб додавання об'єкта. Перший метод – стандартна функція, другий – через магію __set() виконує перший.

//$object - шлях до об'єкта, що підключається //$key - ключ доступу до об'єкта в регістрі public function addObject($key, $object) ( require_once($object); //створюємо об'єкт у масиві об'єктів self::$objects[ $key] = new $key(self::$instance); ) //альтернативний метод через магію public function __set($key, $object) ( $this->addObject($key, $object); )

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

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

Обидва записи виконають одну і ту ж функцію - підключать файл, створять екземпляр класу і помістять його в регістр з ключем. Тут є один важливий момент, надалі про нього не можна забувати: ключ об'єкта в регістрі повинен збігатися з ім'ям класу в об'єкті, що підключається. Якщо ще раз подивіться код, зрозумієте чому.

Який запис використовувати – вирішувати вам. Мені більше подобається запис через магічний метод — він «красивіший» і коротший.

Так, з додаванням об'єкта розібралися, тепер потрібна функція доступу до підключеного об'єкта за ключом – гетер (getter). Його я реалізував також двома функціями, аналогічно сеттеру:

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

Як і з сеттером, для отримання доступу до об'єкта у нас буде 2 рівносильні записи:

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

Уважний читач одразу поставить запитання: чому в магічній функції __set() я просто викликаю звичайну (не магічну) функцію додавання об'єкта, а в геттері __get() я копіюю код функції getObject() замість такого ж виклику?Чесно кажучи, я не можу достатньо точно відповісти на це питання, скажу лише, що у мене виникали проблеми при роботі з магією __get() в інших модулях, але при переписуванні коду «в лоб» жодних проблем немає.

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

"All magic comes with a price." © Rumplestiltskin

На даному етапі основний функціонал нашого реєстру вже готовий: ми можемо створити єдиний екземпляр реєстру, додавати об'єкти та звертатися до них як звичайними методами, так і через магічні методи мови PHP. "А як же видалення?"— поки що ця функція нам не знадобиться, та й я не впевнений, що в майбутньому щось зміниться. Зрештою ми завжди можемо додати потрібний функціонал. Але якщо зараз спробувати створити екземпляр нашого реєстру,

$registry = Registry::singleton();

ми отримаємо помилку:

Fatal error: Class Registry contains 1 abstract method and must therefore be declared abstract or implementation the remaining methods (StorableObject::getClassName) in ...

Все, що ми забули написати обов'язкову функцію. Пам'ятаєте, я на самому початку говорив про функцію, що повертає ім'я модуля? Ось її для повної працездатності і залишилося додати. Вона проста:

Public static function getClassName() ( return self::$className; )

Тепер помилок не має виникнути. Пропоную додати ще одну функцію, вона не обов'язкова, але рано чи пізно може стати в нагоді, її ми надалі використовуватимемо для перевірок та налагодження. Функція повертатиме імена всіх доданих до нашого реєстру об'єктів (модулів):

Public function getObjectsList() ( //масив який будемо повертати $names = array(); //отримуємо ім'я кожного об'єкта з масиву об'єктів foreach(self::$objects as $obj) ( $names = $obj->getClassName() ; ) //дописуємо в масив ім'я модуля регістру array_push($names, self::getClassName()); //і повертаємо return $names; )

От і все. На цьому реєстр закінчено. Перевіримо його роботу? При перевірці нам потрібно буде щось підключити – нехай буде файл конфігурації. Створіть новий файл core/config.php і додайте до нього мінімальний вміст, який вимагає наш реєстр:

//не забуваємо перевіряти константу if (!defined("_PLUGSECURE_")) ( die("Прямий виклик модуля заборонено!"); ) class Config ( //ім'я модуля, що читається private static $className = "Конфіг"; public static function 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() as $names) ( echo "

  • " . $names . "
  • "; }

    Тепер відкриваємо браузер, і пишемо в адресний рядок адресу http://localhost/index.php або просто http://localhost/ (актуально, якщо ви використовуєте стандартні налаштування Open Server або аналогічного веб-сервера)

    В результаті ми маємо побачити щось подібне:

    Як бачите - помилок немає, а значить все працює, з чим я вас і вітаю 🙂

    Сьогодні на цьому ми зупинимося. У наступній статті ми повернемося до бази даних та напишемо клас для роботи з СУДБ MySQL, підключимо до реєстру та перевіримо роботу на практиці. До зустрічі!

    Цей патерн, як і Singleton, рідко викликає позитивну реакцію з боку розробників, оскільки породжує ті ж проблеми при тестуванні додатків. Проте лають, але активно використовують. Як і Singleton , шаблон Реєстр зустрічається в багатьох додатках і так чи інакше значно спрощує вирішення деяких завдань.

    Розглянемо обидва варіанти по порядку.

    Те, що називаються «чистим реєстром» або просто Registry є реалізацією класу зі статичним інтерфейсом. Основною відмінністю від патерна Singleton є блокування можливості створення хоча б одного екземпляра класу. З огляду на це приховувати магічні методи __clone() і __wakeup() за модифікатором private або protected немає сенсу.

    Клас Registryповинен мати два статичні методи – геттер та сеттер. Сеттер поміщає об'єкт, що передається, в сховище з прив'язкою до заданого ключа. Геттер, відповідно, повертає об'єкт із сховища. Сховище – нічим іншим, як асоціативний масив ключ – значення.

    Для повного контролю за реєстром вводять ще один елемент інтерфейсу – метод, що дозволяє видалити об'єкт зі сховища.

    Крім проблем, ідентичних патерну Singleton, виробляють ще дві:

    • введення ще одного типу залежності – від ключів реєстру;
    • два різні ключі реєстру можуть мати посилання на один і той самий об'єкт

    У першому випадку уникнути додаткової залежності неможливо. Якоюсь мірою ми справді стаємо прив'язані до імен ключів.

    Друга проблема вирішується введенням перевірки метод Registry::set() :

    Public static function set($key, $item) ( if (!array_key_exists($key, self::$_registry)) ( foreach (self::$_registry as $val) ( if ($val === $item) ( throw new Exception("Item already exists"); ) ) self::$_registry[$key] = $item; ) )

    « Чистий патерн Registry» породжує ще одну проблему – посилення залежності за рахунок необхідності звернення до сетера та геттера через ім'я класу. Не можна створити посилання на об'єкт і працювати з ним, як у випадку з патерном Одиночка, коли був доступний такий підхід:

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

    Тут ми маємо можливість зберегти посилання на екземпляр Singleton, наприклад, у властивості поточного класу, і працювати з нею так, як того вимагає ідеологія ОВП: передавати як параметр агрегованим об'єктам або використовувати нащадки.

    Для вирішення цього питання існує реалізація Singleton Registry, яку багато хто не любить за надлишковий, як їм здається код. Я думаю, причиною такого ставлення є деяке нерозуміння принципів ОВП або усвідомлене зневага до них.

    _registry[$key] = $object; ) static public function get($key) ( return self::getInstance()->_registry[$key]; ) private function __wakeup() ( ) private function __construct() ( ) private function __clone() ( ) ) ?>

    З метою економії, усвідомлено опустив блоки коментарів для методів та властивостей. Гадаю, у них немає потреби.

    Як я вже казав, принципова різниця в тому, що тепер з'явилася можливість зберегти посилання на обсяг реєстру та не використовувати щоразу громіздкі звернення до статичних методів. Даний варіант мені здається дещо правильнішим. Згода чи не згода з моєю думкою не має великого значення, як і сама моя думка. Жодні тонкощі реалізації не позбавляють патерну від ряду згаданих мінусів.

    Вирішив коротко написати про патерни, що часто використовуються в нашому житті, більше прикладів, менше води, поїхали.

    Singleton (одинак)

    Основний сенс «одиначки» в тому, щоб коли ви кажете «Мені потрібна телефонна станція», вам би говорили «Вона вже побудована там-то», а не «Давай її зробимо заново». "Одиночка" завжди один.

    Class Singleton (private static $instance = null; private function __construct()( /* ... @return Singleton */ ) // Захищаємо від створення через new Singleton private function __clone() ( /* ... @return Singleton * / ) // Захищаємо від створення через клонування private function __wakeup() ( /* ... @return Singleton */ ) // Захищаємо від створення через unserialize public static function getInstance() ( if (is_null(self::$instance) ) ( self::$instance = new self; ) return self::$instance; ) )

    Registry (реєстр, журнал записів)

    Як випливає з назви, даний патерн призначений для зберігання записів які в нього поміщають і повернення цих записів (на ім'я) якщо вони будуть потрібні. У прикладі з телефонною станцією вона є реєстром по відношенню до телефонних номерів мешканців.

    Class Registry ( private $registry = array(); public function set($key, $object) ( $this->registry[$key] = $object; ) public function get($key) ( return $this->registry [$key]; ) )

    Singleton Registry (самотній реєстр)- не плутайте з)

    «Реєстр» нерідко є «одинаком», проте це не завжди має бути саме так. Наприклад, ми можемо заводити в бухгалтерії кілька журналів, в одному працівники від «А» до «М», в іншому від «Н» до «Я». Кожен такий журнал буде «реєстром», але не «одинаком», тому що журналів уже два.

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

    Multiton (пул «одиначок») або іншими словамиRegistry Singleton (реєстр одинаків ) - не плутайте з Singleton Registry (самотній реєстр)

    Нерідко «реєстр» служить саме для зберігання «одинаків». Проте, т.к. патерн «реєстр» не є «патерном, що породжує», а хотілося б розглядати «реєстр» у взаємозв'язку з «одинаком».Тому вигадали патерн Multiton, який засвоєї суті є «реєстром» містить кілька «одиначок», кожен із яких має своє «ім'я» яким до нього можна отримати доступ.

    Коротко: дозволяє створювати об'єкти даного класу, але у разі іменування об'єкта. Життєвого прикладу немає, але в інтернеті нарив такий приклад:

    Class Database ( private static $instances = array(); private function __construct() ( ) private function __clone() ( ) public static function 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) ( ) // Error error: Call to private Database::__construct() from invalid context $dbFatalError = new Database(); // PHP Fatal error: Call to private Database::__clone() $dbCloneError = clone $masterDupe;

    Object pool (пул об'єктів)

    По суті цей патерн є «реєстром», який зберігає лише об'єкти, жодних рядків, масивів тощо. типів даних.

    Factory (фабрика)

    Суть патерну практично повністю описується його назвою. Коли вам потрібно отримувати якісь об'єкти, наприклад, пакети соку, вам зовсім не потрібно знати, як їх роблять на фабриці. Ви просто кажете "дай мені пакет апельсинового соку", а "фабрика" повертає вам необхідний пакет. Як? Все це вирішує сама фабрика, наприклад, «копіює» вже існуючий еталон. Основне призначення "фабрики" в тому, щоб можна було при необхідності змінювати процес "появи" пакету соку, а самому споживачеві нічого про це не потрібно було повідомляти, щоб він запитував його як і раніше. Як правило, одна фабрика займається "виробництвом" тільки одного роду "продуктів". Не рекомендується "фабрику соків" створювати з урахуванням виробництва автомобільних покришок. Як і в житті, патерн "фабрика" часто створюється "одинаком".

    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" ) class AnimalFactory ( public static function factory($animal) ( switch ($animal) ( case "cat": $obj = new Cat(); break; case "dog": $obj = new Dog(); break; default) : throw new Exception("Animal factory could no create animal of species "" . $animal . """, 1000); ) return $obj; ) ) $cat = AnimalFactory::factory("cat"); // object(Cat)#1 echo $cat->getSpecies(); // cat $dog = AnimalFactory::factory("dog"); // object(Dog)#1 echo $dog->getSpecies(); // dog $hippo = AnimalFactory::factory("hippopotamus"); // This will throw an Exception

    Хочеться звернути увагу, що метод factory також є патерном, його називають Factory method (фабричний метод).

    Builder (будівельник)

    Отже, ми вже зрозуміли, що «Фабрика» - це автомат із продажу напоїв, у ньому вже є все готове, а Ви тільки кажете, що вам потрібно. «Будівельник» - це завод, який виробляє ці напої та містить у собі всі складні операції та може збирати складні об'єкти з більш простих (упаковка, етикетка, вода, ароматизатори тощо) залежно від запиту.

    Class Bottle ( public $name; public $liters; ) /** * всі будівельники повинні */ interface BottleBuilderInterface ( public function setName(); public function setLiters(); public function getResult(); ) class CocaColaBuilder implements BottleBuilderInterface ( private $ bottle; public function __construct() ( $this->bottle = new Bottle(); ) public function setName($value) ( ​​$this->bottle->name = $value; ) public function 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();

    Prototype (прототип)

    Нагадуючи «фабрику», він також служить для створення об'єктів, однак із трохи іншим підходом. Уявіть себе в барі, Ви пили пиво і воно у Вас закінчується, Ви кажете бармену – зроби мені ще одне таке саме. Бармен, у свою чергу, дивиться на пиво, яке Ви п'єте і робить копію, як Ви попросили. У php вже є реалізація такого патерну, вона називається .

    $newJuice = clone $juice;

    Lazy initialization (відкладена ініціалізація)

    Наприклад, начальник бачить список звітів за різними видами діяльності і думає, що ці звіти вже є, але насправді виводяться лише назви звітів, а самі звіти ще не сформовані, і формуватимуться лише за наказом (наприклад, за натисканням кнопки Переглянути звіт). Окремий випадок лінивої ініціалізації - створення об'єкта в момент звернення до нього.На вікіпедії можна знайти цікавий, але, т.к. згідно з теорією , правильним прикладом у php буде наприклад функція

    Adapter або Wrapper (адаптер, обгортка)

    Цей патерн повністю відповідає своїй назві. Щоб змусити працювати «радянську» вилку через євророзетку потрібен перехідник. Саме це робить «адаптер», - служить проміжним об'єктом між двома іншими, які можуть працювати безпосередньо друг з одним. Не дивлячись на визначення, в практиці я все ж таки бачу різницю між Adapter і Wrapper.

    Class MyClass ( public function methodA() () ) class MyClassWrapper ( public function __construct()( $this->myClass = new MyClass(); ) public function __call($name, $arguments)( Log::info("You are about to call $name method."); return call_user_func_array(array($this->myClass, $name), $arguments); ) ) $obj = new MyClassWrapper(); $obj->methodA();

    Dependency injection (використання залежності)

    Впровадження залежності дозволяє перекласти частину відповідальності за якийсь функціонал інші об'єкти. Наприклад якщо нам потрібно найняти новий персонал, то ми можемо не створювати свій відділ кадрів, а впровадити залежність від компанії з підбору персоналу, яка свою чергу на першу нашу вимогу «нам потрібна людина», буде або сама працювати як відділ кадрів, або ж знайде іншу компанію (за допомогою «локатора служб»), яка надасть ці послуги.
    «Впровадження залежності» дозволяє перекладати та взаємозамінювати окремі частини компанії без втрати загальної функціональності.

    Class AppleJuice () // цей метод є примітивною реалізацією патерна Dependency injection і далі Ви в цьому переконаєтеся function getBottleJuice()( $obj = new AppleJuice AppleJuice)( return $obj; ) ) $bottleJuice = getBottleJuice();

    А тепер уявімо, що нам більше не хочеться яблучного соку, ми хочемо апельсиновий.

    Class AppleJuice () Class OrangeJuice() // Цей метод реалізує Dependency injection function getBottleJuice()( $obj = new OrangeJuice; // перевіримо об'єкт, а то раптом нам підсунули пиво (адже пиво не сік) if($obj instanceof OrangeJuice)( return $obj; ) )

    Як бачите, нам довелося змінити не тільки вид соку, а й перевірку на вид соку, не дуже зручно. Набагато правильніше використовувати принцип Dependency inversion:

    Interface Juice () Class AppleJuice implements Juice () Class OrangeJuice implements Juice () function getBottleJuice()( $obj = new OrangeJuice; // перевіримо об'єкт, а то раптом нам підсунули пиво (адже пиво не сік) if($obj instanceof Juice)( return $obj; ) )

    Dependency inversion іноді плутають з Dependency injection, але плутати їх потрібно, т.к. Dependency inversion це принцип, а не патерн.

    Service Locator (локатор служб)

    "Локатор служб" є методом реалізації "впровадження залежності". Він повертає різні типи об'єктів залежно від коду ініціалізації. Нехай завдання варто доставити наш пакет соку, створений будівельником, фабрикою або ще чим куди захотів покупець. Ми говоримо локатору «дай нам службу доставки», і просимо службу доставити сік на потрібну адресу. Сьогодні одна служба, а завтра може бути іншою. Нам все одно яка це конкретно служба, нам важливо знати, що ця служба доставить те, що ми їй скажемо і туди, куди скажемо. У свою чергу служби реалізують інтерфейс «Доставити<предмет>на<адрес>».

    Якщо говорити про реальне життя, то це хорошим прикладом Service Locator-а може бути php-розширення PDO, т.к. Сьогодні ми працюємо з базою даних MySQL, а завтра можемо працювати з PostgreSQL. Як Ви вже зрозуміли, нашому класу не важливо, до якої бази даних надсилати свої дані, важливо, що він може це робити.

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

    Відмінність Dependency injection від Service Locator

    Якщо ви ще не помітили, то хочеться пояснити. Dependency injectionв результаті повертає не сервіс (яким можна щось кудись доставити), а об'єкт, дані якого використовує.

    Постараюся розповісти про мою реалізацію патерну Registry під php. Registry - це ООП заміна глобальним змінним, призначена для зберігання даних та передачі їх між модулями системи. Відповідно, його наділяють стандартними властивостями – запис, читання, видалення. Ось типова реалізація.

    Ну і таким чином отримуємо тупу заміну методів $key = $value - Registry::set($key, $value) $key - Registry::get($key) unset($key) - remove Registry::remove($key ) Тільки стає незрозуміло - а навіщо цей зайвий код. Отже, навчимо наш клас робити те, що не вміють глобальні змінні. Додамо до нього перчика.

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

    До типових завдань патерну, я додав можливість блокування змінної від змін, це дуже зручно на великих проектах, випадково не всунеш нічого. Наприклад, зручно для роботи з бд
    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.

    У наступній статті ще розширимо функціонал, додавши ініціалізацію даних та реалізуємо «лінивість».