PHP

индекс
206,76

Singleton и Late static binding

Количество Singleton'ов в проекте зачастую прямо пропорционально его сложности и размеру. Естественно, что описывать закрытый конструктор, статическое свойство-объект и метод его получения для сколь-либо ощутимого количества классов немного утомительно, да и пожалуй неверно. Отсюда встаёт вопрос: как «вынести за скобки» реализацию Singleton'а?

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

Второй способ заключается в реализации суперкласса Singleton'а, от которого будут наследоваться все классы-Singleton'ы. Однако в его реализации на PHP есть один подводный камень — позднее связывание (late/dynamic binding) в статических методах. Точнее — определение имени текущего класса в статическом методе getInstance.
Стандартные средства вроде метода get_class тут применить нельзя, так как никакого объекта у нас ещё собственно нет. В голову приходит отражение, но быстро понимаешь, что тут оно ни причём, так как исходной точкой для него является имя объекта, которое мы собственно и пытаемся узнать.
И тут нам на помощь приходит механизм по названием «позднее статическое связывание» (late static binding), доступный начиная с версии 5.3.0. Суть его заключается в возможности сослаться на вызываемый класс в контексте статического наследования. Другими словами, начиная с версии 5.3.0 мы можем использовать функцию get_called_class для получения имени вызываемого класса в рамках статического метода.

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

  1. /**
  2. * Singleton pattern implementation
  3. */
  4. abstract class Singleton {
  5.  
  6.   /**
  7.    * Collection of instances
  8.    * @var array
  9.    */
  10.   private static $_aInstance = array();
  11.  
  12.   /**
  13.    * Private constructor
  14.    */
  15.   private function __construct(){}
  16.  
  17.   /**
  18.    * Get instance of class
  19.    */
  20.   public static function getInstance() {
  21.  
  22.     // Get name of current class
  23.     $sClassName = get_called_class();
  24.  
  25.     // Create new instance if necessary
  26.     if( !isset( self::$_aInstance[ $sClassName ] ) )
  27.       self::$_aInstance[ $sClassName ] = new $sClassName();
  28.     $oInstance = self::$_aInstance[ $sClassName ];
  29.     
  30.     return $oInstance;
  31.   }
  32.  
  33.   /**
  34.    * Private final clone method
  35.    */
  36.   final private function __clone(){}
  37. }
* This source code was highlighted with Source Code Highlighter.


Дополним код небольшим тестом

  1. class Example extends Singleton {}
  2. $oExample1 = Example::getInstance();
  3. $oExample2 = Example::getInstance();
  4. echo ( is_a( $oExample1, 'Example' ) && $oExample1 === $oExample2)
  5.   ? 'Same' : 'Different', "\n"
* This source code was highlighted with Source Code Highlighter.


результатом которого будет, как вы уже догадались, «Same».

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

P.S.
Согласен, что наследование — не лучший механизм в данном случае. На мой взгляд, если бы PHP поддерживал аннотации и АОП, было бы намного удобней помечать классы аннотацией @Singleton и реализовать аспект, осуществляющий инъекцию необходимого для реализации Singleton'a кода в помеченные классы. Однако, на сколько мне известно, ни аннотаций (как конструкции языка), ни АОП в PHP в ближайшее будущее не планируется.
_________
Текст подготовлен в ХабраРедакторе
+28
28 февраля 2010, 16:46
56

комментарии (61)

