Шаблон проектирования Одиночка (Singleton).
  • 443

Шаблон проектирования Одиночка (Singleton pattern).

Автор: admin | 16 июня (Вс.) 2019г. в 21ч.24м.

Применение паттерна Singleton.

Данный паттерн применяется там, где нужно создавать объект класса единожды. При этом объект сохраняется в свойстре класса и в дальнейшем объект берется из этого свойства. Простой пример - создание объекта подключения к базе данных. 
Шаблон проектирования Одиночка (Singleton) считается антипаттерном. Это связано с тем, что объект, созданный при помощи данного паттерна является глобальным и он может быть доступен из разных мест программы. Кроме того, состояние объекта может изменяться, что может привести к ошибкам в программе. Также Singleton добавляет сложности в тестировании. Также нарушается принцып един­ствен­ной ответ­ствен­но­сти SOLID, т.к. класс построенный по шаблону отвечает за две весчи - создание и хранение объекта(самого себя) и непосредственно функционала в классе, для которого он создавался. Поэтому не стоит злоупотреблять данным шаблоном, а пользоваться им там, где он наиболее уместен. Но все же для большей гибкости программы лучше применять другой шаблон - Инъекцию Зависимости (Dependency Injection) иногда совместно с Сервис Локатором (Service Locator), но об этом в другой статье. Рассмотрим Одиночку подробнее...

Принципы шаблона Singleton.

  • Конструктор __construct () объявляется как защищенный (или приватный), чтобы предотвратить создание нового экземпляра вне класса с помощью оператора new.
  • Магический метод __clone () объявляется как защищенный (или приватный), чтобы предотвратить клонирование экземпляра класса с помощью оператора clone.
  • Магический метод __wakeup () объявляется как защищенный (или приватный) для предотвращения десериализации экземпляра класса с помощью глобальной функции unserialize().
  • Новый экземпляр создается с помощью поздней статической привязки в методе статического создания getInstance() (название может быть любым) с ключевым словом static. Это позволяет создавать подклассы класса Singleton.
  • Если от класса одиночки не будут наследоваться подклассы, то можно класс объявить финальным и указание на самого себя в п.4 указать как self. Также защищенные методы можно сделать приватными. 
//Базовый класс.
class Singleton {
  
  protected static $instance = null;
  
  private function __construct()
  {
      //Тут можно реализовать подключение к базе данных и т.п.
  }
 
  //Объект создается из этого же класса
  protected static function getInstance()
  {
      if (static::$instance == null)
      {
          static::$instance = new static;
      }
 
      return static::$instance;
  }

  protected function __clone()
  {
      //Тут какой-то код
      throw new Exception('Feature disabled.');
  }

  protected function __wakeup()
  {
      //Тут какой-то код
      throw new Exception('Feature disabled.');
  }

}​
Создание объекта класса Singleton:
$obj1 = Singleton::getInstance();​
$obj2 = Singleton::getInstance();​

echo $obj1 === $obj2 ? 'Равны' : 'Не равны'; // Выведет - Равны

Можно также хранить объект в классе в локальной переменной:

//Свойство private static $instance заменено на локальную статическую переменную
protected static function getInstance()
  {
      static $instance = false;

      if ($instance == null)
      {
          $instance = new static;
      }
 
      return $instance;
  }

Примеры применения Singleton.

Для создания соединения с базой данных.

// Singleton для соединения с бд.
class ConnectDb {
  
  private static $instance = null;
  private $connection;
  
  private $host = 'localhost';
  private $user = 'db user-name';
  private $pass = 'db password';
  private $name = 'db name';
   
  // The db connection is established in the private constructor.
  private function __construct()
  {
    $this->connnection = new PDO("mysql:host={$this->host};
    dbname={$this->name}", $this->user,$this->pass,
    array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'"));
  }
  
  public static function getInstance()
  {
    if(!self::$instance)
    {
      self::$instance = new self;
    }
   
    return self::$instance;
  }
  
  public function getConnection()
  {
    return $this->connection;
  }
}

//Применение.
$conn = ConnectDb::getInstance()->getConnection();​

Наследование от Singleton.

class Singleton
{
    private static $instances = array();
    protected function __construct() {}
    protected function __clone() {}
    public function __wakeup()
    {
        throw new Exception("Cannot unserialize singleton");
    }

    public static function getInstance()
    {
        $class = get_called_class(); // late-static-bound class name
        if (!isset(self::$instances[$class])) {
            self::$instances[$class] = new static;
        }
        return self::$instances[$class];
    }
}

//Наследование
class Foo extends Singleton {
    protected $label;

    public function setLabel($label)
    {
        $this->label = $label;
    }

    public function getLabel()
    {
        return $this->label;
    }
}

class Bar extends Singleton {
    //Какой-то код
}

//Пример использования
Foo::getInstance()->setLabel('Любой текст');
$label = Foo::getInstance()->getLabel();

//Еще пример
echo get_class(Foo::getInstance()) . "\n";
echo get_class(Bar::getInstance()) . "\n";​

Применение в трейтах.

В PHP у нас есть хороший инструмент, позволяющий избежать дублирования одних и тех же блоков кода в разных классах - трейты. В нашем случае мы можем трейт вместо класса, который будет использоваться многими другими классами, если они должны реализовать эту функциональность:
//Трейт вместо класса
trait Singleton 
{
    private static $instance;
    
    private final function __construct() {}
    private final function __clone() {}
    private final function __wakeup() {}
    
    public final static function getInstance() 
    {
        if(!self::$instance) {
            self::$instance = new self;    
        }
        
        return self::$instance;
    }
}

class DatabaseManager {
    use Singleton;

    protected $db;

    protected function __construct()
    {
       $this->db = new PDO('mysql:dbname=foodb;port=3305;host=127.0.0.1','foouser','foopass');
    }
}

//Далее используем наследование и в классе наследнике обнуляем instance
class DatabaseManagerChild extends DatabaseManager {
  /**
   * Мы повторяем $instance, чтобы его содержимое было
   * завязано на этом классе, а не на родителе
   **/
   protected static $instance = null;
}

//Так далее...​

Читайте также из этой серии:

Приветствую!

Меня зовут Сергей. Я - автор этого блога.

Если Вам был полезен материал на моем сайте, поддержите пожалуйста мой проект, чтобы о нем узнали другие люди - кликните plizz :) на иконку в соц. сети, чтобы поделиться материалом с другими.