Diversity w polskim IT
Patrick Assoa Adou
Patrick Assoa AdouSolutions Architect

Wzorzec singleton w PHP

Dowiedz się, jak napisać singleton w PHP, aby wykorzystać jego zalety i zminimalizować wady.
12.05.20203 min
Wzorzec singleton w PHP

Kiedy potrzebujemy jednej instancja obiektu lub określonej klasy, to używamy wzorca singleton. Niektórzy jednak krzywo na niego patrzą i traktują go, jak antywzorzec



Oto niektóre z powodów, dla których singleton jest traktowany, jak antywzorzec:

  • Może on być ukrytym długiem technicznym. Założenie, że potrzebna będzie tylko jedna instancja klasy w czasie życia aplikacji może się później okazać nieprawdziwe. Możliwy skutek to żmudna refaktoryzacja.
  • Wprowadzenie stanu globalnego do aplikacji.
  • Jeśli singleton wprowadza stan globalny i go zmienia, to utrudnia to testy jednostkowe.


Wzorzec singleton nie przestrzega w rzeczywistości zasady pojedynczej odpowiedzialności (ang. Single Responsibility Principle), ponieważ wymagane zachowanie klasy (na przykład, połączenie z bazą danych) jest powiązane z kodem potrzebnym do jednoczesnego występowania pojedynczej instancji.

Aby rozwiązać powyższe problemy, dobrze jest zaprojektować klasę z właściwościami i metodami, które są jej potrzebne, a następnie zaprojektować mechanizm zapewniający, że tylko jedna instancja jest w systemie na raz. Poniżej omówimy kod w PHP, który nam to umożliwi, a w dalszej części artykułu wyjaśnię swoje podejście.

Kod

<?php
function singletonize(\Closure $func)
{
    $singled = new class($func)
    {
        // Hold the class instance.
        private static $instance = null;
        public function __construct($func = null)
        {
            if (self::$instance === null) {
                self::$instance = $func();
            }
            return self::$instance;
        }
        // The singleton decorates the class returned by the closure
        public function __call($method, $args)
        {
            return call_user_func_array([self::$instance, $method], $args);
        }
        private function __clone(){}
        private function __wakeup(){}
    };
    return $singled;
}


Możemy użyć powyższej funkcji:

<?php
class ClassThatMarksDateOfInstantiation
{
    private $dob;
    public function __construct()
    {
        $this->dob = new \DateTimeImmutable;
    }
    public function getDob()
    {
        return $this->dob;
    }
}

$factoryOfDobClass = function () {
    return new ClassThatMarksDateOfInstantiation;
};

$DobSingleton = singletonize($factoryOfDobClass); // -> anonymous class
$dob = new $DobSingleton; // -> new instance of anonymous class
$dob2 = new $DobSingleton; // -> same instance of anonymous class
print_r($dob->getDob() === $dob2->getDob()); // -> 1

Podejście


Funkcja singletonize() przyjmuje domknięcie jako argument, który jest funkcją wytwórczą dla oryginalnej klasy:

$factoryOfDobClass = function () { return new ClassThatMarksDateOfInstantiation; };

Ta klasa nie wie, ile obiektów istnieje w jednym momencie. Robi tylko to, co musi zrobić. Tworzymy nową anonimową klasę, która będzie reprezentować wzorzec singleton i delegować wszystkie wywołania metody do przechwyconej instancji danej klasy. Zwróć uwagę na użycie metody __call() do uzyskania delegacji:

public function __call($method, $args)
{return call_user_func_array([self::$instance, $method], $args);}

Co więcej, nie korzystam teraz z idiomatycznych metod getInstance(), czy instance() zwykle używanych z singletonami. Jeśli za każdym razem będziemy otrzymywać tę samą instancję, to może nie trzeba w kółko powtarzać getInstance()?

Ustawienie metod __clone() oraz wakeup() jako prywatne zapobiega:

Wzorzec singleton oraz wstrzykiwanie zależności

Prawdopodobnie zauważyliście, że singletonize() zwraca anonimową klasę. Oznacza to, że nie możemy użyć oryginalnej klasy bezpośrednio, ponieważ nie zawiera ona logiki zapewniającej istnienie jednego obiektu. Programista wywołujący new dla danej klasy otrzyma wtedy nową instancję. To zdecydowanie nie to, czego chcemy, ale na ratunek przybywa wstrzykiwanie zależności (ang. dependency injection lub DI). 

Mówiąc wprost, DI jest implementacją inwersji sterowania (ang. Inversion of Control). Albo, jak mówi Wikipedia:

[Wstrzykiwanie zależności] polega na przekazywaniu gotowych, utworzonych instancji obiektów udostępniających swoje metody i właściwości obiektom, które z nich korzystają (np. jako parametry konstruktora). Stanowi alternatywę do podejścia, gdzie obiekty tworzą instancję obiektów, z których korzystają np. we własnym konstruktorze.


Podczas wstrzykiwania zależności, zamiast ręcznego tworzenia potrzebnych instancji pozwalasz kontenerowi zależności na dostarczenie konstruktorów klas, ich metod oraz właściwości ze wszystkimi niezbędnymi zależnościami. Oto możliwy przykład użycia:

$this->container->singletonize('DobClass', $this->factoryOfDobClass);
$dob = $this->container->get('DobClass');

Trzymanie się DI pomaga w uniknięciu przypadkowego tworzenia instancji oryginalnej klasy. Nie wspominając już o tym, że DI sprzyja tworzeniu luźnych powiązań, co również może pomóc w testowaniu. 

Podsumowanie

(Anty)wzorzec singleton jest jednym z pierwszych, jakich się jako programiści w ogóle uczymy. Pomimo problemów z naruszeniem zasady pojedynczej odpowiedzialności, dostępem do globalnego stanu lub ukrytym długiem technicznym, to właściwie zrozumiany singleton może być bardzo przydatny.

To, co daje nam singletonize(), to zdolność do oddzielania pilnowania liczby istniejących obiektów od zachowania, a więc i odpowiedzialności klasy. W połączeniu ze wstrzykiwaniem zależności i singletonize(), wzorzec singleton staje się bezpiecznym i wartościowym narzędziem dla inżyniera oprogramowania.

Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>