–4
kykapa4a #
Очень хорошая реализация, т.к. постоянное дублирование кода при наследовании, создании новых singleton'ов уже порядком надоело. Вместе с заплаткой для версий ниже 5.3.0 механизм получается универсальным.
0
AlexeyTokar #
наследование создано не для того, что бы избавляться от дублирования кода в одиночках.
+1
Joka #
мне кажется вы спутали принципы наследования. Наследовать нужно только тогда когда ребенок является более специфической реализацией родителя. Например дверь — железная дверь. А у вас получается наследуем потому что все синглтоны, а что они реализуют вообще не важно.
0
krestjaninoff #
Согласен. Я сказал в постскриптуне, что наследование тут не самый удачный инструмент, но другого пути реализации в PHP я не вижу. У вас есть идеи?
–2
Joka #
я бы все таки использовал доступ к классам которые должны быть «синглтонами» через реестр. Zend_Registry как раз, например, для этого. Ибо в вашем случаем мне ничего не мешает сделать class MyClass extends Example и переопределить конструктор чтобы он мог создавать обьекты и тд и тп. Нет предела человеческой глупости :)
+3
mrmot #
Singleton + Registry = Factory
Но вот по поводу глупости — я с Вами согласен.
–1
kykapa4a #
Отвечу за автора =). Тут всё нормально: singleton — singleton для конфига.
+2
Ueasley #
Лучше фабрику (абстрактную, если нужно).
0
Goodkat #
В шестой версии будет static и позднее связывание для статических методов. Вообще, думал, что это появилось уже в 5.3.
0
ArtyV #
Ну как бы и написано, что появилось. И появилось собственно: )
0
Goodkat #
так нельзя будет делать? static::$instance = new static();
0
ArtyV #
Когда нельзя будет?
0
Goodkat #
Когда доступно static::
В php 5.3+
Может быть там только методы так вызывать можно, но не конструктор?
0
ArtyV #
Конструктор не проверял, но т.к. все делают через get_called_class видимо нельзя. Сам не проверял
0
Goodkat #
ниже krestjaninoff подтвердил, что всё работает.
+6
remal #
Если так надо, используйте IoC контейнеры, Registry и т.п.

Кроме того, если честно, не очень понятно в какой ситуации данное решение может пригодится. Для написания самих синглтонов делается один макрос в IDE…

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

Да, и автоподстановка в IDE работать не будет в вашем случае. Ибо у метода getInstance() в документации не указать возвращаемый тип.

ЗЫ: Ваши строчки про аннотации из какого языка взяты? Java? В этом случае эти строчки вызывают крайне много вопросов.
0
tzlom #
сейчас на меня набросятся люди с топорами, НО:

class A
{
static $data;
static Bla()
{
echo self::$data;
}

static Init($str)
{
static $inited = false;
if(!$inited)
self::$data = $str;
}
}

A::Init('test');

в принципе такой код полностью симулирует синглтон, при этом его всё ещё можно наследовать и конструировать, тоесть разницы никакой, ну разве что

$a = new A;
$a->method();

или

A::method();

а собсна что вам нравится решайте сами
+3
Fortop #
Сдается мне Вы не понимаете паттерна синглтон — вся суть которого в том, что объект контролирует число своих инстансов самостоятельно.
+1
tzlom #
боюсь что «сути» этого чудо действия я не понимаю
особенно в свете приведённого в статье кода, который работает точно так же…
+4
Mecid #
это потому что код статьи кривой
0
tzlom #
ну, возможно и кривой и реализует частный случай
однако замечу что паттерн это возможность а не руководство к действию, и в многих применениях такой код бывает полезен
0
keltanas #
ИМХО, синглтоны не стоят того, чтобы из-за них так мучиться.
Ибо, как всем известно, в php5 нет пронстранств имен и синглтоны ни чем не лучше использования глобальных переменных. Введете 2 синглтона с одинаковыми именами и придется делать рефакторинг всего проекта (((
Так что смысл синглтонами делать только классы шаблона Register, а в них уже хранить ссылки на остальные синглтоны.
Подробнее в книге www.books.ru/shop/books/693675
+1
dborovikov #
Поддерживаю. Если развить идею Registry со ссылками на объекты прямиком приходишь к паттерну Dependency Injection. А все эти singleton-ы это только минимальное решение проблемы времени жизни объектов.
+2
reflexing #
+1
Alroniks #
опередили :)
0
keltanas #
Это PHP 5.3
У моего хостера, к примеру, установлен 5.2.11 :'-((
Что же мне делать?
+2
Alroniks #
если мне не изменяет память, то в php 5.3.* пространства имет таки имеются.
0
Mecid #
чтобы Singelton ваш был единственным используйте IoC(DI)
0
krestjaninoff #
Буду очень, очень бладгодарен за пример инверсии контроля приминительно к данному случаю на PHP.
+1
nekufa #
Очень хорошо всё описано в документации компонента Dependency Injection. Очень рекомендую.
0
krestjaninoff #
О, благодарю!
0
krestjaninoff #
Если бы речь шла о Java + Spring, то проблема, на сколько мне известно, действительно решалась бы объявлением следующего baen'а:

<bean id="example" class="package.ExampleImpl" singleton="true">

</bean>

Но в данном случае речь о не о Java.
0
dohlik #
Вместо is_a() лучше использовать instanceof, ЕМНИП :)
0
conf #
с версии 5.3 опять разрешили :)
0
romka777 #
Не стоит делать конструктор приватным, т.к. его нельзя будет переопределить в дочерних классах. Исправьте на протектед.
0
romka777 #
туплю
0
pyatigil #
Когда количество синглтонов в проекте расте пропорционально сложности и размеру, стоит пересмотреть подход к архитектуре, т.к. подобный способ ломает модульность кода и сильно мешает юнит тестированию.

