Если вы взялись всерьёз программировать на ООП, готовьтесь к тому, что тут на каждом шагу грабли и вилы. Код более сложный и менее управляемый в силу своей запутанности, это приводит к куче логических ошибок, которые диагностировать очень трудно. Для того, чтобы избежать подобного рода недоразумений, а так же для того, чтобы как то упорядочить этот запутанный код, придуманы так называемые паттерны. Тоесть по русски - костыли, которые помогают не рухнуть сооружению, если оно строится несколькими подрядчиками или неумелыми гастарбайтерами.
Синглетон (Singleton)
Самым ярким и академическим примером является работа с базой данных.
Вот допустим у нас имеется класс для работы с БД.
<?php
class dataBase
{
public function __construct()
{
echo 'Тут как бы коннект к базе<br>';
/* mysql_connect( DBSERVER, DBUSER, DBPASSWORD );
mysql_select_db( DATABASE ); */
}
public function mysqlAssoc($query)
{
echo 'А тут как бы запрос: '. $query;
/* $res = mysql_query($query);
return mysql_fetch_assoc($res); */
}
}
$db = new dataBase();
$db -> mysqlAssoc("SELECT * FROM `table`");
Все достаточно пристойно, как водится, в конструкторе осуществляется подключение (коннект) к серверу баз данных, дальше работаем с нужными методами.
Однако если разработчиков несколько, либо имеются провалы в памяти, не исключена ситуация, что объект будет инициализирован несколько раз в разных местах. Там, где потребуется работа с базой данных. Это приведет к тому, что конструктор тоже сработает несколько раз. А значит и соединений с сервером баз данных получится несколько, а это лишние телодвижения, перерасход памяти и неоправданные тормоза:
<?php
class dataBase
{
public function __construct()
{
echo 'Тут как бы коннект к базе<br>';
/* mysql_connect( DBSERVER, DBUSER, DBPASSWORD );
mysql_select_db( DATABASE ); */
}
public function mysqlAssoc($query)
{
echo 'А тут как бы запрос: '. $query .'<br>';
/* $res = mysql_query($query);
return mysql_fetch_assoc($res); */
}
}
$db = new dataBase();
$db -> mysqlAssoc("SELECT * FROM `table`");
$db = new dataBase();
$db -> mysqlAssoc("SELECT * FROM `table`");
$db = new dataBase();
$db -> mysqlAssoc("SELECT * FROM `table`");
/*
РЕЗУЛЬТАТ
Тут как бы коннект к базе
А тут как бы запрос: SELECT * FROM `table`
Тут как бы коннект к базе
А тут как бы запрос: SELECT * FROM `table`
Тут как бы коннект к базе
А тут как бы запрос: SELECT * FROM `table`
*/
Чтобы этого избежать, придуман хитрый способ - паттерн синглетон (Singleton). В переводе с английского - одиночка.
Вот мы в класс поместим статическую переменную $instance, а в неё метод getInstance() засунет экземпляр нашего класса.
Теперь этот экземпляр можно получить двумя способами, классическим и из статической переменной:
<?php
class dataBase
{
/*
* В эту переменную мы поместим единственный
* экземпляр класса, с которым и будем работать дальше.
*/
static private $instance;
/*
* А это метод, который и осуществит сие действо
*/
public static function getInstance()
{
self::$instance = new self;
return self::$instance;
}
/*
* Ну а тут прочая белибирда
*/
public function __construct()
{
echo 'Тут как бы коннект к базе<br>';
/* mysql_connect( DBSERVER, DBUSER, DBPASSWORD );
mysql_select_db( DATABASE ); */
}
public function mysqlAssoc($query)
{
echo 'А тут как бы запрос: '. $query .'<br>';
/* $res = mysql_query($query);
return mysql_fetch_assoc($res); */
}
}
// Теперь объект можно получить так:
$db = new dataBase();
$db -> mysqlAssoc("SELECT * FROM `table`");
// Или таким извратным способом:
$db = dataBase::getInstance();;
$db -> mysqlAssoc("SELECT * FROM `table`");
/*
РЕЗУЛЬТАТ
Тут как бы коннект к базе
А тут как бы запрос: SELECT * FROM `table`
Тут как бы коннект к базе
А тут как бы запрос: SELECT * FROM `table`
*/
Обратите внимание, что инициализировали мы его ключевым словом self
//Это
self::$instance = new self;
//тоже самое, что это
self::$instance = new dataBase();
На самом деле это не придает читабельности, дело на любителя. Просто нужно знать, что так можно.
Но вернемся к овину с нашими баранами. Для чего нужен такой выкрутас? А вот для чего. Если мы теперь в классе проверим на пустоту переменную $instance, то сможем узнать, вызывался ли этот класс методом getInstance(). И если уже вызывался, сделать оргвыводы и не дать вызвать его еще раз, выдав экземпляр из этой переменной.
А чтобы не дать вызвать класс обычным способом, объявим коструктор приватным. Тоесть запретим доступ к нему снаружи. Хватит того, что мы внутри класса уже набарагозили. Ну и таким же способом запретим клонирование на всякий случай.
<?php
class dataBase
{
/*
* В эту переменную мы поместим единственный
* экземпляр класса, с которым и будем работать дальше.
*/
static private $instance;
/*
* А это метод, который и осуществит сие действо
* за одно проверив существование объекта.
*/
public static function getInstance()
{
if(empty(self::$instance))
self::$instance = new self;
return self::$instance;
}
/*
* Ну а тут прочая белибирда с приватным конструктором
*/
private function __construct()
{
echo 'Тут как бы коннект к базе<br>';
/* mysql_connect( DBSERVER, DBUSER, DBPASSWORD );
mysql_select_db( DATABASE ); */
}
public function mysqlAssoc($query)
{
echo 'А тут как бы запрос: '. $query .'<br>';
/* $res = mysql_query($query);
return mysql_fetch_assoc($res); */
}
private function __clone()
{
/* Фиг вам. */
}
}
// Теперь получить объект можно только извратным способом:
$db = dataBase::getInstance();;
$db -> mysqlAssoc("SELECT * FROM `table`");
// Несколько раз подряд
$db = dataBase::getInstance();;
$db -> mysqlAssoc("SELECT * FROM `table`");
// А тут будет ошибка
$db = new dataBase();
$db -> mysqlAssoc("SELECT * FROM `table`");
/*
РЕЗУЛЬТАТ
Тут как бы коннект к базе
А тут как бы запрос: SELECT * FROM `table`
А тут как бы запрос: SELECT * FROM `table`
Fatal error: Call to private dataBase::__construct() from invalid context
*/
Теперь сколько раз мы бы не получали объект в разных местах (других классах), соединение с базой будет произведено единожды.
Вот и весь синглетон. Теперь в любом месте мы можем получить единственный экземпляр класса. Он как бы попал в глобальное пространство. Это убережет от повторных инициализаций и пересечений, вызвав фатальную ошибку при попытке это совершить.
Фабрика (Factory)
Этот паттерн можно понять на примере обработки изображений c помощью библиотеки GD.
Не секрет, что для обработки разных форматов (.gif, .jpg, .png) используются разные функции. Это значит нам нужно сначала определить формат, а потом применить нужную функцию. Многие делают это с помощью конструкции if... else, другие с помощью переключаиеля switch:
<?php
function imageCreateForm($path)
{
$type = pathinfo($path, PATHINFO_EXTENSION);
switch ($type)
{
case 'gif':
return imagecreatefromgif($path);
break;
case 'jpeg':
return imagecreatefromjpeg($path);
break;
case 'png':
return imagecreatefrompng($path);
break;
}
}
А если обратить внимание на название функций, можно заметить, что отличаются они только окончанием. И это окончание совпадает с типом картинки. По этому функцию можно переписать так:
<?php
function imageCreateForm($path)
{
$type = pathinfo($path, PATHINFO_EXTENSION);
$function = 'imagecreatefrom'. $type;
return $function($path);
}
Вот это и есть суть паттерна "Фабрика" (Factory) на низком уровне. То есть по какому то признаку мы определяем нужную функцию, и она вступает в дело. Не нужно переписывать повторяющийся код. В данном случае это не критично, но часто бывает довольно много дублирующегося кода и такой подход оправдан.
В случае ООП фабрика обычно штампует не функции, а объекты. Нам нужно знать только название. Мы уже проходили автолоад, вот функционал фабрики чем то на него похож. Только вызывается не сам класс, а класс фабрики. И по переданному в него параметру он выдает нам желаемый объект:
<?php
class Factory
{
public static function loadClass($classname)
{
include_once $classname .'.php';
return new $classname;
}
}
$obj = Factory::loadClass('Example');
Правда толку с такой реализации чуть меньше чем никакого, автолоад справляется с этой задачей куда лучше. Оправдано может быть только применение различных библиотек, которые находятся в разных каталогах. Тогда можно вторым параметром передать название папки, где его искать. В случае с автолоадом этого не выйдет:
<?php
class Factory
{
public static function loadClass($classname, $dir)
{
include_once $dir .'/'. $classname .'.php';
return new $classname;
}
}
$obj = Factory::loadClass('Example', 'classes');
Вот это уже несколько ближе к сути. Но не сама суть. А суть они в песок в том, что параметр, определяющий что именно создавать, можно задать динамически. В выше приведенном примере это не нужно, а вот в следующем может очень даже пригодиться.
Допустим у нас есть два класса по половому признаку. Эм и Жо. В зависимости от того, кто посетил наше богоугодное заведение, мы должны выдать необходимый функционал.
<?php
/*
* Это для мужчин
*/
class WC_Men_s
{
public function __construct()
{
echo 'Ууу, еееее!<br>';
}
}
/*
* Это для женщин
*/
class WC_Women_s
{
public function __construct()
{
echo 'Хи-хи<br>';
}
}
/*
* А тут определим, кому что нужно
*/
class Factory
{
public function __construct($sex)
{
if($sex == 'men')
return new WC_Men_s();
elseif($sex == 'women')
return new WC_Women_s();
else
exit('Ахтунг!');
}
}
$obj = new Factory('men');
$obj = new Factory('women');
///////////////////////////////////
/* РЕЗУЛЬТАТ
Ууу, еееее!
Хи-хи
*/
Вот это уже похоже на правду. Нам теперь не нужно определять, объект какого класса получить. Даже не нужно знать их названия. Фабрика сделает это за нас, нужно только передать в неё определяющий признак.
Особенно удобно это бывает, когда приложение строится на более высоких уровнях абстракции. Тогда, имея один большой базовый класс, можно через фабрику генерировать объекты наследников, переопределяя необходимые методы.
<?php
/*
* Большой общий ту... класс.
*/
abstract class WC_Room
{
abstract protected function defecation();
public function washHands()
{
return 'Буль буль<br>';
}
/* Другие разные методы */
}
/*
* Это для мужчин
*/
class WC_Men_s extends WC_Room
{
public function defecation()
{
return 'Ууу, еееее!<br>';
}
}
/*
* Это для женщин
*/
class WC_Women_s extends WC_Room
{
public function defecation()
{
return 'Хи-хи<br>';
}
}
/*
* А тут определим, кому что нужно
*/
class Factory
{
static public function createObj($sex)
{
if($sex == 'men')
return new WC_Men_s();
elseif($sex == 'women')
return new WC_Women_s();
else
exit('Ахтунг!');
}
}
$obj = Factory::createObj('men');
echo $obj -> defecation();
echo $obj -> washHands();
$obj = Factory::createObj('women');
echo $obj -> defecation();
echo $obj -> washHands();
///////////////////////////////////
/* РЕЗУЛЬТАТ
Ууу, еееее!
Буль буль
Хи-хи
Буль буль
*/
Теперь для того, чтобы по какому то признаку совершить нужные действия, мы через фабрику генерируем объект наследника, переопределяя нужные методы. А те методы, которые не нужно переопределять, автоматически становятся доступными. А сам признак можно задать динамически, получив к примеру из формы. Количество кода в таком случае резко сокращается, хотя прозрачности не прибавляет:
$sex = 'men';
//$sex = 'women';
$obj = Factory::createObj($sex);
echo $obj -> defecation();
echo $obj -> washHands();
Получилась общая комната с умывальником и разными кабинками, куда мы направим представителей разных полов, что довольно часто можно встретить в жизни. Основное действо происходит в разных местах, а руки мы моем в одном, общем базовом классе.
Это уже больше подходит под определение "Абстрактная фабрика". Но определения не столь важны, важнее понять суть процессов, а реализация уже дело фантазии.