Singleton

singleton uml

Opis

Wzorzec projektowy Singleton należy do grupy wzorców konstrukcyjnych. Jego zadaniem jest dostarczenie dokładnie jednej instancji klasy o globalnym dostępie. Celowo użyłem słowa dostarczenie, gdyż kontroluje on też czy obiekt istnieje. Jeśli tak to jest zwracany. Jeśli nie to wcześniej musi zostać utworzony.

Problem i rozwiązanie

Skąd pomysł na dokładnie jedną reprezentację danej klasy? Problem wielu instancji ma miejsce w przypadkach, kiedy to obiekt jest współdzielony i dostęp do niego może płynąć z wielu źródeł, ale już sam jego stan powinien być rzeczywisty i kontrolowany. Dlaczego musi być dostępny globalnie? Każdy komponent może w jakimkolwiek momencie odwołać się do niego. Jako, że najlepiej na przykładach to od razu rzucę dwa najpopularniejsze: baza danych i plik.

Ja posłużę się przykładem klasy odpowiedzialnej za konfigurację w aplikacji. Gdyby obiektów klasy Config było kilka to łatwo wyobrazić sobie konflikt. W jednym miejscu został zmieniony jego stan, a w drugim istnieje potrzeba odczytu tego stanu. Niestety w dwóch miejscach nie są one tożsame.

Błędem jest myślenie, że klasa Config to dokładnie jeden plik konfiguracyjny. Nie ma problemu, żeby zaimplementować łączenie kilku plików w jeden. Chodzi o to, że klasa Config jest swego rodzaju jedynym i globalnym punktem dostępu do konfiguracji aplikacji. To w jaki sposób dostarczana jest konfiguracja to już inna kwestia. Wystarczy zajrzeć chociażby do frameworka Laminas, w którym to właśnie istnieją pliki local.php i global.php i są łączone na poziomie tworzenia jednej konfiguracji.

Plusy i minusy

Zacznę może od kwestii, która jest jednym z dwóch głównych założeń tego wzorca i dla mnie jest zarówno zaletą jak i wadą – globalny dostęp. Zaletą, dlatego że sytuacje w których wzorzec pasuje zawsze tego wymagają, więc spełnia założenia. Natomiast wadą, bo nad globalnym dostępem trudniej zapanować oraz nie jest łatwo go przetestować i debugować. Nad zmiennymi globalnymi ma natomiast jedną przewagę – nie da się go nadpisać.

Plusy Singletona to pewność dokładnie jednej instancji oraz jej tworzenie dopiero w momencie, gdy zajdzie potrzeba (lazy initialization). Duży plus optymalizacyjny, dzięki temu że implementacja wykorzystuje metodę statyczną i kreacja obiektu nie zawsze następuje.

Minusów niestety znajdzie się trochę więcej. Już z samej definicji tego wzorca projektowego można wyczytać złamanie zasady pojedynczej odpowiedzialności (SRP). Skoro mowa o zasadach SOLID – Singleton nie jest wzorem do naśladowania. Lamie też regułę otwarte-zamknięte (OCP). Tak jak wspomniałem, jest też ciężki do testowania i debugowania, stąd też oznaczenie klasy jako finalna – o standardowym mockowaniu i tak nie ma mowy. To, o czym rzadziej się wspomina to ukrywanie zależności. Kiedy obiekt korzysta z innych, a wstrzyknięcie zależności nie jest możliwe, staje się on ciężki w zarządzaniu i ocenie jak klasy są ze sobą powiązane.

Przykładowa implementacja w PHP

W każdej technologii implementacja tego wzorca może się nieco różnić. Zazwyczaj to co jest wspólne to prywatny konstruktor i metoda statyczna zwracająca instancję. Pierwsze, po to żeby nie dało się utworzyć obiektu klasy przez operator new, a drugie po to by w dowolnym miejscu można było pobrać instancję.

Jeśli chodzi o Singleton w PHP trzeba również zadbać o przesłonięcie metod magicznych, które umożliwiłby otrzymanie więcej niż jednego obiektu klasy. Kiedyś były to dwie __clone() i __wakeup(). W wersji 7.4 doszła nowa możliwość deserializacji poprzez nową metodę magiczną __unserialize(). Od tego momentu trzeba przesłonić również ją. Dodam tylko, że jeśli klasa zawiera implementacje obu metod: __wakeup() i __unserialize() to realizowana jest ta występująca później, czyli w poniższej implementacji druga z wyżej wymienionych. O powodach wprowadzenia nowych funkcjonalności związanych z serializacją można poczytaj tutaj: https://wiki.php.net/rfc/custom_object_serialization.

Rzucenie wyjątku we wszystkich przypadkach nie pozwoli utworzyć kopii Singletona. Można również oznaczyć te metody jako prywatne, ale ja wolę je deklarować w ten sposób.

<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Singleton;

use Exception;

final class Config
{
    private static ?Config $instance = null;

    private function __construct()
    {
    }

    public static function getInstance(): Config
    {
        if (self::$instance === null) {
            self::$instance = new Config();
        }

        return self::$instance;
    }

    /**
     * @throws Exception
     */
    public function __clone()
    {
        throw new Exception('Config is singleton - it cannot be cloned');
    }

    /**
     * @throws Exception
     */
    public function __wakeup()
    {
        throw new Exception('Config is singleton - it cannot be unserialized');
    }

    /**
     * @throws Exception
     */
    public function __unserialize(array $data)
    {
        throw new Exception('Config is singleton - it cannot be unserialized');
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Singleton\Test;

use DesignPatterns\Creational\Singleton\Config;
use Exception;
use PHPUnit\Framework\TestCase;
use ReflectionClass;

final class ConfigTest extends TestCase
{
    public function testIsConstructorPrivate(): void
    {
        $configReflection = new ReflectionClass(Config::class);

        self::assertFalse($configReflection->getConstructor()->isPublic());
    }

    public function testIfOnlyOneInstanceExists(): void
    {
        $config = Config::getInstance();
        $secondConfig = Config::getInstance();

        self::assertSame($config, $secondConfig);
    }

    public function testCannotCloneConfig(): void
    {
        $config = Config::getInstance();

        self::expectException(Exception::class);

        clone $config;
    }

    public function testCannotDeserializeConfig(): void
    {
        $config = Config::getInstance();

        self::expectException(Exception::class);

        unserialize(serialize($config));
    }
}

Singleton – podsumowanie

Każdy wzorzec można wykorzystać w zły sposób i rozwiązać jeden problem równocześnie tworząc nowe, często rzutujące na cały cykl rozwoju aplikacji. Przy Singletonie należy jednak szczególnie uważać, bo globalny dostęp jest sporą pokusą. Według mnie stąd właśnie opinia wśród programistów, że jest on antywzorcem. Tak jak zmienne globalne niosą więcej minusów niż plusów, tak samo źle użyty Singleton.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.