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;
class UserNotFoundException extends \Exception
{
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Facade\EmailModule;
class CloudApiSender
{
private CloudClientInterface $cloudClient;
public function __construct(CloudClientInterface $cloudClient)
{
$this->cloudClient = $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;
class SmtpSender
{
public function send(string $email): string
{
return sprintf('Email (SMTP): %s', $email);
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Facade\SmsModule;
class SmsNotifier
{
public function notify(string $phoneNumber): string
{
return sprintf('SMS: %s', $phoneNumber);
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Facade;
class User
{
private string $email;
private ?string $phoneNumber = null;
public function __construct(string $email)
{
$this->email = $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;
class NotificationFacade
{
private CloudApiSender $cloudEmailSender;
private SmtpSender $smtpEmailSender;
private SmsNotifier $smsNotifier;
public function __construct(
CloudApiSender $cloudEmailSender,
SmtpSender $smtpEmailSender,
SmsNotifier $smsNotifier
) {
$this->cloudEmailSender = $cloudEmailSender;
$this->smtpEmailSender = $smtpEmailSender;
$this->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ł.
Odpowiedz