Адаптер і фасад обидва стоять між вашим кодом і чимось складнішим, тому їх легко сплутати. Відрізняє їх напрям інтерфейсу. Адаптер починається з інтерфейсу, який у вас уже є (PaymentGateway::charge(string $orderId, int $kopecks)), і змушує чужий SDK під нього підлаштуватися: перекладає копійки в гривні, масив відповіді в рядок ідентифікатора, коди помилок у ваш PaymentDeclined. Фасад нічого не підганяє: він вводить новий, вужчий вхід до кількох власних сервісів і бере на себе знання про порядок кроків.
У проєкті з двома платіжками доречні обидва. Кожен провайдер отримує свій адаптер за спільним портом, і сценарій оплати не знає, з ким говорить. Якщо оплату супроводжують фіскалізація чеку й повідомлення бухгалтерії, їх можна сховати за фасадом, який викликають сценарії. Важливо, щоб адаптер справді ізолював постачальника: обʼєкти SDK і його винятки не повинні виходити назовні, інакше заміна провайдера зачепить усі місця виклику так само, як без адаптера.
Фасади Laravel взяли назву, але не намір. Cache::get() нічого не обʼєднує: Facade::__callStatic() дістає з контейнера сервіс за ключем з getFacadeAccessor() і викликає на ньому звичайний метод. Це статичний проксі до одного обʼєкта, і саме тому працюють Cache::shouldReceive(), Mail::fake() і Queue::fake(): вони підміняють обʼєкт, до якого фасад веде. У Symfony статичних фасадів немає зовсім, там фасад GoF - звичайний сервіс з автовайрингом, як і в Laravel, коли ви пишете його самі.
Обидва патерни мають межу корисності. Адаптер навколо зручного SDK, який використовується в одному місці й міняти який ніхто не планує, лише додає клас. Фасад над одним сервісом, що переказує його методи, нічого не ховає. А фасад, у який дописують усе повʼязане з темою, перетворюється на God object з десятками залежностей: тримайте його на одному сценарії або вузькій групі, а коли сценаріїв багато, діліть на кілька фасадів чи сервісів застосунку.
// Порт: інтерфейс, який ваш код описує своєю мовою
interface PaymentGateway
{
/** @return string ідентифікатор платежу в провайдера */
public function charge(string $orderId, int $kopecks): string;
}
// Adapter: перекладає ваш виклик на чужий клієнт і назад, поведінку не змінює
final class LiqPayGateway implements PaymentGateway
{
public function __construct(private LiqPay $sdk) {} // офіційний SDK
public function charge(string $orderId, int $kopecks): string
{
// api() повертає stdClass з json_decode(); параметри скорочено
$response = $this->sdk->api('request', [
'action' => 'pay',
'version' => 3,
'order_id' => $orderId,
'amount' => $kopecks / 100, // API чекає гривні, ваш код рахує в копійках
'currency' => 'UAH',
]);
// Формат відповіді й помилки постачальника не виходять за межі адаптера
if (($response->status ?? null) !== 'success') {
throw new PaymentDeclined($response->err_description ?? 'unknown');
}
return (string) $response->payment_id;
}
}
// Facade: новий простий вхід, за яким кілька сервісів і порядок їх виклику
final class Shipping
{
public function __construct(
private Carrier $carrier,
private LabelPrinter $printer,
private Tracking $tracking,
) {}
public function send(Order $order): string
{
$waybill = $this->carrier->createWaybill($order);
$this->printer->print($waybill);
$this->tracking->watch($waybill, $order);
return $waybill;
}
}
Розведіть патерни двома питаннями: чий інтерфейс (ваш готовий чи новий) і скільки обʼєктів за обгорткою (один чужий чи кілька своїх). Потім окремо скажіть про Laravel: `Cache::` - це статичний доступ до сервісу з контейнера, а фасад у сенсі GoF ви пишете звичайним класом і інжектуєте.