Facade (Fasada)

fasada uml

Opis

Wzorzec projektowy Facade (Fasada) należy do grupy wzorców strukturalnych. Dostarcza jeden interfejs pozwalający korzystać z szerokiej gamy zachowań. Mogą one należeć do kilku klas lub modułów. Dzięki temu klient nie musi się zastanawiać, co dzieje się za fasadą. Interesuje go tylko, jakie operacje może wykonać korzystając z danych komponentów. To gwarantuje odpowiednia abstrakcja.

Problem i rozwiązanie

Tworząc bibliotekę, framework, moduł – tak naprawdę dowolny komponent – dobrym pomysłem okaże się zapewnienie odpowiedniej enkapsulacji. Co się stanie, kiedy jej zabraknie? Klient chcący skorzystać z owego kodu, niestety będzie musiał wgryźć się w szczegóły implementacyjne. Jeżeli chce zrobić to właściwie. Być może niektórych obiektów trzeba używać w konkretny sposób, a może wywołanie metod ma swoją określoną kolejność i tak dalej. Tego wszystkiego da się uniknąć gwarantując odpowiednio opisaną abstrakcję, która będzie wystarczająca, żeby w prosty sposób użyć danego komponentu.

Jednym ze sposobów na realizację tego podejścia jest właśnie wzorzec projektowy Fasada. Moduł wystawia odpowiednie API do komunikacji z nim. Odbywa się to za pomocą tak zwanej fasady – jednej, bądź wielu – w zależności od kontekstu. Analogicznie w momencie korzystania z innych modułów, gdy jakaś funkcjonalność wymaga skorzystania z więcej, niż jednego komponentu. Można wówczas stworzyć fasadę, która połączy operacje w całość i stworzy jedną abstrakcję.

Plusy i minusy

Fasada zapewnia odpowiednią enkapsulację. Gwarantuje elastyczność i wygodę używania, ale też pozwala uniknąć błędów. Dostarcza tylko konkretne metody dostępu, tym samym blokując klientowi nieuprawniony dostęp i mając odpowiednią kontrolę.

Dzięki jednemu interfejsowi, użycie tego wzorca prowadzi do luźniejszych powiązań między komponentami. Przede wszystkim – i od tego powinno się zacząć – ułatwia korzystanie z danych modułów. Wystarczy przygotować właściwie opisaną abstrakcje i swobodnie z niej korzystać we wszystkich miejscach w projekcie, gdzie akurat potrzeba użyć kilku zewnętrznych zależności.

Nie ma jednak rozwiązań bez wad. Tych natomiast, akurat w tym wypadku, łatwo uniknąć. Wystarczy właściwie podzielić interfejs na kilka mniejszych, a dzięki temu można łatwo ustrzec się złamania reguły pojedynczej odpowiedzialności (single responsibility) oraz prościej jest utrzymywać i rozwijać fasady, które też zależą od mniejszej liczby klas.

Przykładowa implementacja w PHP

Do zilustrowania fasady, przygotowałem przykład wysyłania powiadomień. Oczywiście, żeby go nie zaciemniać, sama wysyłka nie została zaimplementowana, a jedynie zaimprowizowana poprzez zwrócenie odpowiednich ciągów znaków.

Najważniejsza klasa z perspektywy tego materiału, czyli NotificationFacade scala w sobie funkcjonalności dwóch modułów. Jeden do wysyłki wiadomości SMS, a drugi do wysyłki wiadomości e-mail. Wyższy priorytet w tym wypadku ma powiadomienie typu SMS. Jednakże, jeżeli użytkownik nie posiada zdefiniowanego numeru telefonu, do akcji wkracza wysyłka e-mail. I tutaj istnieją dwie opcje: wysyłka przez chmurę (dla użytkowników posiadających konto) lub klasycznie przez SMTP z adresu ogólnego aplikacji.

Wzorzec Facade pozwala stworzyć jeden punkt dostępu do powiadomień. Dzięki czemu, nigdzie w aplikacji poza tą klasą, nie istnieją odwołania do zewnętrznych modułów. W trakcie rozwoju systemu, jeśli dojdą nowe operacje, można stworzyć nowe fasady lub rozszerzyć istniejącą o ile zachowania będą do niej pasowały.

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade\EmailModule;

interface CloudClientInterface
{
    public function hasAccount(string $email): bool;
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade\EmailModule;

use Exception;

final class UserNotFoundException extends Exception
{
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade\EmailModule;

final class CloudApiSender
{
    public function __construct(
        private CloudClientInterface $cloudClient
    ) {}

