Паттерн Singleton в PHP
Сегодня мы с вами изучим ещё один паттерн – Singleton. Этот паттерн относится к числу порождающих паттернов проектирования, то есть тех, с помощью которых в нашей программе создаются объекты. Прежде чем перейти непосредственно к самому паттерну синглтон, давайте поймём проблему, которую он решает.
Давайте взглянем более детально на код наших сущностей User и Article. Оба этих класса наследуются от класса ActiveRecordEntity, а следственно имеют методы getById() и findAll(). Давайте посмотрим их код.
src/MyProject/Models/ActiveRecordEntity.php
/**
* @return static[]
*/
public static function findAll(): array
{
$db = new Db();
return $db->query('SELECT * FROM `' . static::getTableName() . '`;', [], static::class);
}
/**
* @param int $id
* @return static|null
*/
public static function getById(int $id): ?self
{
$db = new Db();
$entities = $db->query(
'SELECT * FROM `' . static::getTableName() . '` WHERE id=:id;',
[':id' => $id],
static::class
);
return $entities ? $entities[0] : null;
}
Как видим, каждый раз при вызове этих методов у нас создаётся новый объект класса Db. Разумеется, это приводит к тому, что каждый раз вызывается конструктор класса Db и устанавливается новое соединение с базой данных.
То есть вот такой код:
$article = Article::getById($articleId);
$nickname = $article->getAuthor()->getNickname();
приведёт к тому, что будет создан новый объект Db и установлено новое соединение с базой данных при:
- вызове метода Article::getById($articleId)
- вызове метода User::getById($this->authorId) внутри $article-> getAuthor()
То есть одно и то же действие будет выполнено дважды. А ведь создание нового объекта, установка соединения с базой данных - всё это занимает время, а программы нужно писать так, чтобы они выполнялись за минимальное количество времени.
Давайте чтобы убедиться в том, что объект действительно создаётся дважды, создадим статическое свойство у класса Db, в котором будем хранить число вызовов конструктора. Мы уже проделывали подобное в уроке “Статические методы и свойства в PHP”.
Итак, давайте добавим классу статическое свойство $instancesCount, по умолчанию равное нулю.
src/MyProject/Services/Db.php
<?php
namespace MyProject\Services;
class Db
{
private static $instancesCount = 0;
/** @var \PDO */
private $pdo;
public function __construct()
{
self::$instancesCount++;
...
Сделаем его приватным. В конструкторе в самом начале будем увеличивать этот счётчик на единицу.
Также давайте добавим публичный статический метод, который будет возвращать значение этого счётчика.
src/MyProject/Services/Db.php
...
public static function getInstancesCount(): int
{
return self::$instancesCount;
}
...
Давайте теперь временно добавим вывод этого значения в конце выполнения программы. Просто добавим вывод с помощью var_dump() в конце нашего фронт-контроллера.
www/index.php
...
var_dump(\MyProject\Services\Db::getInstancesCount());
Теперь перейдём на страничку со списком статей http://myproject.loc/ и увидим внизу странички значение 1.
Всё в порядке – одно единственное соединение с базой данных. Но что будет, если мы перейдём на страничку с одной статьей, где мы выводим автора? Давайте проверим: http://myproject.loc/articles/1 - теперь значение уже 2. А что будет, если мы потом добавим статьям хотя бы такой функционал как рубрики и комментарии? Будет уже создано 4 объекта и установлено 4 соединения с базой! Это будет значительно замедлять наш скрипт.
А как на счёт того, чтобы использовать статическое свойство класса для того, чтобы хранить единственный созданный экземпляр этого класса? То есть в свойство класса мы положим созданный объект класса Db, а потом сможем использовать его, когда нам потребуется. Ведь статические свойства принадлежат классу и всем его объектам целиком и в единственном экземпляре.
Давайте создадим в классе Db статическое свойство $instance, в котором будет храниться созданный объект.
src/MyProject/Services/Db.php
<?php
namespace MyProject\Services;
class Db
{
private static $instancesCount = 0;
private static $instance;
...
А теперь давайте добавим в этот класс специальный статический метод, который будет делать следующее:
- Проверять, что свойство $instance не равно null
- Если оно равно null, будет создан новый объект класса Db, а затем помещён в это свойство
- Вернёт значение этого свойства.
Давайте напишем этот простейший код:
src/MyProject/Services/Db.php
...
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
Теперь мы можем создавать объекты класса Db с помощью этого метода, вот так:
$db = Db::getInstance();
Теперь, когда мы вызовем этот метод несколько раз подряд, то произойдёт следующее:
- Во время первого запуска self::$instance будет равен null, поэтому создастся новый объект класса Db и задастся в это свойство. Затем этот объект просто вернётся в качестве результата
- При всех последующих запусках в свойстве $instance уже будет лежать объект и условие не выполнится. Вместо создания нового объекта вернётся уже созданный ранее.
А для того чтобы нельзя было в других местах кода создать новые объекты этого класса, стоит сделать конструктор приватным – тогда создать объект можно будет только с помощью этого метода.
src/MyProject/Services/Db.php
private function __construct()
{
self::$instancesCount++;
$dbOptions = (require __DIR__ . '/../../settings.php')['db'];
$this->pdo = new \PDO(
'mysql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['dbname'],
$dbOptions['user'],
$dbOptions['password']
);
$this->pdo->exec('SET NAMES UTF8');
}
Если мы теперь попробуем запустить наш скрипт http://myproject.loc/articles/1 то получим ошибку о том, что нельзя вызвать приватный конструктор.
Давайте изменим места в коде, в которых мы создавали новые объекты класса Db напрямую. Мы делали это в классе ActiveRecordEntity. Заменим все места с кодом
$db = new Db();
на
$db = Db::getInstance();
Получим следующее:
src/MyProject/Models/ActiveRecordEntity.php
<?php
namespace MyProject\Models;
use MyProject\Services\Db;
abstract class ActiveRecordEntity
{
/** @var int */
protected $id;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
public function __set(string $name, $value)
{
$camelCaseName = $this->underscoreToCamelCase($name);
$this->$camelCaseName = $value;
}
private function underscoreToCamelCase(string $source): string
{
return lcfirst(str_replace('_', '', ucwords($source, '_')));
}
/**
* @return static[]
*/
public static function findAll(): array
{
$db = Db::getInstance();
return $db->query('SELECT * FROM `' . static::getTableName() . '`;', [], static::class);
}
/**
* @param int $id
* @return static|null
*/
public static function getById(int $id): ?self
{
$db = Db::getInstance();
$entities = $db->query(
'SELECT * FROM `' . static::getTableName() . '` WHERE id=:id;',
[':id' => $id],
static::class
);
return $entities ? $entities[0] : null;
}
abstract protected static function getTableName(): string;
}
Попробуем снова открыть страничку с выводом одной статьи http://myproject.loc/articles/1.
И что мы видим внизу странички? Число 1! То есть несмотря на то, что мы выполнили 2 запроса к базе данных (получение статьи и получение пользователя), мы при этом создали только один объект базы данных и только одно соединение!
Так вот этот шаблон проектирования называется Singleton (синглтон). Этот паттерн говорит о том, что в рамках одного запущенного приложения будет гарантироваться что будет использован только один объект какого-то класса. Классы, реализующие паттерн синглтон сами гарантируют, что будет использоваться только один их экземпляр – создать объекты можно только с помощью специального метода, ведь конструктор больше недоступен извне. А этот метод следит за тем, чтобы не было более одного созданного объекта и предоставляет единую точку доступа к этому экземпляру. Вот и вся суть паттерна Singleton.
Давайте теперь приберемся и удалим вывод отладочной информации во фронт-контроллере www/index.php, а также удалим логику подсчёта числа созданных экземпляров в классе Db – она нам больше не нужна, так как всегда теперь будет один объект.
src/MyProject/Services/Db.php
<?php
namespace MyProject\Services;
class Db
{
private static $instance;
/** @var \PDO */
private $pdo;
private function __construct()
{
$dbOptions = (require __DIR__ . '/../../settings.php')['db'];
$this->pdo = new \PDO(
'mysql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['dbname'],
$dbOptions['user'],
$dbOptions['password']
);
$this->pdo->exec('SET NAMES UTF8');
}
public function query(string $sql, array $params = [], string $className = 'stdClass'): ?array
{
$sth = $this->pdo->prepare($sql);
$result = $sth->execute($params);
if (false === $result) {
return null;
}
return $sth->fetchAll(\PDO::FETCH_CLASS, $className);
}
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}
Теперь во всех местах, где нам нужна будет база данных, мы будем писать:
Db::getInstance()
Вот такой вот довольно простой но очень полезный паттерн проектирования, о котором начинающих часто спрашивают на собеседовании.
Текущая версия проекта на гитхабе.
Комментарии