Диагноз: Трейтизм

в 11:40, , рубрики: php, traits, метки: ,

Всем привет.

Сегодня я хочу рассказать о довольно забавном способе использования РНР и трейтов — сборке класса с нужным вам функционалом, по кусочкам.

Интересно? Тогда прошу под кат.

Прежде, чем мы перейдём непосредственно к делу, давайте совершим небольшую прогулку в РНР.

Как это было

Итак, в своё время, при разработке РНР 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js