Створення строго типізованих масивів та колекцій із використанням 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, які можуть реалізувати обидві ці можливості в пізніших версіях, так що тримаємо схрещеними пальці!