По-хорошему, класс должен сам знать все свои депенденси, так что особой нужды в жутких количествах синглтонов нет.
0
tzlom #
ну кстати ни разу не правы

модульность кода может достигаться за счёт использования указателей на объект
а вот класс не всегда знает свои зависимости, допустим если один и тот же класс работает с разными базами данных
тогда только 2 решения — или прокси на нужную базу или указатель на синглет работы с БД
0
pyatigil #
>> модульность кода может достигаться за счёт использования указателей на объект
не понял мысль

>> а вот класс не всегда знает свои зависимости, допустим если один и тот же класс работает с разными базами данных
>> тогда только 2 решения — или прокси на нужную базу или указатель на синглет работы с БД
решение в духе ООП для такой ситуации — это интерфейс
синглтон для этого не нужен. Интерфейсов в пхп для БД много — PEAR::DB, adodb…
0
tzlom #
про казатель не очень точно выразился, идея следующая:

class DatabaseRecord
{
private $db;
function __construct($database)
{
self::$db = $database;
}

fucntion read()
{
$db = self::$db;
$db::doSomething();
}
}
+1
pyatigil #
абсолютно согласен — никакой нужды в синглтонах в данном примере нет =)
+1
smartov #
В PHP 5.3.0 для позднего статического связывания появился референс static:: который резолвится именно так, как надо.

php.net/manual/en/language.oop5.late-static-bindings.php
–1
krestjaninoff #
Да, но для получения имени вызываемого класса использовать его не получится.
+1
smartov #
Для создания синглтона это и не требуется
–1
krestjaninoff #
Уверены? Можно пример?
+2
alexshelkov #
class Sin
{
public $a = 1;

static private $_instance;

static function getInstance()
{
if (! self::$_instance ) {
self::$_instance = new static; // тут только начиная с php 5.3.*
}

return self::$_instance;
}
}

class B extends Sin
{
public $a = 2;
}

$s = B::getInstance();

