<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
ПОРОДЖУВАЛЬНИЙ ПАТЕРН

Фабричний методFactory Method

Базовий клас описує сценарій і просить потрібний обʼєкт у власного методу. Який саме клас створити, вирішує підклас, а сценарій працює з будь-яким.

Laravel Symfony складність 2 з 3 6 хв читання
ГЛІФ
01 · ПРОБЛЕМА

Сценарій оплати знає всіх провайдерів

Інтернет-магазин приймає оплату через monobank і LiqPay. Перевірка замовлення, журнал і повернення посилання однакові для всіх, а різниться лише обʼєкт, який будує посилання на оплату. Проте спільний клас сам створює кожен такий обʼєкт і тягне ключі всіх провайдерів.

src/Payments/Checkout.php
1final class Checkout
2{
3 public function pay(Order $order): string
4 {
5 if ($order->amount <= 0) {
6 throw new InvalidOrder($order->id);
7 }
8
9 $link = match ($order->provider) {
1Спільний сценарій сам вирішує, який клас створити, і тому знає конструктори всіх.
10 'mono' => new MonoLink($this->config['mono_token'], $order),
11 'liqpay' => new LiqPayLink($this->config['liqpay_public'], $this->config['liqpay_private'], $order),
2Ключі LiqPay потрібні одному рядку, але через конфіг Checkout бачить секрети всіх провайдерів.
12 // 'wayforpay' => ...
3Новий провайдер означає правку класу, через який проходить кожна оплата.
13 };
14
15 $this->log->info('payment link created', ['order' => $order->id]);
16
17 return $link->url();
18 }
19}
02 · ІДЕЯ РІШЕННЯ

Підклас вирішує, що створити

ДІАГРАМА · УСЯ СХЕМА
Кроки: 4
url()newnewnewабстрактний класCheckoutpay() · createLink()«interface»PaymentLinkurl(): stringMonoCheckoutLiqPayCheckoutновий класWayForPayCheckoutMonoLinkLiqPayLinkновий класWayForPayLink
абстрактний класCheckoutpay() · createLink()«interface»PaymentLinkurl(): stringMonoCheckoutLiqPayCheckoutновий класWayForPayCheckoutMonoLinkLiqPayLinkновий класWayForPayLink
Уся схема. Натисніть → або виберіть крок нижче, щоб пройти по ній частинами.
Абстракція Ключовий учасник Клієнт реалізує · - - ▷
  • Checkout використовує PaymentLink (url()).
  • MonoCheckout реалізує Checkout.
  • LiqPayCheckout реалізує Checkout.
  • WayForPayCheckout реалізує Checkout.
  • MonoLink реалізує PaymentLink.
  • LiqPayLink реалізує PaymentLink.
  • WayForPayLink реалізує PaymentLink.
  • MonoCheckout створює MonoLink.
  • LiqPayCheckout створює LiqPayLink.
  • WayForPayCheckout створює WayForPayLink.
03 · КОД

Запустіть і подивіться

PHP 8.5
1final class Checkout
2{
3 public function pay(Order $order): string
4 {
5 if ($order->amount <= 0) {
6 throw new InvalidOrder($order->id);
7 }
8
9 $link = match ($order->provider) {
10 'mono' => new MonoLink($this->config['mono_token'], $order),
11 'liqpay' => new LiqPayLink($this->config['liqpay_public'], $this->config['liqpay_private'], $order),
12 // 'wayforpay' => ...
13 };
14
15 $this->log->info('payment link created', ['order' => $order->id]);
16
17 return $link->url();
18 }
19}
1<?php
2
3final class Order
4{
5 public function __construct(
6 public readonly int $id,
7 public readonly int $amount,
8 ) {}
9}
10
111interface PaymentLink
12{
13 public function url(): string;
14}
15
16final class MonoLink implements PaymentLink
17{
18 public function __construct(private readonly Order $order) {}
19
20 public function url(): string
21 {
22 return "https://mono.example/invoice/{$this->order->id}?amount={$this->order->amount}";
23 }
24}
25
26final class LiqPayLink implements PaymentLink
27{
28 public function __construct(private readonly Order $order) {}
29
30 public function url(): string
31 {
32 $amount = number_format($this->order->amount / 100, 2, '.', '');
33
34 return "https://liqpay.example/checkout?order={$this->order->id}&amount={$amount}";
35 }
36}
37
382abstract class Checkout
39{
40 public function pay(Order $order): string
41 {
42 if ($order->amount <= 0) {
43 throw new InvalidArgumentException("Замовлення #{$order->id}: сума має бути більшою за нуль");
44 }
45
46 return $this->createLink($order)->url();
47 }
48
49 abstract protected function createLink(Order $order): PaymentLink;
50}
51
523final class MonoCheckout extends Checkout
53{
54 protected function createLink(Order $order): MonoLink
55 {
56 return new MonoLink($order);
57 }
58}
59
60final class LiqPayCheckout extends Checkout
61{
62 protected function createLink(Order $order): LiqPayLink
63 {
64 return new LiqPayLink($order);
65 }
66}
67
68$order = new Order(id: 1042, amount: 125_000);
69
704foreach ([new MonoCheckout(), new LiqPayCheckout()] as $checkout) {
71 echo str_pad($checkout::class, 16), $checkout->pay($order), "\n";
72}
73
74try {
75 (new MonoCheckout())->pay(new Order(id: 1043, amount: 0));
76} catch (InvalidArgumentException $e) {
77 echo 'Помилка: ', $e->getMessage(), "\n";
78}
Перший запуск завантажить рушій PHP 8.5, приблизно 3,8 МБ Відкрити в пісочниці ↗
ВИВІД
Завантажуємо рушій PHP 8.5
Лише перший раз. Далі код запускається миттєво, рушій лишається в кеші браузера.
$ php main.php ▍
  1. 1 рядки 11–36

    Продукти: спільний інтерфейс PaymentLink і по класу на провайдера. Кожен будує посилання по-своєму.

  2. 2 рядки 38–50

    Базовий клас тримає спільний сценарій pay() і просить посилання в абстрактного createLink(). Це і є фабричний метод.

  3. 3 рядки 52–66

    Підкласи перевизначають лише createLink(). Тип повернення можна звузити до MonoLink: PHP дозволяє коваріантні типи.

  4. 4 рядки 70–72

    Клієнт обирає підклас, а виклик pay() однаковий для всіх.

  5. Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
