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ż dodatkowo kontroluje on istnienie obiektu. Jeśli istnieje to jest zwracany. Jeśli nie to musi wcześniej 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. Większość popularnych frameworków pozwala na umieszczenie konfiguracji w wielu plikach. Przy okazji poprawia się czytelność.

Plusy i minusy

Zacznę może od kwestii, która jest jednym z dwóch głównych założeń tego wzorca. Dla mnie jest zarówno zaletą jak i wadą – globalny dostęp. Zaletą, dlatego że sytuacje w których wzorzec znajduje swoje zastosowanie zawsze tego wymagają. Spełnia więc swoje założenia. 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ża korzyść optymalizacyjna, 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 (single responsibility). Skoro mowa o zasadach SOLID – Singleton nie jest wzorem do naśladowania. Łamie też regułę otwarte-zamknięte (open-closed). Tak jak wspomniałem, jest 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 nie tylko ciężki w zarządzaniu, ale też w 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ę. Po to żeby nie dało się utworzyć obiektu przez operator new i 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 metodę magiczną __unserialize(). Od tego momentu trzeba również ją przesłonić. Dodam tylko, że jeśli klasa zawiera implementacje obu metod: __wakeup() i __unserialize() to realizowana jest ta występująca później. 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 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 (null === self::$instance) {
            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.

Mi osobiście poza konceptualnymi przykładami nie zdarza się używać tego wzorca projektowego. Nie dlatego, że uważam go za antywzorzec, ale konkretne problemy w których mógłby się przydać najczęściej zostały już rozwiązane na poziomie frameworków. W dodatku, aktualnie istnieją sensowniejsze rozwiązania.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.