var_dump($s);
0
smartov #
Опередили :)
0
Goodkat #
О! А этот код «компилируется»? :)
А то мы тут выше теоретические рассуждения разводим, работает это или нет — проверить негде.
0
krestjaninoff #
Проверил — компилируется. И даже правильно работает ) Я почему-то ошибочно полагал, что через static можно обращаться только к методам/свойствам.
0
Goodkat #
А почему вы пишете:
if (! self::$_instance ) {
self::$_instance = new static; // тут только начиная с php 5.3.*

а не
if (! static::$_instance ) {
static::$_instance = new static();
?
Или static:: работает только для методов, но не для полей? А скобочки после new static не нужны?
0
krestjaninoff #
Оба способа монописуальны, так как при создании класса с помощью конструкции new в случае отсутствия параметров скобки можно опускать:

ini_set('display_errors', 1);
error_reporting(E_ALL | E_STRICT);

class Test {
    function __construct() {
        echo 'Test Constructor', "\n";
    }
}

$oTest1 = new Test(); // Печатает 'Test Constructor'
$oTest2 = new Test; // Печатает 'Test Constructor'
0
alexshelkov #
Потому что $_instance приватное свойство класса Sin, и не доступно для потомков. Вообще можно объявить $_instance как protected, тогда можно будет использовать static::$_instance. Я просто хотел чтобы было как у автора (private свойство), он вроде тоже об этом упоминает.

> А скобочки после new static не нужны?
Как вам больше нравиться =) Классы можно создавать и так и так. Т.е:

class B {}
$a = new B;

тоже вполне допустимо.

> Или static:: работает только для методов, но не для полей?
Нет, работает и так и так, как я уже писал здесь self только из-за того что поле private.
0
Goodkat #
Да, не заметил приватности инстанции.
А в случае self::$_instance не перепишется приватное поле родительского класса?

Хотя все эти вопросы чисто теоретические — мне вряд ли светит переход на php6 :)
0
alexshelkov #
> Хотя все эти вопросы чисто теоретические — мне вряд ли светит переход на php6 :)
Ну в общем то php6 тут и не нужен, хватит php 5.3.

> А в случае self::$_instance не перепишется приватное поле родительского класса?
Гм, я не уверен, что понял, что именно вы спрашиваете. Если объявить в классе B static private $_instance, то нет, ничего не перепишется. Обращаясь к B self::$_instance и к Sin self::$_instance вы получите. Т.е когда модификатор свойства private потомок не может напрямую получить доступ к нему.

class Sin
{
    static private $_instance = 'Sin $_instance';
 
    public function test()
    {
        return self::$_instance;
    }
}
 
class B extends Sin
{
 
    static protected $_instance = 'B $_instance';
 
    public function test2()
    {
        return self::$_instance;
    }
}
 
$s = new B;
 
var_dump($s->test()); // Sin $_instance
echo '<Br>';
var_dump($s->test2()); // B $_instance


0
Goodkat #
В промежуточном переходе на php5.3 смысла я не вижу, мне интересней нативная поддержка юникода в php6 — часто сталкиваюсь с проблемами с восточноевропейскими кодировками.

Вопрос был в другом:
class Sin
{
public $a = 1;

static private $_instance;

static function getInstance()
{
if (! self::$_instance ) {
self::$_instance = new static; // тут только начиная с php 5.3.*
}

return self::$_instance;
}
}

class B extends Sin
{
public $a = 2;
}

$b = B::getInstance();
$sin = Sin::getInstance();

Мне кажется, что переменные $b и $sin будут имет одно и то же значение — инстанцию класса B, так как он создастся раньше, но сохранится в общей для них поле self::$_instance в классе Sin.
0
alexshelkov #
Вот, теперь все ясно. Отвечаю (раз уж начал, то до конца) =) Да вы правы, и действительно свойство $_instance будет одно на всех. Для того чтобы это избежать, нужно делать что-то вроде, того что привел автор. Ну или объявлять static protected $_instance в потомках (и в родительском классе), и так же использовать везде static:: вместо self::. Я думаю можно еще найти какие-то способы, но все они мне почему-то не очень нравятся, ибо помахивают каким-то не здравым шаманством.
0
alexshelkov #
Еще один Singleton. Крутой пример, он несколько по другому подходит к проблеме. Используется static «внутри» функции.
0
krestjaninoff #
На сколько мне известно ввиду того, что некоторые нововведения, которые планировались в 6-ой версии, были бэкпортированы в 5-ую, сколь-либо скорого появления 6-ой версии ихо ожидать не стоит.
0
tenshi #
а мне нравится такой вариант:
$example_obj= $core->new_example;

где ядро самостоятельно находит файл 'inc/example.php', исполняет его получая имя класса, инстанцирует и кэширует результат.
0
fuse #
Ох, уж эти префиксы в названиях переменных.
–1
Partizan #
Чем дальше, тем все больше убеждаюсь, что singleton не для web. Он больше мешает, чем приносит пользы.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.