04 · У ВАШОМУ ФРЕЙМВОРКУ

Ви вже цим користуєтесь

LARAVEL

Manager::createDriver()

Illuminate\Support\Manager у методі driver() викликає createDriver(), а той шукає в підкласі метод create{Name}Driver(). Так влаштовані SessionManager і HashManager. Model factories на зразок Job::factory() мають з ним спільну лише назву: це будівельники тестових даних, а не Factory Method.

final class PaymentManager extends Manager
{
    public function getDefaultDriver(): string
    {
        return $this->config->get('payments.default');
    }
 
    protected function createMonoDriver(): MonoGateway
    {
        return new MonoGateway($this->config->get('payments.mono.token'));
    }
}

Ви вже користуєтесь цим, коли Laravel обирає драйвер сесій за SESSION_DRIVER і викликає createFileDriver() чи createDatabaseDriver().

SYMFONY

Фабрики сервісів у контейнері

Контейнер може створити сервіс не через new, а викликом методу фабрики: ключ factory у services.yaml. Тут рішення приймає метод фабрики, а не підклас, тож за GoF це ближче до простої фабрики, ніж до Factory Method.

# config/services.yaml
App\Payment\PaymentGateway:
    factory: ['@App\Payment\GatewayFactory', 'create']
    arguments: ['%env(PAYMENT_PROVIDER)%']

Ви вже користуєтесь цим, коли у services.yaml сервіс має ключ factory, і контейнер викликає метод замість конструктора.

✓ ЯК ЦЕ ТЕСТУВАТИ

Спільний сценарій перевіряйте через анонімний підклас, який повертає фейкове посилання. Кожен справжній createLink() достатньо перевірити на те, що він повертає потрібний клас.

it('refuses an empty order', function () {
    $checkout = new class extends Checkout {
        protected function createLink(Order $order): PaymentLink
        {
            return new FakeLink('https://pay.test');
        }
    };
 
    expect(fn () => $checkout->pay(new Order(id: 1, amount: 0)))
        ->toThrow(InvalidArgumentException::class);
});
05 · КОЛИ НЕ ТРЕБА

Підклас заради одного new зайвий

Якщо підклас існує лише для того, щоб викликати new, передайте готовий обʼєкт або замикання в конструктор. Композицію простіше читати й тестувати, ніж ієрархію класів.

final class Checkout
{
    public function __construct(private readonly Closure $createLink) {}
}
 
new Checkout(fn (Order $order) => new MonoLink($order));
06 · СУСІДИ

Схожі на вигляд, різні за змістом

07 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з архітектури →

Лише який конкретний клас продукту створити. Сценарій навколо створення лишається в базовому класі й працює з продуктом через інтерфейс.

Ні, спільна лише назва. Model factory описує типові атрибути в definition(), а state(), count(), make() і create() збирають тестові записи крок за кроком. За змістом це будівельник тестових даних.

Strategy підміняє поведінку: готовий обʼєкт приходить ззовні, і його можна замінити під час роботи. Factory Method стосується створення: базовий клас сам просить продукт у свого методу, а вибір закладено в підкласі через спадкування.