Основная идея
Паттерн Singleton (одиночка или синглтон) используют в нескольких случаях:
- когда требуется, чтобы существовал только 1 объект какого-либо класса,
- в случае, если инициализация объекта требует много ресурсов,
- если объект используется во многих местах, а передавать его в качестве параметров методов проблематично.
Это достаточно простой паттерн, поэтому во многих проектах он используется «на полную катушку». Однако, необходимо помнить, что Singleton свойственны все недостатки, присущие статическим методам — по причине явного указания имени класса одиночки тестирование затрудняется, а развивать проект становится труднее. Поэтому используйте Singleton только там, где действительно требуется создание одного и только одного экземпляра объекта. Во многих случаях использование Singleton можно избежать, если внедрить в проект паттерн Registry (реестр).
Проблема с использованием Singleton
Если вы знакомы с проблемами, которые связаны с использованием статических вызовов в рабочем коде для тестирования, то вам должно быть понятно, почему одиночки затрудняют тестирование. Например:
<?php class A { function method1() { $b = B :: instance(); $b->doSomething(); } } class B { function instance(){...} function doSomething() { // method implementation } } ?>
Это типичная ситуация, которая характерна для большого количества проектов. Есть класс А, который использует методы класса B, являющегося одиночкой. В этом случае мы имеем зависимость класса А от класса B, а не от какого-либо интерфейса. При тестировании класса А нам придётся создавать фикстуру, которая будет обеспечивать правильную инициализацию класса В, поэтому тест будет бОльшим по размеру, сложнее, более хрупким и т. д. Не будем развивать эту мысль, так как она была уже очень хорошо описана ранее.
Использование одиночек в качестве диспетчеров
Итак, основная проблема с Singleton — это явное указание класса одиночки в коде. Так как мы не можем изменить эту зависимость, значит нужно иметь возможность подменять реализацию интерфейса одиночки. То есть Singleton нужно использовать для делегирования выполнения методов другим объектам. То есть гораздо лучше будет сделать так:
<?php class A { function method1(){ $b = B :: instance(); $b->doSomething(); } } interface Server { function doSomething(); } class B implements Server { private $server; function instance(){...} function doSomething(){ $this->server->doSomething(); } function setServer($server){ $this->server = $server; } } class C implements Server { function doSomething() { // method implementation } } ?>
Теперь можно передать в одиночку B объект другого класса в качестве $server. То есть мы будем иметь возможность расширить поведение класса A, в случае необходимости. Плюс теперь тест не обязан знать детали реализации интерфейса Server.
<?php Mock :: generate('Server'); class ClassATest extends UnitTestCase { private $mock_server; function setUp() { $this->mock_server = new MockServer($this); $this->_fixtureForClassA(); } function tearDown() { $this->mock_server->tally(); } function test1(){ $b = B :: intance(); $b->setServer($this->mock_server); $this->mock_server->expectOnce('doSomething'); $this->mock_server->setReturnValue('doSomething', $for_result1); $a = new A(); $this->assert($a->method1(), $result1); } } ?>
Данный способ хорошо зарекомендовал себя в случае, когда одиночек мало, а их интерфейсы не слишком раздуты. Как аналог можно предложить добавление в Singleton метода setInstance($new_instance). Этим приёмом очень часто пользуются при рефакторинге legacy систем, которые напичканы одиночками.
Использование паттерна Registry
Паттерн Registry является прекрасной альтернативной использованию Singleton. Registy может заменить целый набор одиночек в случаях, когда внедрение одиночек было связано именно с экономией на инициализации объекта, а также в том случае, если одиночкой был просто «популярный» объект.
Например, если все клиенты используют Registry для получения часто используемых объектов, то достаточно внедрить LazyLoading для каждого такого объекта. Например:
<?php class AnyRegistry extends Registry { private $_some_object; function getSomeObject() { if($this->_some_object) return $this->_some_object; $this->_some_object = new SomeObject(); return $this->_some_object; } } ?>
Даже если требование по наличию только одного экземпляра какого-либо класса класса сохраняется, то конструктор такого класса можно закрыть и продолжать использовать Singleton. Но в целях облегчения тестирования, клиенты, всё же, должны получать этот экземпляр только через Registry.
Автор: Вики.Агилдев.Ру.