    /** @throws UserNotFoundException */
    public function send(string $email): string
    {
        if (! $this->cloudClient->hasAccount($email)) {
            throw new UserNotFoundException();
        }

        return sprintf('Email (Cloud): %s', $email);
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade\EmailModule;

final class SmtpSender
{
    public function send(string $email): string
    {
        return sprintf('Email (SMTP): %s', $email);
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade\SmsModule;

final class SmsNotifier
{
    public function notify(string $phoneNumber): string
    {
        return sprintf('SMS: %s', $phoneNumber);
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade;

final class User
{
    private ?string $phoneNumber = null;

    public function __construct(
        private string $email
    ) {}

    public function setPhoneNumber(?string $phoneNumber): void
    {
        $this->phoneNumber = $phoneNumber;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function hasPhoneNumber(): bool
    {
        return null !== $this->phoneNumber;
    }

    public function getPhoneNumber(): ?string
    {
        return $this->phoneNumber;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade;

use DesignPatterns\Structural\Facade\EmailModule\UserNotFoundException;
use DesignPatterns\Structural\Facade\EmailModule\CloudApiSender;
use DesignPatterns\Structural\Facade\EmailModule\SmtpSender;
use DesignPatterns\Structural\Facade\SmsModule\SmsNotifier;

final class NotificationFacade
{
    public function __construct(
        private CloudApiSender $cloudEmailSender,
        private SmtpSender $smtpEmailSender,
        private SmsNotifier $smsNotifier
    ) {}

    public function send(User $user): string
    {
        if ($user->hasPhoneNumber()) {
            return $this->smsNotifier->notify($user->getPhoneNumber());
        }

        try {
            return $this->cloudEmailSender->send($user->getEmail());
        } catch (UserNotFoundException) {
            return $this->smtpEmailSender->send($user->getEmail());
        }
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Facade\Test;

use DesignPatterns\Structural\Facade\EmailModule\CloudApiSender;
use DesignPatterns\Structural\Facade\EmailModule\CloudClientInterface;
use DesignPatterns\Structural\Facade\EmailModule\SmtpSender;
use DesignPatterns\Structural\Facade\NotificationFacade;
use DesignPatterns\Structural\Facade\SmsModule\SmsNotifier;
use DesignPatterns\Structural\Facade\User;
use PHPUnit\Framework\TestCase;

final class NotificationFacadeTest extends TestCase
{
    public function testCanSendSmsIfUserHasPhoneNumber(): void
    {
        $phoneNumber = '123456789';
        $user = new User('test@koddlo.pl');
        $user->setPhoneNumber($phoneNumber);

        $notificationFacade = new NotificationFacade(
            new CloudApiSender($this->createMock(CloudClientInterface::class)),
            new SmtpSender(),
            new SmsNotifier()
        );

        $this->assertSame($notificationFacade->send($user), "SMS: $phoneNumber");
    }

    public function testCanSendEmailByCloudIfUserHasAccount(): void
    {
        $email = 'test@koddlo.pl';
        $user = new User($email);
        $cloudClient = $this->createMock(CloudClientInterface::class);
        $cloudClient
            ->method('hasAccount')
            ->with($email)
            ->willReturn(true);

        $notificationFacade = new NotificationFacade(
            new CloudApiSender($cloudClient),
            new SmtpSender(),
            new SmsNotifier()
        );

        $this->assertSame($notificationFacade->send($user), "Email (Cloud): $email");
    }

    public function testIsEmailSendBySmtpIfUserDoesNotHaveCloudAccount(): void
    {
        $email = 'test@koddlo.pl';
        $user = new User($email);
        $cloudClient = $this->createMock(CloudClientInterface::class);
        $cloudClient
            ->method('hasAccount')
            ->with($email)
            ->willReturn(false);

        $notificationFacade = new NotificationFacade(
            new CloudApiSender($cloudClient),
            new SmtpSender(),
            new SmsNotifier()
        );

        $this->assertSame($notificationFacade->send($user), "Email (SMTP): $email");
    }
}

Facade – podsumowanie

Fasada jest świetną opcją, by zdjąć odpowiedzialność z klienta. Twórcy modułu sami najlepiej wiedzą, jak powinno się z niego korzystać, dlatego wystawiając taki interfejs w jasny sposób to deklarują. Przy okazji zapewniają odpowiednią implementację.

Podczas wpinania zewnętrznych zależności często korzysta się z wzorca projektowego Adapter. W momencie, gdy dostarcza właściwą fasadę, implementacja wtyczki sprowadzi się tylko i wyłącznie do delegacji wywołań metod. Nawet jeśli bezpośrednio korzysta się z zewnętrznego kodu w aplikacji – dużo luźniejsze powiązania gwarantuje jedna klasa zamiast wielu.

Fasadę może tworzyć klient kilku komponentów, ale też może być ona stworzona po stronie modułu w ramach wystawienia jednego interfejsu spinającego cały moduł. Tak naprawdę ten wzorzec projektowy albo jego wariacje wykorzystuje się bardzo często. Nawet nie zawsze będąc tego świadomym. Jednak, by robić to we właściwy sposób, przy dużej liczbie funkcjonalności, warto dzielić interfejsy na mniejsze. W ten sposób można uniknąć jednego wielkiego obiektu odpowiadającego za dziesiątki operacji, z których być może klient będzie potrzebował tylko jednej.

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