Пишем на php… статично

в 19:05, , рубрики: php, spl, типизация, эффективность работы, метки: , , ,

PHP- это язык с динамической неявной слабой типизацией. Динамическая типизация бывает очень полезна во многих случаях, да и вообще сильно упрощает жизнь, ведь ты концентрируешься на значениях и не задумываешься о том какого они типа.

Я как и многие php программисты думал что статическая типизация это «усложнение». Она ограничивает гибкость и вообще: как люди с ней работают? И искренне не понимал, почему многие опытные программисты отдают предпочтение языкам со статической типизацией и строгой проверкой типов.

Дебаты о типизации

Я относился к правой половине людей, которые мало что знают о типах, но при этом искренне верят, что это не удобно. И так было до тех пор пока я не познакомился с одним из строго типизированных языков (c#) вплотную. С тех пор мое отношение к php да и вообще к программированию в целом изменилось.

«Титанов» программирования эта статья вряд ли заинтересует, потому что здесь вы не найдете ничего революционного и нового, а остальным заинтересованным в улучшении своего кода и собственной производительности добро пожаловать под кат.

Для начала определимся вообще что такое статическая типизация:

Статическая типизация определяется тем, что конечные типы переменных и функций устанавливаются на этапе компиляции. Т.е. уже компилятор на 100% уверен, какой тип где находится.

В php у нас нет компилятора, и все типы в любом случае будут вычисляться на этапе выполнения, но это не мешает нам самим строго следить за ними, придерживаясь определенных правил.

Для чего все это нужно в PHP?

Изучая c# и параллельно осваивая продукт, по которому ставились задачи, не имея достаточного опыта и документации (точнее документации не было вообще), меня выручало то, что благодаря строгой типизации большинство ошибок отсеивалось сразу, а еще большим плюсом были подсказки IDE, выдаваемые на основе заданных типов. Мне не нужна была документация, что бы понять что и куда передавать, я не мог сделать что то не так, оно просто не компилировалось!

Это заметно сокращало время, потому что ты сразу видишь, что этот метод, принимает только такие параметры, а создавая объекты для данных параметров, узнаешь, каких звеньев не хватает для их конструирования и так далее.

“Хорошо, это и вправду удобно” — скажите вы — “Но ты не рассказал нам ничего нового, в PHP есть PHPDoc, который понимает большинство современных IDE, и помогает так же “на лету” писать код!”

Все верно, но что бы это работало так же как в статично типизированных языках необходимо не просто написать комментарий у каждого метода и свойства класса, но важно что бы вся архитектура была заточена под работу с заранее известными объектами. Что бы построить такую архитектуру, нужно придерживаться определенных правил. Именно о них я бы хотел поговорить далее.

Используйте объекты вместо массивов

Да, именно так. Во общем эта истина не нова и о ней написано много: объекты проще расширить, к ним проще добавить методы, и вообще, дорогу ООП.

Но я хочу рассмотреть этот вопрос с другой стороны — с точки зрения построения правильной архитектуры для эффективного программирования.

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

$eventData = array(
	'sender' => $controller, //экземпляр некоего контроллера
	'name' => 'onDelete',
	'group' => 'global',
	'arguments' => array(
		'id' => 15
	),
);

$eventDispatcher->triggerEvent($eventData);

...

/**
 * Обработчик события удаления
 * @param array $eventData
 */
protected function onDelete($eventData) {
	//Здесь к нам в $eventData попадает некий массив, 
        //структуру которого мы сможем узнать только вызвав var_dump() 
        //или же найдя код где это событие вызывается
	$model = $eventData['sender']->model->deleteChilds();
}

Простейший и довольно частый случай, ничего сложного, составляем массив с данными и отправляем его в функцию.

А теперь представим, что вызов этого события находится где-то глубоко в недрах написанной не вами (или вами, но не вчера) программы.

Представили? И хорошо, если есть документация, в которой описан формат этого массива — значит вам не придется тратить время на поиски вызывающего участка кода, дабы подсмотреть структуру массива. Либо же вы воспользуетесь var_dump(), и все равно потратите время, что бы понять, что там происходит.

Давайте теперь рассмотрим что будет, если вместо массива мы описали бы класс и создали его объект. Изменим код:

class EventData {
	/**
	 * Экземпляр контроллера, инициировавший событие
	 * @var BaseController
	 */
	public $sender;
	
	/**
	 * Имя события
	 * @var string 
	 */
	public $name;
	/**
	 * Группа к которой относится событие (нэймспейс)
	 * @var string 
	 */
	public $group;
	/**
	 * Массив с произвольными аргументами
	 * @var array 
	 */
	public $arguments;
	
}

$eventData = new EventData();

$eventData->sender = $controller;
$eventData->name = 'onDelete';
$eventData->group = 'global';
$eventData->arguments = array('id' => 15);

$eventDispatcher->triggerEvent($eventData);

...

/**
 * Обработчик события удаления
 * @param EventData $eventData
 */
protected function onDelete($eventData) {
	$eventData->sender->model->deleteChilds();
}	

Что мы получили в итоге? Теперь наша любимая IDE подсказывает нам, что же содержит полученный объект.

Пишем на php… статично

Кроме того, благодаря тому что мы подробно расписали тип для каждого атрибута в переданном объекте, мы можем тут же получить подсказки и по ним. Т.е. набрав $eventData->controller-> — мы можем увидеть, что находится внутри атрибута с именем controller.

Теперь вам не придется лезть куда-то в дебри кода, для того что бы понять, что же передается в этом объекте и какого оно типа. Представьте, как вам будут благодарны другие программисты, которые будут работать с вашим кодом!

Если в вашем приложении активно используется ActiveRecord паттерн, то вы уже близки к тотальной типизации, но если вы до сих пор получаете элементы из базы данных в виде массивов, и гоняете их туда сюда, много раз от функции к функции, то стоит задуматься о смене архитектуры.

Это был простой и довольно синтетический пример. Возможно в вашем проекте в похожей ситуации и так используется объект, а не массив. Но посмотрите внимательнее, бывают ситуации не похожие на эту, например, в какую-либо функцию передаются трехэтажные массивы, или же функция имеет десяток аргументов непонятного типа.

Продолжая улучшать наш пример, я бы не остановился на создании одного класса. Обычно в современных веб-приложениях происходит много различных событий, и в них передаются разные аргументы, их можно сгруппировать, и для каждой группы событий создать свой — подходящий класс аргументов.

Не бойтесь создавать классы. Вам не обязательно создавать для каждого отдельный файл, если они взаимосвязаны друг с другом.
Например, я бы создал файл EventArguments.php, и в нём бы описал все возможные варианты аргументов событий.

Инициализируйте переменные с правильным типом.

Сразу к примеру:

	/**
	 * Получить детишек
	 * @return array
	 */
public function getChildren(){
	
	if(!$this->isLeaf) {
		return $this->find('where id = :id', array(':id', $this->id));
	}
	
}
...
		foreach($model->getChildren() as $child) {
			//делаем что либо с потомками
		}

Знакомо? Вся проблема кроется в том, что если условие !$this->isLeaf не выполнится, то результатом функции будет совсем не array, и нам придется писать кучу шаблонного кода, что бы foreach не падал, из-за неправильных аргументов.

Как должно быть:

	/**
	 * Получить детишек
	 * @return array
	 */
public function getChildren(){
	//Во первых необходимо ВСЕГДА определять переменную, прежде чем её использовать - это хороший тон. 
	//Но в нашем случае мы её определяем не абы как, а сразу с тем типом, которым предполагаем использовать. 
	$children = array(); 
	if(!$this->isLeaf) {
		$children = $this->find('where id = :id', array(':id', $this->id));
	}
	return $children;
}

Теперь если условие !$this->isLeaf не выполнится, то результатом функции будет пустой массив который мы определили в начале, и foreach в который попадет значение из этой функции не упадет если что то пойдет не так.

Рассмотренный пример — это лишь вершина айсберга. Если вы всегда будете сразу определять переменную с нужным, заранее известным типом, то вы сможете сэкономить кучу времени. А так же писать более красивый код без дополнительных ухудшающих восприятие кода проверок.

Кстати, это касается не только php. К примеру, в JavaScript в движке V8, определение типа переменной на этапе её создания ускоряет исполнение скрипта, так как не заставляет движок делать лишних преобразований (подробнее в этой статье)

Инструменты строгой типизации.

PHP имеет слабую типизацию, что означает что он не проверяет типы при операциях с ними. Т.е. вы можете сложить строку с числом, или проверить равенство между объектом скаляром, и интерпретатор вам ничего не скажет. Заставить PHP вести себя строго типизировано мы не можем. Однако в своем комплекте PHP имеет несколько удобных (пусть и не всегда логичных) инструментов для контроля типов.

Например, при определении функции вы можете явно указать, какого типа должны быть её параметры:

	public function triggerEvent(EventData $event) { //указываем что в эту функцию можно передавать только объекты типа EventData или его потомки
	}
	...
$dispatcher->triggerEvent(new EventData);  //Функция вызовется
$dispatcher->triggerEvent(new EventDataChild);  //EventDataChild - потомок EventData, функция вызовется
$dispatcher->triggerEvent(new AnotherClass()); //Catchable fatal error: Argument 1 passed to EventDispatcher::triggerEvent() must be an instance of EventData, instance of new AnotherClass given

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

Цитата со страниц мануала о контроле типов php.net/oop5.typehinting:

На данный момент функции имеют возможность заставлять параметры быть либо объектами (путем указания имени класса в прототипе функции), либо интерфейсами, либо массивами (начиная с PHP 5.1), или колбеком с типом callable (начиная с PHP 5.4).

Однако, если NULL использовался как значение параметра по умолчанию, то это будет также допустимо в качестве аргумента для последующего вызова.

Если класс или интерфейс указан для контроля типа, то все его потомки или реализации также допустимы.
Контроль типа не может быть использован со скалярными типами, такими как int или string. Трейты также недопустимы.

Делая вывод из вышесказанного, мы можем указывать почти все, кроме скалярных типов, но и на этот счет PHP имеет решение. В стандартной библиотеке SPL, содержится несколько занимательных классов призванных решить эту проблему:

  • SplInt
  • SplFloat
  • SplEnum
  • SplBool
  • SplString

Поскольку эти типы являются классами (масло масляное, но в контексте php где int является типом, но не является классом — позволительно) их можно указать в определении функции, тем самым предотвратив передачу в функцию, к примеру, строк там где требуется boolean. Соответственно эти классы вам придется использовать по всему коду, взамен стандартных скаляров.

Честно признаться, я этим не пользовался, поскольку на мой взгляд это перебор. Кроме того эти классы не входят в стандартную поставку PHP, и распространяются PECL пакетом, что затрудняет их использование.

Используйте PHPDoc везде где это возможно

PHPDoc и его поддержка в IDE — это отличный инструмент, экономящий время PHP программисту.

Поскольку синтаксис языка и его идеология не позволяет установить тип переменной при ее создании, вашей IDE очень сложно понять какая переменная чем является. Так помогите ей — укажите тип в комментарии, и ваша IDE отблагодарит вас подсказками во время работы.

Это еще один этап на пути полной типизации вашего приложения. Если вы будете знать, что получает и что возвращает нужный метод, у вас не возникнет сложностей с его использованием.

Я придерживаюсь следующих правил:
Все публичные атрибуты класса, должны иметь комментарий с описанием типа;
Все публичные методы так же должны иметь описание того, что они делают, и описание каждого параметра, включая то что они возвращают.

Небольшие подсказки для тех, кто не знаком достаточно хорошо с PHPDoc (все это корректно для среды NetBeans)

	//Если метод возвращает объект некоего класса, 
        // то можно указать имя этого типа в инструкции @return
	//И тогда IDE при получении результата этой функции будет трактовать его как объект этого класса
	
	/**
	 * Этот метод возвращает объект класса EventData
	 * @return EventData
	 */
	public function getEventData(){
		return new EventData();
	}
	
	//Если метод возвращает массив, содержащий объекты некоего класса, то это можно указать так:
	//@return EventHandler[]
	//Ключевой нюанс - это квадратные скобки после имени класса. 
	//Эта конструкция говорит парсеру, что будет возвращен массив, состоящий из объектов этого класса
	//
	//Пример:
	/** Возвращаем массив элементов EventHandler
	 * @return EventHandler[]
	 */
	public function getEventHandlers(){
		return $this->handlers;
	}
	
	public function triggerEvent(){
		foreach ($this->getEventHandlers() as $handler) {
			$handler; //на этом этапе IDE будет знать, что $handler является объектом класса EventHandler, и будет предлагать подсказки 
		}
	}
	
	public function getModel(){
		//некая фабрика возвращает объект неведомого типа 
		//(она может возвращать разные объекты. Какой именно, будет известно исходя из переданных параметров)
		//но вы точно знаете, что в этом месте метод вернет вам некий класс. Пропишите его вот так: 
		
		$model = NodeFactory::byId($id);
		/* @var $model Folder */
		$model; // здесь IDE будет подсказывать вам атрибуты и методы из класса Folder
	}

Заключение

Что бы кто не говорил, но PHP вырос до достаточно мощного инструмента. Динамическая типизация — это что позволяет быть ему максимально гибким и простым. Но вместе с ней приходят и сложности.

Пишем на php… статично

Статическая типизация, позволяет улучшить производительность труда и сократить количество ошибок в коде.

Из комментариев на хабре к статье Ликбез по типизации:

В задачах, сложность которых заставляет испытывать дикий восторг от возможности автоматической проверки хоть чего-либо, языки с сильной статической типизацией обгоняют всё остальное. Сложность задач решаемых при сайтостроении плавно приближается к такому уровню.

Мы не можем изменить отношение PHP к типам, да это и не нужно. Мы так же не можем взять и в одночасье перевести все свои проекты на какой либо другой язык, на это нет ни времени ни возможности.
Но благодаря правильно построенной архитектуре, использованию контроля типов в прототипах ваших функций, а так же документированию с помощью PHPDoc, можно разгрузить себя от лишней работы и поиска ошибок в ваших проектах.

Придерживаясь советов, изложенных в этой статье, мы сможем объединить лучшее из двух миров, и получить мощный и гибкий инструмент разработки сложных проектов.

Полезные ссылки:

Ликбез по типизации в языках программирования
Контроль типов в PHP
PHP SPL Типы
Статический анализ кода (Wikipedia)

Автор: thekip

Источник


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


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