Всем привет.
Сегодня я хочу рассказать о довольно забавном способе использования РНР и трейтов — сборке класса с нужным вам функционалом, по кусочкам.
Интересно? Тогда прошу под кат.
Прежде, чем мы перейдём непосредственно к делу, давайте совершим небольшую прогулку в РНР.
Как это было
Итак, в своё время, при разработке РНР 5.0 были введены классы, что стало самым значительным отличием пятой версии от четвёртой.
Классы привнесли свежий ветер в язык, сделав его ещё привлекательнее для программистов, заинтересованных в ООП.
Я застал этот переход и ещё больше вдохновился этим в чём-то парадоксальным языком.
Со временем я стал повсеместно замечать довольно странное использование классов — фанатическое внедрение ООП там, где оно, в принципе, не нужно.
Но я отвлёкся, вернёмся ближе к теме. Одна из проблем классов заключалась в том, что они практически не помогали решить вопрос повторного использования. Особенно это стало заметно среди -быдло-среднестатистического кода. Наследование, на мой взгляд, только усугубляло ситуацию.
В результате стали появляться классы-потомки, наделённые кучей методов, которые не используются. В добавок сильная связанность классов — стала нормой, которая маскировалась возможностью расширения и использованием своих «связок».
Чтобы как-то повлиять на сложившуюся сиутацию, в версии 5.4 были введены трейты (traits), также известные как примеси, миксины.
Суть достаточно проста: наконец-то появились концептуально корректные варианты добавить классу необходимую функциональность, не связывая его с другими классами.
Первые размышления
Наблюдая всю эту картину, я периодически задумывался о том, что класс по своей сути — всего лишь структура, оболочка для нескольких связанных логикой функций и свойств. И настал таки момент, когда я решился на пробу новой концепции.
Раз класс, по моему виденью, просто оболочка, то почему бы не представить его как мир, наделённый свойствами и действиями, которые могут быть связаны, а могут и нет; могут знать о существовании друг друга, а могут и не знать.
Результатом стала сборка класса из трейтов. Т.к. в трейтах позволяется определять и магические методы, то результирующий класс может быть ничем не обделён, по сравнению с «обычными» классами. А удобный синтаксис подключения трейтов, позволяет элементарно автоматизировать сборку.
Небольшой пример
Давайте я приведу небольшой фрагмент кода с подключением трейтов, чтобы было понятнее о чём я говорю:
<?php
require_once 'traits/Rivers.php';
require_once 'traits/Rocks.php';
require_once 'traits/Lands.php';
require_once 'traits/Sky.php';
require_once 'traits/Animals.php';
require_once 'traits/Birds.php';
require_once 'traits/PHP.php';
class World
{
use Rivers, Rocks, Lands, Sky, Animals, Birds, PHP;
}
Я специально привёл require, чтобы было видно, что трейты реквайрятся также, как классы. Ещё для наглядности приведу пример пары трейтов:
traits/Rivers.php:
<?php
trait Rivers
{
public $rivers = [];
public function getRubyFromTheBottom(){...}
}
traits/Animals.php:
<?php
trait Animals
{
public $animals = [];
public function drinkWater()
{
if(!empty($this->rivers))
$water = array_shift($this->rivers);
}
}
Примеры лишены особого смысла, они просто показывают, что ничто не мешает взаимодействовать трейтам между собой.
Что я хочу
Итак, что я хочу — один класс, в котором есть всё необходимое без надобности подгружать ещё что-либо. Кроме того, я должен иметь полный контроль за тем, что подключается.
Последнее условие решается тривиальным конфигом сборки в духе
<?php
return ["Rivers", "Rocks", "Lands", "Sky", "Animals", "Birds", "PHP"];
В примере я испоьзую нативные массивы, чтобы не привлекать лишние функции по парсингу конфигов. Да нам и неважно сейчас, в каком виде хранятся настройки сборки.
Я не буду приводить код сборщика — если кому-то интересно, пишите в личку: скину ссылку на проект сделанный в такой концепции.
Давайте пройдёмся по очевидным плюсам/минусам данного подхода, а потом я поделюсь немножко практическим опытом.
Минусы:
- Нет поддержки в IDE (пользуюсь штормом). Я имею в виду, что пока я не нашёл возможности подсказать шторму, что в данном трейте я использую метод из такого-то трейта (но машинально проставляю в докблоках use). Если кто-то знает, как решить эту проблему — буду рад комментариям (и добавлю в статью).
- Уничтожается смысл private и protected полей, т.к. класс у нас один. Минус, лично для меня, сомнительный, но всё же посчитал нужным его указать.
- Непривычное управление сборкой проекта. Да, по началу действительно странно, что настройка проекта сводится, по сути, к настройке сборки класса. Но потом привыкаешь и входишь во вкус (:
- Сложный контроль. Дело в том, что если в нескольких трейтах есть метод с одинаковым именем и вы не разрулили этот конфликт, то будет ошибка (правда она отловится в момент сборки, но всё равно неприятно).
- Что-то ещё из комментариев — уверен, что-нибудь подскажут.
Плюсы:
- Это новый, другой взгляд на разработку на РНР. Приносит своеобразный кураж, когда пробуешь новый подход, который не просто корректирует маршрут, а полностью меняет дорогу.
- Элементарны расширение и замена пластов функционала разного уровня. Подключаем трейт в сборку — оппа, у нас новый функционал, о котором наш «мир» ничего не знал. Например, беря классику жанра примеров — синглтон: подключаем трейт реализующий этот функционал и наш мир уже «одиночка». Заменили трейт и уже вместо json'a у нас используется yaml (см. пример под списком)
- Можно одновременно иметь несколько сборок с разным поведением. Удобно при тестировании: собрали несколько вариантов с разными модулями, запустили тесты и посмотрели, что эффективнее, корректнее и т.п. Кроме того, это может быть полезно при реализации функционала, подразумевающем роли/группы пользователей: для каждой группы можно собрать свой класс (в котором уже не будет проверок на роли и всего сопутствующего).
- Что-то ещё из комментариев — надеюсь, что-нибудь подскажут.
trait Json
{
public function getConfig(){...}
}
trait Yaml
{
public function getConfig(){...}
}
trait Somewhere
{
public function inTheCode(){... $this->getConfig ...}
}
Теперь предлагаю перейти к практическому опыту.
У меня имеется два проекта, которые я попробовал выполнить в данной концепции. При разработке я пришёл к следующим выводам/решениям/взглядам:
1. Элементарна поддержка событийной модели.
На данный момент в одном из проектов сделал так: у меня есть простая карта вида:
[
'событие' => ['слушатель', 'listener']
]
И во время срабатывания события происходит нечто подобное:
if(method_exists($this, $listener))
$this->$listener($context)
Где контекст — это какие-то данные, которые сопутствуют событию.
Т.е. это тот момент, когда чувствуешь профит от того, что всё в одном классе.
2. Попробовал довольно странный метод хранения конфигов — тоже в трейтах.
Т.е. условно это выглядит так:
trait World_Config
{
public $worldConfig = [
// какие-то данные
];
}
И в настройках сборки я указываю нужный мне конфиг. Это очень удобно по нескольким причинам:
- По опыту, как правило, в проектах формируются конфиги, которые потом постоянно используются. Соответственно, зачем каждый раз парсить этот конфиг, если можно сразу его подцеплять и использовать.
- Возвращаясь к ситуации с разными ролями, для каждой роли так удобно подключать свой конфиг.
- Нам сразу же в момент создания объекта доступны все данные из конфига. На самом деле это открывает большие возможности.
Заключение
Думаю, для начала информации достаточно, чтобы поэкспериментировать (:
Если эта тема будет интересна, то я могу написать ещё несколько статей с детальным описанием одного из своих проектов, в котором опишу реальные боевые условия, в которых приходится разрабатывать в такой концепции.
Если есть вопросы — задавайте, с удовольствием отвечу.
p.s. Если вы знаете какие-то рабочие проекты с подобной реализацией — пишите, буду рад добавить в статью.
Автор: Funcraft