Казалось бы, из всех шаблонов проектирования что может быть проще всем известного синглетона. Во многих классических примерах реализации на разных языках программирования она может занимать всего пару десятков строк а того и меньше.
Так получилось, что я реализую этот шаблон вот уже второй год начиная с первого выхода PHP 5.3 в 2009 году. В то время у его предшественника версии 5.2 не было позднего статического связывания и для создания экземпляра класса в метод приходилось передавать его имя, что казалось мне архинеудобным.
С выходом PHP 5.4, взглянув еще раз на старую реализацию и на новые возможности языка, я переписал этот шаблон еще раз получив — как мне казалось тогда и кажется сейчас — конечный вариант.
Подробности реализации ниже.
Сразу бы хотелось отметить основные особенности:
- Параметрическое порождение. Позволяет создавать экземпляры классов используя сигнатуру вызова метода ::getInstance. Каждой сигнатуре будет соответствовать свой экземпляр класса. По умолчанию такой тип порождения отключен. Включается в дочерних классах переопределением метода ::useParametricInstantiation.
- Получение дочернего объекта по имени родительского класса. Позволяет ссылаться на дочерние классы из родительских а также из других классов не зная их имени.
- Создание дочернего класса по имени родительского класса. Аналогично второму пункту, только в случае если дочерний объект не был создан к моменту вызова метода — создаст его.
Теперь сама реализация на PHP 5.4 в качестве примеси, последнего нововведения этого языка. Размер кода небольшой, поэтому привожу его здесь.
<?php
/**
* Trait TSingleton.
* An implementation of the Singleton design pattern.
*/
namespace Traits;
const SINGLETON_GLOBAL_VARS_PREFIX = 'singleton';
trait TSingleton
{
/**
* Do not allow creating object by the new operator.
*
* @final
* @access private
* @return void
*/
final private function __construct() { }
/**
* Do not allow cloning object.
*
* @final
* @access private
* @return void
*/
final private function __clone() { }
/**
* Called when class is being instantiated.
*
* @access protected
* @return void
*/
protected function onCreate() { }
/**
* Returns true if child class has a parent specified by the mask.
*
* @param string $child
* @param string $parentMask
* @final
* @static
* @access public
* @return boolean
*/
final static function hasParentClass($child, $parentMask)
{
$currentClass = get_parent_class($child);
if (!$currentClass)
return false;
do
{
if (strpos($currentClass, $parentMask) !== false)
return true;
}
while ($currentClass = get_parent_class($currentClass));
return false;
}
/**
* Returns instance of child class using its parent' class name specified by the mask.
*
* @param string $parentMask Any substring of parent fully qualified class name.
* @final
* @static
* @access public
* @return array|null
*/
final static function getObjectByParent($parentMask)
{
foreach ($GLOBALS as $name => $value)
{
if(strpos($name, SINGLETON_GLOBAL_VARS_PREFIX) === false)
continue;
foreach ($value as $object)
{
if (self::hasParentClass($object, $parentMask))
return $value;
}
}
return null;
}
/**
* Finds an object by the mask of its parent's class namе. If not found the
* method will create it.
*
* @param string $parentMask
* @param array $initArgs
* @final
* @static
* @access public
* @return array|null
*/
final static function getObjectByParentSafe($parentMask, $initArgs = [])
{
$child = self::getObjectByParent($parentMask);
if ($child !== null)
return $child;
// Look up all declared classes.
$result = [];
foreach (get_declared_classes() as $class)
{
if (self::hasParentClass($class, $parentMask))
{
$result[] = call_user_func_array(($class . '::getInstance'), $initArgs);
}
}
return count($result) ? $result : null;
}
/**
* Returns child object of the parent class that called the method.
*
* @see TSingleton::getObjectByParent
* @final
* @static
* @access public
* @return array
*/
final static function getMyChild()
{
return self::getObjectByParent(get_called_class());
}
/**
* Safe variant of ::getMyChild.
*
* @see TSingleton::getObjectByParentSafe
* @final
* @static
* @access public
* @return array
*/
final static function getMyChildSafe()
{
$initArgs = func_get_args();
return self::getObjectByParent(get_called_class(), $initArgs);
}
/**
* Returns class instance.
*
* @static
* @final
* @access public
* @return TSingleton
*/
final static function getInstance()
{
static $objPool = [];
$argsArray = func_get_args();
$class = get_called_class();
if (static::useParametricInstantiation() && count($argsArray))
{
$fingerprint = '';
foreach ($argsArray as $arg)
{
if (is_array($arg) || is_object($arg))
$fingerprint .= serialize($arg);
else
$fingerprint .= $arg;
}
$key = md5($class . $fingerprint);
}
else // Use class name as a key.
$key = $class;
if (isset($objPool[$key]))
return $objPool[$key];
$instance = new $class();
$varname = SINGLETON_GLOBAL_VARS_PREFIX . $class;
if (isset($GLOBALS[$varname]))
$GLOBALS[$varname][] = $instance;
else
$GLOBALS[$varname] = [$instance];
$objPool[$key] = $instance;
call_user_func_array([$instance, 'onCreate'], $argsArray);
return $instance;
}
/**
* Enables or disables the parametric class instantiation. Disabled by default.
*
* @access public
* @static
* @return boolean
*/
static function useParametricInstantiation()
{
return false;
}
}
Метод TSingleton::useParametricInstantiation запрещает или разрешает параметрическое порождение. Как было упомянуто выше и видно из реализации по умолчанию она отключена.
Пример абстрактного класс, реализующий интерфейс взаимодействия с базой данных использующего примесь TSingleton:
namespace Lib;
abstract class CDb
{
use TraitsTSingleton;
/**
* @var PDO PDO connection object
*/
private $pdoConnect;
/**
* Returns connection string to main database.
*
* @return string
*/
abstract static function getDefaultNode();
/**
* @return string
*/
abstract function getDSNByNodeId($nodeId);
/**
* @return void
*/
final protected function onCreate()
{
$args = func_get_args();
$nodeId = $args[0];
try
{
$dsn = $this->getDSNByNodeId($nodeId);
$this->pdoConnect = new PDO($dsn, $this->getUsername(), $this->getPass(),
$this->getPDODriverOptions());
}
catch(PDOException $pdoExcep)
{
}
}
/**
* @return CDb
*/
final static function connect($node = null)
{
if ($node === null)
$node = static::getDefaultNode();
return static::getInstance($node);
}
/**
* @return true
*/
final static function useParametricInstantiation()
{
return true;
}
}
Надеюсь никого не утомило еще читать PHP код. Осталось немного. Дочерний класс MyCDb:
namespace MyApp;
class MyCDb extends LibCDb
{
const DEFAULT_NODE = 1;
const SHARD_1 = 2;
/**
* @return string
*/
function getDSNByNodeId($nodeId)
{
switch ($nodeId)
{
case self::DEFAULT_NODE:
return 'mysql:host=main.database.local;dbname=mydb';
case self::SHARD_1:
return 'mysql:host=shard1.database.local;dbname=mydb';
}
}
/**
* @return integer
*/
function getDefaultNode()
{
return self::DEFAULT_NODE;
}
/**
* @return string
*/
function getUsername()
{
return 'root';
}
/**
* @return string
*/
function getPass()
{
'mypass';
}
/**
* @return array
*/
function getPDODriverOptions()
{
return [PDO::MYSQL_ATTR_USE_BUFFERED_QUERY];
}
}
// Connection to the main database
$db = MyCDb::connect();
// Connection to the shard 1
$dbShard1 = MyCDb::connect(MyCDb::SHARD_1);
// $db !== $dbShard1
$dbShard2 = MyCDb::connect(MyCDb::SHARD_1);
// $dbShard1 === $dbShard2
Из примера выше видно, что с помощью параметрического порождения у нас есть удобная возможность получать доступ к той или иной базе данных. Разумеется область применения такого типа порождения этим не ограничивается.
Пример получения дочернего класса:
abstract class Model
{
function query()
{
// Returns instance of MyCDb class.
$db = LibCDb::getMyChild()[0];
// Safe variant
$db = LibCb::getMyChildSafe()[0];
}
}
Пожалуй все. Сама реализация паттерна тестировалась, поэтому при желании можно смело брать и пользоваться. Благодарю за внимание.
P.S.
Все же я был оптимистом, говоря, что это был конечный вариант реализации. Во время написания статьи примесь пополнилась еще двумя методами.
Автор: AccessGranted