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ących 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, a 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, tak by 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ę, przez co gwarantuje elastyczność i wygodę używania, ale też pozwala uniknąć błędów, gdyż 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 korzystać z niej 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 SRP oraz prościej jest utrzymywać i rozwijać fasady, które też zależą od mniejszej liczby klas.

Przykładowa implementacja w PHP

Do zilustrowania fasady, przygotowany został 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ągu 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 SMS, a drugi do wysyłki 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 $this->phoneNumber !== null;
    }

    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ą, aby 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ąc 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 często 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ł.

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.