Dlaczego wzorzec projektowy singleton nie jest wzorcem trywialnym

Mogło by się wydawać, że wzorzec projektowy singleton jest najprostszym i najlepiej znanym wzorcem projektowym w programowaniu obiektowym PHP. Jednak ma on swoje mniej trywialne rozwiązania. Cały poniższy tekst i kod ma zastosowanie jeśli używanie PHP w wersji co najmniej 5.3 .

UWAGA: Poniższy tekst nie ma na celu rozstrzygania sensowności użycia antywzorca singleton. Proszę podejść prezentowanej treści następująco. Masz zastosować ten wzorzec w pewnym miejscu w kodzie i nie ważne czy Ci się to podobna czy nie. A skoro już masz go użyć to zastosuj jego najlepszą implementację.

Poniższy przykład jest klasyczną implementacją wzorca singleton.

final class singleton
{
    private static $objInstance;

    private function __construct() {}

    public static function getInstance()
    {
        if ( ! isset( self::$objInstance ) ) {
            self::$objInstance = new self();
        }

        return self::$objInstance;
    }
}

var_dump( singleton::getInstance() );

object(singleton)[1]

Kod działa poprawnie, a kod jest czytelny. W zasadzie tej implementacji nie można prawie nic zarzucić poza możliwości odwołania do instancji obiektu z wnętrza klasy.

Kolejny przykład jest rozwinięciem pierwszego, ale z kompletną hermetyzacją instancji klasy.

final class singleton
{
    private function __construct() {}

    public static function getInstance()
    {
        static $objInstance;

        if ( ! isset( $objInstance ) ) {
            $objInstance = new self();
        }

        return $objInstance;
    }
}

var_dump( singleton::getInstance() );

object(singleton)[1]

Ten super bezpieczny singleton działa dzięki odwołaniu do zmiennej statycznej wewnątrz funkcji. Do takiej zmiennej można się odwołać  tylko wewnątrz ciała funkcji, co uniemożliwia metodą obiektu odwołanie do zmiennej przechowującej instancję obiektu!

No i mogło by się wydawać, że można zakończyć omawianie wzorca projektowego singleton. Jednak według mnie to dopiero połowa przypadków użycia. W wielu implementacjach wzorca singleton celowo lub z niewiedzy nie używa się słowa kluczowego final. Oznacza to, że nie mamy żadnej kontroli nad rozszerzeniem klasy implementującej wzorzec singleton. Doprowadzić to może do niewłaściwego działania aplikacji. Istnieje jednak rozwiązanie tego problemu co zaprezentuję w dalszej części tekstu.

Pierwsza zła implementacja dziedziczenia.

class singleton
{
    private static $objInstance;

    private function __construct() {}

    public static function getInstance()
    {
        if ( ! isset( self::$objInstance ) ) {
            self::$objInstance = new self();
        }

        return self::$objInstance;
    }
}

class singletonExt extends singleton
{
}

var_dump( singleton::getInstance() );

object(singleton)[1]

var_dump( singletonExt::getInstance() );

object(singleton)[1]

W tym przykładzie błędem jest zwracanie zawsze instancji klasy singleton niezależnie na rzecz której klasy wywołamy metodę getInstance.

Równie kiepską wersją choć poprawną z punktu widzenia działania aplikacji jest poniższe zdublowanie kodu.

class singleton
{
    private static $objInstance;

    private function __construct() {}

    public static function getInstance()
    {
        if ( ! isset( self::$objInstance ) ) {
            self::$objInstance = new self();
        }

        return self::$objInstance;
    }
}

class singletonExt extends singleton
{
    private static $objInstance;

    private function __construct() {}

    public static function getInstance()
    {
        if ( ! isset( self::$objInstance ) ) {
            self::$objInstance = new self();
        }

        return self::$objInstance;
    }
}

var_dump( singleton::getInstance() );

object(singleton)[1]

var_dump( singletonExt::getInstance() );

object(singletonExt)[2]

Rozwiązanie zwraca poprawne rezultaty, ale trzeba się bardzo pilnować żeby nie zepsuć kodu. Osobiście nie polecam.

Rozwiązaniem najlepszym jest użycia statycznej zmiennej w obrębie metody getInstance z późnym rozwiązywaniem zmiennych.

class singleton
{
    private function __construct() {}

    public static function getInstance()
    {
        static $objInstance;

        if ( ! isset( $objInstance ) ) {
            $objInstance = new static();
        }

        return $objInstance;
    }
}

class singletonExt extends singleton
{
}

var_dump( singleton::getInstance() );

object(singleton)[1]

var_dump( singletonExt::getInstance() );

object(singletonExt)[2]

W tym rozwiązanie wywołanie metody getInstance na klasie singleton oraz singletonExt zwróci poprawne instancje klas. Czyli dla każdej klasy jest tworzona osoba instancja co było naszym zamiarem.

Dobrym pomysłem jest również umożliwienie resetu instancji np. poprzez przekazanie parametru "RESET" do metody getInstance. Co umożliwi stworzenie testów jednostkowych dla takiej klasy i jest zadaniem domowym dla czytających.

Podsumowanie

Można stworzyć niezawodną implementację wzorca projektowego singleton przy niewielkiej ilości kodu i wiedzy.

Autor: Tomasz Rutkowski
Data publikacji: 2012-04-01 07:20

Opublikuj na: Facebook Twitter Wykop