Приватный конструктор класса в PHP

Приватный конструктор класса в PHP

Определение приватного конструктора - это паттерн, используемый в объектно-ориентированных языках программирования, который предотвращает создание экземпляра класса, вызовом из вне конструкции new ClassName(). Эта паттерн, который наделяет класс возсожностью создать объект только изнутри самого себя (ниже покажу детальнее, что это значит).

Впервые увидев этот паттерн на практике, я был немного растерян. Не сразу было понятно, где такая функция будет полезна в реальном мире, и какой смысл в создании private конструктора. В сознании возникал логичный вопрос: как вы собираетесь использовать класс, если он не может быть создан снаружи? Зачем вообще пытаться определить конструктор, если он не может быть вызван?

Но, оказывается, есть несколько случаев использования, когда приватный конструкторы приходятся очень кстати. В этой статье я расскажу о нескольких случаях, в которых он несёт несослужимую службу. Примеры приведены на PHP, но их можно перенести на любой язык объектно-ориентированный язык программирования, который поддерживает эту функцию.

Основы

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

class User
{
    private function __construct()
    {
        // ...
    }
}

new User();
// Fatal error: Uncaught Error: Call to private User::__construct() from invalid context…

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

Если унаследованный класс определяет свой собственный конструктор, оригинальный, приватный, не будет вызван.

class SuperUser extends User
{
    public function __construct()
    {
        // ...
    }
}

new SuperUser();
// Это успешно сработает

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

final class User
{
    const ROLE_USER = 'user';
    const ROLE_ADMIN = 'admin';

    private $role;

    private function __construct(string $role)
    {
        $this->role = $role;
        // ...
    }

    public static function user(): self
    {
        return new self(self::ROLE_USER);
    }
    
    public static function admin(): self
    {
        return new self(self::ROLE_ADMIN);
    }
}

$user = User::user();
// Если нужно создать обычного пользователя

$admin = User::admin();
// Или же, создаём админа

Теперь, когда мы ознакомились с фундаментальными понятиями того, как мы можем взаимодействовать с приватными конструкторами в PHP, давайте наконец-то узнает, зачем нужен приватный конструктор в PHP и рассмотрим некоторые шаблоны, в которых он используется, и которые, благодаря ему, можно реализовать.

Синглтоны (Singletons)

Синглтон (объект одиночка) - это объект, который может быть создан только один раз за время выполнения приложения. В случаях попытки создания нового объекта синглтона, будет возвращать ранее (первый) созданный объект данного класса. Обычно это реализуется в контейнере внедрения зависимостей или объектах запроса/ответа веб-фреймворка.

С помощью приватного конструктора мы можем заставить программистов взаимодействовать с ним только через статический метод, в котором реализуем код создания синглтона на PHP. В результате чего, будет существовать только одна возможность создания экземпляра нашего класса - через статический метод. А в этом методе уже реализуем условие по которому будет создавать именно синглтон, вместо того, чтобы поручать пользователю делать это самостоятельно.

final class Singleton
{
    private static $instance;

    private function __construct()
    {
        // ...
    }

    // паттерн создания синглтона
    public static function getInstance(): self
    {
        if (is_null(static::$instance)) {
            static::$instance = new self();
        }

        return static::$instance;
    }
}

var_dump(Singleton::getInstance());
// object(Singleton)#1 (0) {}

var_dump(Singleton::getInstance());
// ссылка на тот же объект
// object(Singleton)#1 (0) {}

Обратите внимание, что независимо от того, сколько раз мы вызываем Singleton::getInstance(), возвращается один и тот же экземпляр объекта с одним и тем же идентификатором.

Класс, содержащий только статические методы

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

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

final class StringHelpers
{
    private function __construct() {}

    public static function e(string $value): string
    {
        return htmlspecialchars($string);
    }

    public static function upper(string $value): string
    {
        return strtoupper($value);
    }

    public static function lower(string $value): string
    {
        return strtolower($value);
    }

    // ...
}

Предопределённые значения

Иногда вам может потребоваться создать экземпляр класса, над которым вы хотите получить больший контроль при его создании, и, какие параметры он может принимать, например, иметь заранее определённый список значений. Подобным подходом мы воспользовались в начале статьи, когда запретили создание пользователя напрямую через конструктор, инкапсулировав логику их создания в статические методы: user(), и admin().

Другая возможность - это нормализация/модификация входных параметров, и создание на основании них нужных экземпляр класса.

final class PaymentFailedException extends Exception
{
    private function __construct(string $message)
    {
        parent::__construct($message);
    }

    public static function lowBalance()
    {
        return new self('У вас слишком мало денег на счету.');
    }

    public static function fraudulentTransaction()
    {
        return new self('Текущий платеж был автоматически отклонен по причине мошенничества.');
    }

    // ...
}

// где-то в клиентском коде
throw PaymentFailedException::lowBalance();

Enum-значения в PHP

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

С полностью пустым приватным конструктором мы можем предотвратить создание экземпляров подобных классов.

final class UserStatus
{
    const ACTIVE = 'active';
    const MODERATION = 'moderation';
    const BANNED = 'banned';
    const DELETED = 'banned';

    private function __construct() {}
}

class User extends Model
{
    // ...

    public function delete()
    {
        $this->setStatus(UserStatus::DELETED);
    }
}

Резюме

Из этой статьи вы должны были увидели, что есть множество полезных применений приватного конструктора в PHP, при которых они являются идеальным решением. Несмотря на то, что они часто упускаются из виду в пользу других функций, которые может иметь язык программирования, таких как абстрактные классы/методы или перегрузка методов (не для PHP), они так же имеют место быть.

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