Створення строго типізованих масивів та колекцій із використанням value object
Квест →Як хакнути форму
Однією з особливостей мови, анонсованої ще PHP 5.6, було додавання синтаксису . - Для позначення того, що функція або метод приймає змінну довжину аргументів.
Щось я рідко зустрічаю, щоб це було поєднано із зазначенням типу, зокрема для створення типізованих масивів.
Наприклад, ми можемо створити клас Movie з методом завдання масиву дат виходу, який приймає тільки DateTimeImmutable об'єкти:
Тепер методу setAirDates() ми можемо передати змінну кількість окремих DateTimeImmutable об'єктів:
Якби ми передали щось інше, а не DateTimeImmutable , рядок, наприклад, буде викинуто виключення фатальної помилки:
Якщо у нас натомість вже є масив DateTimeImmutable об'єктів, які ми хотіли б передати в setAirDates() , ми можемо знову використовувати . , але цього разу, щоб розпакувати їх:
Якщо масив містить значення, яке відповідає очікуваному типу, ми отримаємо фатальну помилку, згадану раніше.
Крім того, починаючи з PHP 7 ми можемо використовувати скалярні типи таким же чином. Наприклад, для нашого класу Movie ми можемо додати метод для визначення списку рейтингів з типом float:
Знову ж таки, це гарантує, що властивість $ratings завжди буде містити float і нам не доведеться перебирати весь вміст масиву, щоб перевірити його. Так що тепер ми можемо легко зробити деякі математичні операції на них в getAverageRating() , не турбуючись про неприпустимі типи.
Проблеми з такого типу Типізованими масивами
Один із недоліків використання цієї можливості як створення типізованихмасивів полягає в тому, що ми можемо визначити лише один такий масив в одному методі. Скажімо, ми хочемо мати клас Movie, який у конструкторі чекає на список дат виходу разом зі списком оцінок, замість того, щоб встановлювати їх пізніше за допомогою додаткових методів. За допомогою методу, який використовується вище, це було б неможливо.
Інша проблема полягає в тому, що при використанні PHP 7, типи наших get() -методів, що повертаються, як і раніше повинні бути «array», що часто має занадто загальний характер.
Рішення: Класи Колекцій
Щоб виправити обидві проблеми, ми можемо просто вбудувати наші типізовані масиви в так званих класах «колекцій». Це також покращить розподіл відповідальності, тому що тепер ми можемо винести метод розрахунку середнього рейтингу у відповідний клас-колекцію:
Зверніть увагу, як ми (все ще) використовуємо список типізованих аргументів змінної довжини у нашому конструкторі, який рятує нас від потреби у циклі проходити по кожному рейтингу, щоб перевірити його тип.
Якщо ми хочемо мати можливість використовувати цей клас-колекцію в циклі foreach, ми просто повинні реалізувати інтерфейс IteratorAggregate:
Рухаючись далі, ми можемо також створити колекцію для нашого списку дат виходу:
Зібравши всі шматочки пазла, тепер ми можемо в конструкторі класу Movie вбудувати дві окремо типізовані колекції. Крім того, ми можемо визначити більш конкретні типи значень, що повертаються, ніж «array» у наших get-методах:
Використання Value Objects для кастомної валідації
Якщо ми захочемо додати додаткову валідацію для наших рейтингів, ми можемо піти на крок далі та визначити об'єкт-значення (value object) Rating з деякими кастомними обмеженнями.Наприклад, рейтинг може бути обмежений між 0 та 5:
Повертаючись до нашого класу-колекції Ratings, ми повинні зробити лише деякі незначні зміни, щоб використовувати ці об'єкти замість float-ів:
Таким чином, ми отримуємо додаткову валідацію окремих членів сімейства, все ще без потреби перебирати в циклі кожен переданий об'єкт.
Переваги
Вводити ці окремі класи колекцій та об'єкт-значення може здатися надмірним та трудомістким, але вони мають ряд переваг у порівнянні зі звичайними масивами та скалярними величинами:
Проста перевірка типу в одному місці. Ми ніколи не повинні обходити масив вручну, щоб перевірити типи елементів нашої колекції;
Скрізь, де в нашому додатку ми використовуємо ці колекції та об'єкти-значення, ми знаємо, що їх значення завжди перевірятимуться під час виклику конструктора. Наприклад, будь-який Rating завжди буде між 0 та 5;
Ми можемо легко додати логіку користувача до колекції та/або об'єкт-значення. Наприклад, метод getAverage() , яким ми можемо користуватися у всьому нашому додатку;
Ми отримуємо можливість передачі параметрів кількох типізованих списків в одній функції або методі, чого ми не можемо зробити за допомогою синтаксису . якщо не винесемо спочатку значення в класи-колекції;
Значно знижується ймовірність плутанини аргументів у сигнатурі методу. Наприклад, коли ми хочемо запровадити список рейтингів, і список дат виходу, їх можна легко переплутати випадково при виклику конструктора, використовуючи звичайні масиви;
Що щодо змін?
Тепер ви можете бути здивовані, як ви могли б змінити значення вашої колекції та об'єкти-значення після початкової ініціалізації через конструктор.
Незважаючи на те, що ми могли б додати методи для полегшення редагування, це може швидко стати громіздким, тому що нам доведеться дублювати більшість методів по кожній колекції, щоб зберегти перевагу підказок типу. Наприклад, метод add() у класі Ratings повинні приймати лише об'єкт Rating , а метод add() в AirDates повинен приймати лише об'єкт DateTimeImmutable . Це робить дуже важким створення інтерфейсу та/або повторне використання цих методів.
Натомість, ми могли б просто тримати наші колекції та об'єкти-значення незмінними, і конвертувати їх у примітивні типи, коли нам потрібно внести зміни. Після того, як ми закінчили внесення змін, ми можемо просто перестворити будь-які необхідні колекції чи об'єкти-значення з оновленими значеннями. При перестворенні всі типи будуть знову перевірені, поряд з будь-якою додатковою валідацією, яку ми визначили.
Наприклад, ми могли б додати простий метод toArray() для наших колекцій і вносити зміни ось так:
Таким чином, ми можемо повторно використовувати існуючі функції для масивів, такі як array_filter() .
Якщо нам дійсно необхідно зробити виправлення на самій колекції об'єктів, ми можемо додати потрібні методи до необхідного мінімуму там, де вони потрібні. Але майте на увазі, що для більшості з них також доведеться робити перевірки типу даних аргументів, тому важко повторно використовувати їх у всіх різних класах колекцій.
Перевикористання універсальних методів
Як ви могли помітити, ми ще маємо деяке дублювання коду в наших класах-колекціях при реалізації методів toarray() і getIterator() для кожного з них.На щастя, ці методи є досить універсальними, щоб перенести їх у загальнийбатьківський клас, тому що вони обидва просто повертають внутрішній масив:
Все, що нам потрібно залишити в нашому класі-колекції – перевірку типу в конструкторі та будь-яку додаткову логіку, специфічну для цієї колекції:
За бажання ми можемо зробити нашу колекціюfinal, щоб запобігти будь-яким дочірнім класам від плутанини зі значеннями властивостей, яка може скасувати нашу перевірку типів.
Висновок
Незважаючи на те, що ще далеко від ідеалу, все ж таки в останніх версіях РНР стає легше працювати з перевіркою типів у колекціях та об'єктах-значеннях.
В ідеалі, у майбутніх версіях PHP ми отримаємо деяку реалізацію узагальнень для подальшого полегшення створення повторно використовуваних класів колекцій.
Можливість, яка дозволить значно покращити використання об'єктів-значень - буде можливість привести об'єкт до різних примітивних типів (крім рядка). Це може бути легко реалізовано шляхом додавання поряд з методом __tostring() , додаткових магічних методів таких, як __toInt() , __toFloat() і т.д.
На щастя, в даний час є кілька RFC, які можуть реалізувати обидві ці можливості в пізніших версіях, так що тримаємо схрещеними пальці!