ООП в разработке WordPress плагинов – Singleton
При разработке WordPress плагинов правильная структура и организация кода играют важную роль. C этой статьи мы начнем погружение в тему использования паттернов объектно-ориентированного программирования (ООП) в процессе разработки WordPress плагинов. От простых примеров до более сложных сценариев, мы рассмотрим, как паттерны ООП могут улучшить качество кода, обеспечив его более высокую читаемость и простоту поддержки. Первый в нашем списке паттерн Singleton
Оглавление
Singleton — пример работы
Пожалуй, самый популярный паттерн в ООП и при разработке WordPress плагинов. Используется для того, чтобы убедиться, что плагин инициализируется только один раз, и не будет создано дублей, которые приведут к ошибкам.
Суть данного паттерна в том, что класс можно создать только с помощью статического метода, который инициализирует класс только в первый раз, а затем возвращает уже созданный инстанс. Такой метод чаще всего называют get_instance. Для того чтобы класс нельзя было создать другими способами конструктору нужно добавить модификатор доступа private или protected.
Минимальный пример singleton выглядит вот так:
<?php
/**
* Класс Singleton определяет метод GetInstance, который служит
* альтернативой конструктору и позволяет клиентам снова и снова
* получать доступ к одному и тому же экземпляру этого класса.
*/
class Singleton {
/**
* Экземпляр класса хранится в статическом поле.
*/
private static ?Singleton $instance = null;
/**
* Переменная для примера.
*/
private int $counter_for_test = 0;
/**
* Конструктор Singleton всегда должен быть закрытым,
* чтобы предотвратить прямые вызовы конструкции с помощью оператора new.
*/
private function __construct() {
}
/**
* Это статический метод, который контролирует доступ к экземпляру Singleton класса.
* При первом запуске он создает экземпляр и помещает его в статическое поле.
* При последующих запусках он возвращает существующий объект клиента,
* хранящийся в статическом поле.
*/
public static function getInstance(): Singleton {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Метод для тестирования.
*/
public function test() {
return $this->counter_for_test++;
}
}
Теперь попробуем создать экземпляр класса привычным способом с помощью оператора new
:
new Singleton();
И получим ошибку:
Fatal error: Uncaught Error: Call to private Singleton::__construct() from global scope...
Теперь, чтобы получить экземпляр класса нам нужно воспользоваться методом get_instance
:
$t = Singleton::get_instance();
echo get_class( $t ); //Singleton
Также в классе созданы поле $counter_for_test
и метод test
, которые мы сейчас и запустим. Чтобы убедиться, что нам возвращается один и тот же экземпляр класса Singleton
при каждом запуске метода get_instance
, поместим результаты вызова в разные переменные и вызовем метод test
. Если всё написано правильно, то счетчик не должен сбиться даже при вызове метода на разных переменных. Проверим:
$t1 = Singleton::get_instance();
$t2 = Singleton::get_instance();
echo $t1->test(); // 0
echo $t2->test(); // 1
echo $t1->test(); // 2
echo $t2->test(); // 3
Да, теперь мы точно видим, что работаем с одним экземпляром класса.
Singleton — улучшаем пример
Остается решить вопрос переиспользования кода и разбиения логики по классам. Раз мы активно используем ООП в работе, то можем вынести всю логику Singleton в отдельный абстрактный класс, а бизнес логику оставим в классе App.
Перед тем как идти дальше, давайте разберем в чем разница между ключевыми словами self
и static
в PHP:
Если кратко, то self указывает на класс в котором метод или свойство описаны, а static на метод, в котором код выполняется. Проще всего это понять на примере:
class A {
protected static $name = 'ClassA';
public static function get_self_name() {
return self::$name;
}
public static function get_static_name() {
return static::$name;
}
}
class B extends A {
protected static $name = 'ClassB';
}
echo A::get_self_name(); //1) ClassA
echo A::get_static_name(); //2) ClassA
echo B::get_self_name(); //3) ClassA
echo B::get_static_name(); //4) ClassB
1) и 2) случаи нас не сильно интересуют, ведь если нет наследования и переопределения, то self
и static
отрабатывают одинаково
3) Несмотря на то, что в классе B
имя переопределено, вернется ClassA
. Это происходит потому, что get_self_name
вызывается в классе А
, где свойство $name
равно ClassA
. А как мы помним, self
указывает на класс, в котором метод определен
4) Здесь же вернется ClassB
, потому что get_static_name
хотя и также определен в классе A
, но работает с оператором static
, который берет свойства из того класса, из которого вызывается метод. Вызывается он из класса B
, а значит вернет ClassB
Теперь вернемся к паттернам. Следующий пример содержит правильную идею, но требует доработок:
abstract class Singleton {
private static ?Singleton $instance = null;
private function __construct() {
}
public static function get_instance(): Singleton {
if ( is_null( self::$instance ) ) {
self::$instance = new static(); //Меняем self на static, чтобы вызывать конструктор дочернего класса
}
return self::$instance;
}
}
class App1 extends Singleton {
private int $counter_for_test = 0;
public function test() {
return $this->counter_for_test++;
}
}
class App2 extends Singleton {
private int $counter_for_test = 0;
public function test() {
return $this->counter_for_test++;
}
}
echo App1::get_instance()->test(); // 0
echo App2::get_instance()->test(); // 1, а должен быть 0
Здесь мы создали два класса App на основе класса Singleton, но метод get_instance
будет возвращать только экземпляр того класса, на котором он был вызван в первый раз, в примере это App1. Давайте убедимся в этом:
echo get_class( App1::get_instance() ); //App1
echo get_class( App2::get_instance() ); //App1
Следующий запуск приложения, теперь классы в другом порядке:
echo get_class( App2::get_instance() ); //App2
echo get_class( App1::get_instance() ); //App2
Почему так? Всё дело в статической переменной $instance класса Singleton
. В нее помещается первый созданный экземпляр дочернего класса и теперь он и только он будет создаваться и возвращаться.
Решить эту проблему можно достаточно легко. Нужно хранить не один экземпляр, а их массив. В нем ключом будет имя класса, а значением созданный экземпляр.
Давайте реализуем это:
abstract class Singleton {
/**
* Экземпляры дочерних классов также хранятся в статическом поле, но теперь в виде массива.
* Для правильного сохранения данных, нужно доработать метод get_instance.
*/
private static array $instances = [];
private function __construct() {
}
/**
* Теперь мы проверяем существование экземпляра класса по имени класса.
* На данном этапе важно понимать разницу между self и static в PHP.
*/
public static function get_instance(): Singleton {
if ( ! isset( self::$instances[ static::class ] ) ) {
self::$instances[ static::class ] = new static();
}
return self::$instances[ static::class ];
}
}
Теперь протестируем вызовы нашей бизнес логики в виде метода test
у двух отдельных App
:
echo get_class( App1::get_instance() ); //App1
echo get_class( App2::get_instance() ); //App2
echo App1::get_instance()->test(); //0
echo App2::get_instance()->test(); //0
echo App1::get_instance()->test(); //1
echo App2::get_instance()->test(); //1
Именно такой результат мы и хотели получить изначально. Теперь можно удобно использовать класс Singleto
n без лишней копипасты.
Как еще можно использовать Singleton
Общий ответ достаточно прост: Singleton можно использовать там, где нужен контроль над ресурсами. Такими ресурсами могут быть объекты логирования, конфигурирования или основной класс проекта. Благодаря Singleton можно быть уверенным, что работа с ресурсами происходит из одного места и не возникнет проблем во время работы или отладки
Когда не стоит использовать Singleton
К главному минусу данного паттерна относится возникновение сложностей с написанием автотестов. Singleton создает глобальное состояние, а значит, тестируя метод, использующий это глобальное состояние (например данные из конфига), мы тестируем и класс производный от Singleton. А еще параллельный запуск тестов невозможен, ведь ресурс один, а значит и тест может быть только один
Поэтому если планируется написание автоматических тестов на проекте, то, возможно, стоит отказаться от использования Singleton в главном классе приложения, заменив его на обычный класс. Но отказаться от SIngleton можно и позже, если вопрос написания тестов туманен.
Итоги
В заключение, использование паттерна Singleton в объектно-ориентированной разработке WordPress плагинов представляет собой мощный инструмент, который может значительно упростить управление ресурсами и обеспечить доступ к одному экземпляру класса в пределах всего приложения. Этот паттерн позволяет избежать создания множества лишних объектов, экономя память и процессорное время.
Кроме того, Singleton обеспечивает глобальную точку доступа к объекту, что делает его идеальным выбором для управления настройками, соединениями с базой данных и другими ресурсами, которые должны быть общими для всего плагина. Он также способствует улучшению читаемости кода и облегчает его поддержку.
Однако, важно помнить, что Singleton не всегда является наилучшим решением. В некоторых случаях, его применение может усложнить код и привести к проблемам с тестированием. Поэтому перед использованием Singleton в своем WordPress плагине, разработчикам следует внимательно взвесить плюсы и минусы и рассмотреть альтернативные подходы.
В итоге, правильно примененный паттерн Singleton может значительно улучшить структуру и производительность WordPress плагинов, делая их более надежными и эффективными. Однако, он должен использоваться разумно и только в случаях, когда это действительно необходимо.