Полезно иметь в своем арсенале инструментарий, который можно легко применять из приложения в приложение для решения широкого круга задач, не касаясь кода этого инструментария и вне зависимости от характера самих задач. В ходе разработки всевозможных приложений программистами был выделен ряд абстрактных программных решений, которые, как шаблоны или трафареты, можно свободно приложить к конкретно стоящей проблеме и вычертить по ним готовый продукт.
Нижеприведенные широко применяющиеся шаблоны проектирования (design patterns) могут быть весьма полезны в больших приложениях, в которых изменение какого-то куска кода может сильно повлиять на другие его части и на работу всей системы в целом. Дабы этого не происходило и была выделена группа шаблонов, которая сводит к минимуму в приложении тесную связь между компонентами.
Фабрика
Шаблон фабрики позволяет нам создавать объекты определенного типа не используя каждый раз напрямую инструкцию new. Вместо этого, new прописывается в одном из статических методов класса фабрики.
<?php
interface IArticle
{
function getTitle();
}
class Article implements IArticle
{
public function __construct( $id ) { }
public function getTitle()
{
return "Blog Article";
}
}
class ArticleFactory
{
public static function Create( $id )
{
return new Article( $id );
}
}
$ao = ArticleFactory::Create( 1 );
echo( $ao->getTitle()."\n" );
?>
В данном примере созданы интерфейс IArticle, в котором определено, что должен делать объект класса, реализующего этот интерфейс. Класс Article как раз его и реализует.
Статичный метод (и единственный) класса фабрики ArticleFactory просто создает новый объект Article.
Мы можем упростить вышеприведенный код, избавившись от отдельного фабричного класса и добавив фабричные методы непосредственно в класс Article.
<?php
interface IArticle
{
function getTitle();
}
class Article implements IArticle
{
public static function Load( $id )
{
return new Article( $id );
}
public static function Create( )
{
return new Article( null );
}
public function __construct( $id ) { }
public function getTitle()
{
return "Blog Article";
}
}
$ao = Article::Load( 1 );echo( $ao->getTitle()."\n" );
?>
Помимо избавления от постоянного использования new, в фабричном методе (например, Load) мы можем прописать какие-либо инициализирующие инструкции для объекта Article. К примеру, указать дату публикации или метатеги, если они не сохранены в БД по каким-то причинам. Достаточно будет один раз вписать их в фабричный метод, и потом только вызывать этот метод. Он создаст соответствующий объект и задаст начальные свойства для него. Таким образом не придется повторять один и тот же кусок кода во всех местах, где нужно создать объект Article и присвоить ему начальные значения.
Синглетон
В приложении могут наличествовать ресурсы, которые необходимо использовать в единственном и только единственном экземпляре. Например, это может быть подключение к базе данных. Довольно накладно каждый раз открывать и закрывать соединения в тех местах приложения, где нужно выполнять запросы к БД.
<?php
require_once("DB.php");
class DBConnection
{
public static function get()
{
static $db = null;
if ( $db == null ) $db = new DBConnection();
return $db;
}
private $_handle = null;
private function __construct()
{
$dsn = 'mysql://root:password@localhost/photos';
$this->_handle =& DB::Connect( $dsn, array() );
}
public function handle()
{
return $this->_handle;
}
}
print( "Handle = ".DBConnection::get()->handle()."\n" );
print( "Handle = ".DBConnection::get()->handle()."\n" );
?>
Результатом будет тот факт, что в обоих вызовах возвращается один и тот же объект и, соответственно, метод handle вернет в обоих случаях значение одного и того же свойства _handle:
% php singleton.php
Handle = Object id #3
Handle = Object id #3
%
Если в различных частях приложения применяется данный синглетон, то будет задействован один и только один экземпляр классса подключния к БД. В методе get определена статическая переменная $db , которая должна хранить объект DBConnection. При вызове get происходит проверка: если до этого где-либо в приложении не производилось обращение к синглетону, т.е. переменная $db пуста, значит нужно создать объект и поместить его в эту переменную, а затем вернуть это сохраненное в ней значение. В противном случае, нужно просто вернуть значение $db, т.к. в ней этот объект уже должен храниться.
Конечно, можно где-то в начале приложения прописать установку соединения с БД и сохранить ссылку на него в глобальную переменную. Однако в больших приложениях такой подход неэффективен. Удобнее применять принципы ООП и инкапсуляцию, избегая глобальных переменных, а для доступа к ресурсам использовать объекты и методы.
Наблюдатель
Данный шаблон проектирования предлагает еще один подход к устранению тесной взаимосвязи между компонентами. Суть в следующем: один из объектов становится Наблюдаемым за счет дополнительного метода, который регистрирует Наблюдателей. Когда Наблюдаемый объект как-то изменяется, он отсылает об этом уведомления всем зарегистрированным у себя Наблюдателям. Что дальше делают с полученной информации Наблюдатели для Наблюдаемого объекта совершенно неважно. Его основная задача оповестить другие компоненты, что у него произошли изменения, которые могут пригодиться этим самым компонентам.
Простой пример: уведомление об обновлении списка комментариев. В данном коде, когда добавляется новый комментарий, компонент комментариев оповещает об этом компонент, отвечающий за создание и отправку email пользователям и админам.
<?php
interface IObserver
{
function onChanged( $sender, $args );
}
interface IObservable
{
function addObserver( $observer );
}
class Comments implements IObservable
{
private $_observers = array();
public function addComment( $text )
{
foreach( $this->_observers as $obs )
$obs->onChanged( $this, $text );
}
public function addObserver( $observer )
{
$this->_observers[]= $observer;
}
}
class EMail implements IObserver
{
/*
... прочие методы и свойства, где здесь метод отправки письма send ...
*/
public function onChanged( $sender, $args )
{
$this->send( "Текст комментария: '$args'\n" );
}
}
$comments = new Comments();
$comments->addObserver( new EMail() );
$ul->addComment( "It's my comment." );
?>
В этом коде мы определяем 2 интерфейса, которые указывают, что должны делать Наблюдатель и Наблюдаемый. У Наблюдателя должен быть определен метод onChanged, который обрабатывает поступающую от Наблюдаемого информацию. У Наблюдаемого объекта должен быть определен метод регистрации Наблюдателей (addObserver).
После кода классов, мы создаем новый объект Comments, регистрируем в нем Наблюдателя EMail и добавляем новый комментарий. Метод добавления комментария в коде класса Comments пробегает по списку зарегистрированных Наблюдателей и у каждого из них вызывает метод onChanged. В нашем примере Наблюдатель только один и он в onChanged вызывает метод формирования и отправки письма.
Видно, что Наблюдаемому объекту совершенно неважно, что делается дальше - после того, как он отправил Наблюдателям информацию с текстом комментария. Помимо отправки e-mail можно сделать Наблюдателя из обработчика БД, который бы добавлял в базу данных текст нового комментария либо формировал таблицу-очередь рассылки e-mail. Наблюдаемый объект концентрируется на собственной части работ, а Наблюдатели - на собственной.
Цепочка команд
Данный шаблон проводит команду, сообщение, запрос или что-либо еще через цепь обработчиков. Каждый обработчик решает для себя, может ли он обработать проводимый запрос/сообщение/команду. Если может, он ее проводит и процесс прекращается. Можно добавлять и удалять из цепочки любое количество обработчиков, не задевая при этом другие.
<?php
interface ICommand
{
function onCommand( $name, $args );
}
class CommandChain
{
private $_commands = array();
public function addCommand( $cmd )
{
$this->_commands []= $cmd;
}
public function runCommand( $name, $args )
{
foreach( $this->_commands as $cmd )
{
if ( $cmd->onCommand( $name, $args ) )
return;
}
}
}
class PostCommand implements ICommand
{
public function onCommand( $name, $args )
{
if ( $name != 'addPost' ) return false;
echo( "PostCommand обрабатывает 'addPost'\n" );
return true;
}
}
class MailCommand implements ICommand
{
public function onCommand( $name, $args )
{
if ( $name != 'mail' ) return false;
echo( "MailCommand обрабатывает 'mail'\n" );
return true;
}
}
$cc = new CommandChain();
$cc->addCommand( new PostCommand() );
$cc->addCommand( new MailCommand() );
$cc->runCommand( 'addPost', null );
$cc->runCommand( 'mail', null );
?>
В этом примере мы создаем класс цепочки CommandChain, который позволяет добавлять обработчики команд и выполнять команды через них. Сначала мы добавляем два обработчика, которые являются реализациями интерфейса ICommand - PostCommand и MailCommand. Этот интерфейс предписывает обработчикам иметь у себя метод onCommand - именно он отвечает за проверку и выполнение поступившего запроса(команды).
Затем в цепочку команд мы отправляем сначала запрос на добавление поста (addPost), а после - запрос mail. Если обработчик "узнает свою команду", он ее обрабатывает и дальше процесс не идет, если же нет - запускается следующий обработчик. Обработчики в очереди пробегаются через foreach метода runCommand в классе цепочки команд.
Благодаря тому, что обработчики могут свободно добавляться и удаляться, подобная архитектура позволяет создавать легко и понятно расширяемые приложения.
Стратегия
В последнем рассматриваемом шаблоне под названием Стратегия алгоритмы работы выносятся из сложных классов дабы потом можно было легко заменить один алгоритм на другой. Если в качестве примера взять поисковый движок, то из объемного класса, описыващего его работу можно вынести ранжирование страниц в отдельный класс. Затем сделать несколько вариантов ранжирования, дополняющих друг друга и по очереди применить к классу поисковой системы.
Посмотрим на следующий простой пример:
<?php
interface IStrategy
{
function filter( $record );
}
class FindAfterStrategy implements IStrategy
{
private $_name;
public function __construct( $name )
{
$this->_name = $name;
}
public function filter( $record )
{
return strcmp( $this->_name, $record ) <= 0;
}
}
class RandomStrategy implements IStrategy
{
public function filter( $record )
{
return rand( 0, 1 ) >= 0.5;
}
}
class UserList
{
private $_list = array();
public function __construct( $names )
{
if ( $names != null )
{
foreach( $names as $name )
{
$this->_list []= $name;
}
}
}
public function add( $name )
{
$this->_list []= $name;
}
public function find( $filter )
{
$recs = array();
foreach( $this->_list as $user )
{
if ( $filter->filter( $user ) )
$recs []= $user;
}
return $recs;
}
}
$ul = new UserList( array( "Andy", "Jack", "Lori", "Megan" ) );
$f1 = $ul->find( new FindAfterStrategy( "J" ) );
print_r( $f1 );
$f2 = $ul->find( new RandomStrategy() );
print_r( $f2 );
?>
Мы определяем интерфейс стратегии IStrategy, который предписывает всем его реализаторам иметь метод filter. Затем мы определяем 2 стратегии. В первой сравнивается 2 передаваемые в стратегию строки и в зависимости от этого возвращается true или false. Во втором случае true или false возвращается в зависимости от сгенерированного случайным образом числа.
После определения классов мы создаем новый объект класса UserList, добавляя в него через конструктор имена пользователей. Затем мы вызываем метод find этого класса, который пробегает по списку имен пользователей и возвращает массив, отфильтрованный посредством переданного в этот метод объекта стратегии.
В первом случае передается стратегия со сравнением строк: если имя пользователя начинается с J или с буквы, которая в алфавите стоит после J, пользователь добавляется в массив. Поэтому в конечном массиве нет имени Andy.
Во втором случае, пользователь будет добавлен в конечный возвращаемый массив, если стратегия вернет определенное случайное значение (больше или равно 0.5). Соответственно, во втором случае конечный результат всегда будет разный.
В обоих примерах, мы задали поиск по списку пользователей с различными критериями фильтрации. Сами критерии были вынесены в отдельные классы-стратегии.