Паттерн синглтон в PHP

Паттерн Singleton в PHP

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

Давайте взглянем более детально на код наших сущностей User и Article. Оба этих класса наследуются от класса ActiveRecordEntity, а следственно имеют методы getById() и findAll(). Давайте посмотрим их код.

src/MyProject/Models/ActiveRecordEntity.php

Код доступен только после покупки курса ООП в PHP.

Как видим, каждый раз при вызове этих методов у нас создаётся новый объект класса Db. Разумеется, это приводит к тому, что каждый раз вызывается конструктор класса Db и устанавливается новое соединение с базой данных.

То есть вот такой код:

Код доступен только после покупки курса ООП в PHP.

приведёт к тому, что будет создан новый объект Db и установлено новое соединение с базой данных при:

  1. вызове метода Article::getById($articleId)
  2. вызове метода User::getById($this->authorId) внутри $article-> getAuthor()

То есть одно и то же действие будет выполнено дважды. А ведь создание нового объекта, установка соединения с базой данных - всё это занимает время, а программы нужно писать так, чтобы они выполнялись за минимальное количество времени.

Давайте чтобы убедиться в том, что объект действительно создаётся дважды, создадим статическое свойство у класса Db, в котором будем хранить число вызовов конструктора. Мы уже проделывали подобное в уроке “Статические методы и свойства в PHP”.

Итак, давайте добавим классу статическое свойство $instancesCount, по умолчанию равное нулю.

src/MyProject/Services/Db.php

Код доступен только после покупки курса ООП в PHP.

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

src/MyProject/Services/Db.php

Код доступен только после покупки курса ООП в PHP.

Давайте теперь временно добавим вывод этого значения в конце выполнения программы. Просто добавим вывод с помощью var_dump() в конце нашего фронт-контроллера.

www/index.php

Код доступен только после покупки курса ООП в PHP.

Теперь перейдём на страничку со списком статей http://myproject.loc/ и увидим внизу странички значение 1.

Вывод числа экземпляров класса

Всё в порядке – одно единственное соединение с базой данных. Но что будет, если мы перейдём на страничку с одной статьей, где мы выводим автора? Давайте проверим: http://myproject.loc/articles/1 - теперь значение уже 2. А что будет, если мы потом добавим статьям хотя бы такой функционал как рубрики и комментарии? Будет уже создано 4 объекта и установлено 4 соединения с базой! Это будет значительно замедлять наш скрипт.

А как на счёт того, чтобы использовать статическое свойство класса для того, чтобы хранить единственный созданный экземпляр этого класса? То есть в свойство класса мы положим созданный объект класса Db, а потом сможем использовать его, когда нам потребуется. Ведь статические свойства принадлежат классу и всем его объектам целиком и в единственном экземпляре.

Давайте создадим в классе Db статическое свойство $instance, в котором будет храниться созданный объект.

src/MyProject/Services/Db.php

Код доступен только после покупки курса ООП в PHP.

А теперь давайте добавим в этот класс специальный статический метод, который будет делать следующее:

  1. Проверять, что свойство $instance не равно null
  2. Если оно равно null, будет создан новый объект класса Db, а затем помещён в это свойство
  3. Вернёт значение этого свойства.

Давайте напишем этот простейший код:

src/MyProject/Services/Db.php

Код доступен только после покупки курса ООП в PHP.

Теперь мы можем создавать объекты класса Db с помощью этого метода, вот так:

Код доступен только после покупки курса ООП в PHP.

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

  1. Во время первого запуска self::$instance будет равен null, поэтому создастся новый объект класса Db и задастся в это свойство. Затем этот объект просто вернётся в качестве результата
  2. При всех последующих запусках в свойстве $instance уже будет лежать объект и условие не выполнится. Вместо создания нового объекта вернётся уже созданный ранее.

А для того чтобы нельзя было в других местах кода создать новые объекты этого класса, стоит сделать конструктор приватным – тогда создать объект можно будет только с помощью этого метода.

src/MyProject/Services/Db.php

Код доступен только после покупки курса ООП в PHP.

Если мы теперь попробуем запустить наш скрипт http://myproject.loc/articles/1 то получим ошибку о том, что нельзя вызвать приватный конструктор.

Ошибка при создании объекта с приватным конструктором

Давайте изменим места в коде, в которых мы создавали новые объекты класса Db напрямую. Мы делали это в классе ActiveRecordEntity. Заменим все места с кодом

Код доступен только после покупки курса ООП в PHP.

на

Код доступен только после покупки курса ООП в PHP.

Получим следующее:

src/MyProject/Models/ActiveRecordEntity.php

Код доступен только после покупки курса ООП в PHP.

Попробуем снова открыть страничку с выводом одной статьи http://myproject.loc/articles/1.
И что мы видим внизу странички? Число 1! То есть несмотря на то, что мы выполнили 2 запроса к базе данных (получение статьи и получение пользователя), мы при этом создали только один объект базы данных и только одно соединение!

Так вот этот шаблон проектирования называется Singleton (синглтон). Этот паттерн говорит о том, что в рамках одного запущенного приложения будет гарантироваться что будет использован только один объект какого-то класса. Классы, реализующие паттерн синглтон сами гарантируют, что будет использоваться только один их экземпляр – создать объекты можно только с помощью специального метода, ведь конструктор больше недоступен извне. А этот метод следит за тем, чтобы не было более одного созданного объекта и предоставляет единую точку доступа к этому экземпляру. Вот и вся суть паттерна Singleton.

Давайте теперь приберемся и удалим вывод отладочной информации во фронт-контроллере www/index.php, а также удалим логику подсчёта числа созданных экземпляров в классе Db – она нам больше не нужна, так как всегда теперь будет один объект.

src/MyProject/Services/Db.php

Код доступен только после покупки курса ООП в PHP.

Теперь во всех местах, где нам нужна будет база данных, мы будем писать:

Код доступен только после покупки курса ООП в PHP.

Вот такой вот довольно простой но очень полезный паттерн проектирования, о котором начинающих часто спрашивают на собеседовании.