31 октября 2023

ООП в разработке WordPress плагинов – Singleton

7 минут
ООП в WordPress разработку - превью

При разработке 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

Именно такой результат мы и хотели получить изначально. Теперь можно удобно использовать класс Singleton без лишней копипасты.

Как еще можно использовать Singleton

Общий ответ достаточно прост: Singleton можно использовать там, где нужен контроль над ресурсами. Такими ресурсами могут быть объекты логирования, конфигурирования или основной класс проекта. Благодаря Singleton можно быть уверенным, что работа с ресурсами происходит из одного места и не возникнет проблем во время работы или отладки

Когда не стоит использовать Singleton

К главному минусу данного паттерна относится возникновение сложностей с написанием автотестов. Singleton создает глобальное состояние, а значит, тестируя метод, использующий это глобальное состояние (например данные из конфига), мы тестируем и класс производный от Singleton. А еще параллельный запуск тестов невозможен, ведь ресурс один, а значит и тест может быть только один

Поэтому если планируется написание автоматических тестов на проекте, то, возможно, стоит отказаться от использования Singleton в главном классе приложения, заменив его на обычный класс. Но отказаться от SIngleton можно и позже, если вопрос написания тестов туманен.

Итоги

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

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

Однако, важно помнить, что Singleton не всегда является наилучшим решением. В некоторых случаях, его применение может усложнить код и привести к проблемам с тестированием. Поэтому перед использованием Singleton в своем WordPress плагине, разработчикам следует внимательно взвесить плюсы и минусы и рассмотреть альтернативные подходы.

В итоге, правильно примененный паттерн Singleton может значительно улучшить структуру и производительность WordPress плагинов, делая их более надежными и эффективными. Однако, он должен использоваться разумно и только в случаях, когда это действительно необходимо.