Media Belajar Coding untuk Developer Indonesia

Ruang Coding

Media belajar online bareng untuk mengembangkan keahlianmu di bidang pemrograman web dan aplikasi smartphone.

139+Member Aktif
30+Artikel & Tutorial
16+Project Siap Pakai
Free Mahir

PHP Mahir: Design Pattern, DI, Testing & Optimasi Skala Besar

Kelas ini dirancang untuk developer PHP yang sudah menguasai dasar-menengah dan ingin naik ke level production-grade setara kebutuhan industri. Anda akan mempelajari arsitektur lanjut, trade-off desain, strategi testing dengan PHPUnit, hingga teknik optimasi performa aplikasi skala besar, ditutup dengan studi kasus kompleks nyata.

2Modul
28Menit baca
PHPKategori
1

Arsitektur Lanjut PHP: SOLID, Domain-Driven Design, dan Hexagonal Architecture di Balik Layar

14 menit baca

Pendahuluan: Kenapa Arsitektur "Bagus di Kertas" Sering Gagal di Production

SOLID, DDD, Hexagonal Architecture — semua sudah familiar sebagai istilah. Masalahnya, sebagian besar developer menerapkannya secara dangkal: interface di mana-mana tanpa alasan jelas, folder Domain/ yang isinya cuma anemic model, atau "port-adapter" yang sebenarnya cuma rename dari MVC biasa. Modul ini membedah mengapa prinsip-prinsip ini bekerja, kapan mereka justru menambah kompleksitas tanpa manfaat, dan bagaimana ketiganya saling melengkapi dalam satu arsitektur koheren.

SOLID di Balik Layar: Bukan Sekadar Checklist

SOLID sering diajarkan sebagai lima aturan terpisah. Padahal secara fundamental, lima prinsip ini punya satu tujuan: mengontrol arah dependency agar perubahan tidak menjalar (ripple effect).

Dependency Inversion sebagai Fondasi, Bukan Aturan Terpisah

DIP (Dependency Inversion Principle) sebenarnya adalah mekanisme yang membuat OCP, LSP, dan ISP bisa diterapkan secara praktis. Tanpa DIP, OCP ("open for extension, closed for modification") hanya jargon kosong — karena tanpa abstraksi yang stabil, setiap ekstensi tetap butuh modifikasi kode existing.

// Tanpa DIP: high-level module bergantung pada detail
class OrderService {
    public function checkout(Order $order): void {
        $mysqlRepo = new MysqlOrderRepository(); // coupling langsung
        $mysqlRepo->save($order);

        $stripeGateway = new StripePaymentGateway(); // coupling langsung
        $stripeGateway->charge($order->getTotal());
    }
}

// Dengan DIP: high-level module bergantung pada abstraksi
interface OrderRepository {
    public function save(Order $order): void;
}

interface PaymentGateway {
    public function charge(Money $amount): PaymentResult;
}

class OrderService {
    public function __construct(
        private OrderRepository $repository,
        private PaymentGateway $gateway
    ) {}

    public function checkout(Order $order): void {
        $result = $this->gateway->charge($order->getTotal());
        if ($result->isSuccessful()) {
            $this->repository->save($order);
        }
    }
}

Perhatikan: OrderService tidak lagi tahu apakah pembayaran lewat Stripe, Midtrans, atau mock untuk testing. Inilah yang membuat unit test OrderService bisa dilakukan tanpa hit API eksternal — konsekuensi langsung dari DIP, bukan fitur PHPUnit.

ISP: Kenapa Interface Gemuk Itu Bahaya Nyata, Bukan Estetika

Interface Segregation sering dianggap soal "kerapian kode". Padahal dampaknya konkret: interface gemuk memaksa class implementasi punya dependency yang tidak relevan, yang berarti setiap perubahan pada satu method memaksa rebuild/retest semua consumer interface tersebut — masalah nyata di sistem besar dengan CI/CD yang lambat.

// Interface gemuk - bermasalah
interface Notifier {
    public function sendEmail(string $to, string $body): void;
    public function sendSms(string $to, string $body): void;
    public function sendPush(string $deviceToken, string $body): void;
}

// Class yang cuma butuh email dipaksa implement semua
class WelcomeEmailNotifier implements Notifier {
    public function sendEmail(string $to, string $body): void { /* ... */ }
    public function sendSms(string $to, string $body): void {
        throw new \LogicException('Not supported'); // code smell serius
    }
    public function sendPush(string $deviceToken, string $body): void {
        throw new \LogicException('Not supported');
    }
}

Solusi: pecah jadi EmailNotifier, SmsNotifier, PushNotifier terpisah. Setiap consumer men-depend hanya pada kontrak yang benar-benar dipakai.

Domain-Driven Design: Melampaui "Bounded Context" sebagai Buzzword

DDD sering direduksi jadi penamaan folder (Domain, Application, Infrastructure). Inti sebenarnya adalah memodelkan aturan bisnis eksplisit di dalam kode, bukan tersebar di controller atau service layer sebagai if-else.

Anemic Model vs Rich Domain Model

Anemic model — entity yang cuma berisi getter/setter — adalah anti-pattern DDD paling umum di codebase PHP. Business logic akhirnya bocor ke service layer, membuat entity kehilangan encapsulation atas invariant-nya sendiri.

// Anemic - logic bisnis bocor ke service
class Order {
    private array $items = [];
    private string $status = 'pending';

    public function getItems(): array { return $this->items; }
    public function setItems(array $items): void { $this->items = $items; }
    public function getStatus(): string { return $this->status; }
    public function setStatus(string $status): void { $this->status = $status; }
}

class OrderService {
    public function cancel(Order $order): void {
        if ($order->getStatus() === 'shipped') {
            throw new \DomainException('Cannot cancel shipped order');
        }
        $order->setStatus('cancelled'); // siapapun bisa bypass validasi ini
    }
}
// Rich domain model - invariant dijaga entity itu sendiri
final class Order {
    private OrderStatus $status;
    private array $items = [];

    public function cancel(): void {
        if ($this->status->equals(OrderStatus::SHIPPED)) {
            throw new \DomainException('Cannot cancel shipped order');
        }
        $this->status = OrderStatus::CANCELLED;
        $this->recordEvent(new OrderCancelled($this->id));
    }

    public function addItem(OrderItem $item): void {
        if ($this->status->equals(OrderStatus::CANCELLED)) {
            throw new \DomainException('Cannot modify cancelled order');
        }
        $this->items[] = $item;
    }
}

Dengan rich model, tidak ada cara untuk membuat Order berada di state tidak valid — validasi terjadi di dalam boundary entity, bukan opsional tergantung siapa yang memanggil.

Bounded Context: Batasan Model, Bukan Batasan Database

Kesalahan umum: menganggap bounded context sebagai skema database terpisah. Sebenarnya, bounded context adalah batasan bahasa dan makna. Kata "Customer" di context Sales punya atribut dan aturan berbeda dari "Customer" di context Support — meskipun keduanya merujuk ke entitas fisik yang sama. Mencoba membuat satu model Customer universal untuk semua context adalah sumber utama god-object dan coupling tersembunyi antar tim/modul.

Hexagonal Architecture: Ports & Adapters Bukan Sekadar Layering

Hexagonal Architecture (Ports and Adapters) sering disalahartikan sebagai "Controller-Service-Repository" yang di-rename. Perbedaan krusialnya: dalam Hexagonal, domain/application core tidak boleh tahu apa pun tentang teknologi di luar dirinya — termasuk framework.

Port sebagai Kontrak, Adapter sebagai Implementasi Teknis

// Port (didefinisikan di application core)
interface OrderRepositoryPort {
    public function findById(OrderId $id): ?Order;
    public function save(Order $order): void;
}

// Driving port - cara luar "masuk" ke aplikasi
interface CheckoutUseCase {
    public function execute(CheckoutCommand $command): CheckoutResult;
}

// Adapter untuk driven port (keluar ke infrastruktur)
final class DoctrineOrderRepository implements OrderRepositoryPort {
    public function __construct(private EntityManagerInterface $em) {}

    public function findById(OrderId $id): ?Order {
        return $this->em->find(Order::class, $id->toString());
    }

    public function save(Order $order): void {
        $this->em->persist($order);
        $this->em->flush();
    }
}

// Adapter untuk driving port - HTTP controller
final class CheckoutController {
    public function __construct(private CheckoutUseCase $useCase) {}

    public function __invoke(Request $request): Response {
        $command = new CheckoutCommand(
            $request->get('order_id'),
            $request->get('payment_method')
        );
        $result = $this->useCase->execute($command);
        return new JsonResponse($result->toArray());
    }
}

Poin krusial: CheckoutUseCase dan OrderRepositoryPort tidak tahu apakah dipanggil dari HTTP, CLI, atau message queue — dan tidak tahu apakah data disimpan di MySQL, MongoDB, atau in-memory. Ini membuat pergantian framework atau database menjadi masalah adapter, bukan rewrite total.

Kapan Hexagonal Justru Overengineering

Untuk CRUD sederhana tanpa business logic kompleks, memaksakan port-adapter penuh menambah indirection tanpa manfaat nyata — biaya kognitif naik, velocity turun. Hexagonal bernilai tinggi ketika: (1) business logic kompleks dan sering berubah, (2) ada kebutuhan mengganti infrastruktur (multi-database, multi-payment gateway), atau (3) testability tinggi adalah requirement keras (misal sistem finansial dengan regulasi audit).

Studi Kasus: Refactor Sistem Reservasi Hotel Multi-Channel

Konteks: Sistem reservasi hotel menerima booking dari tiga channel — website langsung, OTA (Booking.com/Agoda via API), dan call center (input manual staff). Kode awal berbentuk satu BookingController raksasa dengan logic validasi, pengecekan ketersediaan kamar, dan integrasi payment tercampur, ditambah duplikasi logic serupa di endpoint API OTA yang terpisah. Bug sering muncul: kamar bisa double-booked karena validasi ketersediaan berbeda antara flow website dan flow OTA.

Analisis Masalah

  • Duplikasi aturan bisnis: aturan "kamar tidak boleh double-booked" diimplementasikan dua kali dengan logic sedikit berbeda — inilah akar bug utama.
  • Anemic model: entity Booking hanya struct data, validasi tersebar di beberapa controller.
  • Coupling ke infrastruktur: logic bisnis langsung memanggil Eloquent model dan HTTP client OTA, membuat unit test nyaris mustahil tanpa hit database asli.

Solusi dan Alasan Pemilihannya

1. Konsolidasi aturan bisnis ke Domain Model (DDD). Dibuat entity Booking rich model dengan method reserve(RoomAvailability $availability) yang menjadi satu-satunya tempat validasi double-booking terjadi. Alasan: single source of truth untuk invariant kritikal — menghilangkan kemungkinan divergensi logic antar channel.

final class Booking {
    public static function reserve(
        RoomId $roomId,
        DateRange $period,
        RoomAvailabilityChecker $checker
    ): self {
        if (!$checker->isAvailable($roomId, $period)) {
            throw new RoomNotAvailableException($roomId, $period);
        }
        return new self($roomId, $period, BookingStatus::CONFIRMED);
    }
}

2. Satu Use Case, tiga Adapter (Hexagonal). Dibuat satu CreateBookingUseCase sebagai driving port, dipanggil oleh tiga adapter berbeda: WebBookingController, OtaWebhookAdapter, dan CallCenterCliCommand. Ketiganya menerjemahkan input masing-masing menjadi CreateBookingCommand yang sama. Alasan dipilih: menghilangkan duplikasi tanpa memaksa ketiga channel punya format input yang sama di layer luar — perbedaan format request tetap ditangani di adapter, tapi aturan bisnis inti hanya ada satu tempat.

3. RoomAvailabilityChecker sebagai Port dengan implementasi locking (DIP + SOLID). Untuk concurrency (dua booking masuk bersamaan dari channel berbeda), interface RoomAvailabilityChecker diimplementasikan dengan pessimistic locking di level database (SELECT ... FOR UPDATE) dalam adapter DoctrineRoomAvailabilityChecker. Alasan memilih pessimistic lock dibanding optimistic: booking hotel punya window kontensi pendek tapi biaya kegagalan tinggi (kehilangan reservasi > overhead lock singkat), sehingga trade-off throughput sedikit lebih rendah bisa diterima demi konsistensi data.

4. Testability sebagai hasil samping arsitektur, bukan tujuan tambahan. Karena CreateBookingUseCase hanya bergantung pada port (interface), unit test bisa memverifikasi skenario race condition dan aturan bisnis tanpa menyentuh database sungguhan — cukup mock RoomAvailabilityChecker mengembalikan false pada percobaan kedua untuk mensimulasikan konflik.

Hasil: Bug double-booking hilang karena aturan bisnis tidak lagi bisa "dilewati" dari channel manapun — konsekuensi arsitektural, bukan sekadar tambal patch di satu endpoint. Ini ilustrasi konkret kenapa SOLID, DDD, dan Hexagonal bukan tiga hal terpisah: DIP menyediakan mekanisme, DDD menyediakan tempat aturan bisnis hidup, dan Hexagonal menyediakan struktur agar keduanya tidak tercemar detail infrastruktur.

Lanjut ke modul berikutnya
2

Design Pattern Struktural Lanjut: Adapter, Decorator, Proxy, dan Composite dalam Praktik Produksi

14 menit baca

Pada modul sebelumnya kita sudah membahas bagaimana SOLID dan hexagonal architecture memaksa kita memisahkan domain dari detail infrastruktur. Modul ini masuk ke level implementasi: empat pola struktural yang paling sering muncul di kode produksi—Adapter, Decorator, Proxy, dan Composite—beserta jebakan desain yang sering terjadi kalau dipakai tanpa pertimbangan trade-off.

Keempatnya sering disalahpahami sebagai "cara membungkus objek lain", padahal masing-masing punya niat (intent) yang berbeda. Kesalahan paling umum di kode production adalah memakai Decorator padahal yang dibutuhkan Proxy, atau membuat Adapter yang justru menambah logic bisnis — padahal tugasnya murni translasi interface.

1. Adapter: Translasi Interface, Bukan Tempat Logic

Adapter dipakai saat port domain (sesuai hexagonal architecture) tidak cocok secara langsung dengan SDK atau API eksternal. Prinsip ketat: Adapter tidak boleh mengandung business rule, hanya transformasi data dan pemetaan exception.

interface PaymentGateway {
    public function charge(Money $amount, string $token): PaymentResult;
}

final class StripeAdapter implements PaymentGateway
{
    public function __construct(private StripeClient $client) {}

    public function charge(Money $amount, string $token): PaymentResult
    {
        try {
            $response = $this->client->charges->create([
                'amount'   => $amount->toCents(),
                'currency' => $amount->currency(),
                'source'   => $token,
            ]);
        } catch (StripeApiException $e) {
            // translasi exception vendor ke domain exception
            throw PaymentFailedException::fromVendor($e);
        }

        return PaymentResult::fromStripeResponse($response);
    }
}

Jebakan produksi: tim sering menaruh validasi bisnis (misal cek limit transaksi harian) di dalam Adapter karena "sudah ada di situ". Ini melanggar single responsibility — Adapter jadi tahu aturan domain, sehingga ganti vendor pembayaran berarti harus menulis ulang validasi di setiap adapter baru. Validasi semacam itu seharusnya ada di use case / domain service, bukan di adapter.

Adapter vs Facade

Keduanya sering tertukar. Facade menyederhanakan subsistem kompleks dengan interface baru yang lebih mudah dipakai, tapi tidak wajib menyesuaikan ke interface target tertentu. Adapter punya kontrak spesifik yang harus dipenuhi (biasanya port yang sudah didefinisikan domain). Kalau Anda membungkus beberapa service jadi satu pintu masuk tanpa kontrak formal, itu Facade, bukan Adapter — penamaan yang salah akan membingungkan tim saat code review.

2. Decorator: Menambah Tanggung Jawab Secara Transparan

Decorator dipakai ketika Anda ingin menambah behavior (logging, caching, retry, rate limiting) tanpa mengubah kelas asli dan tanpa membuat eksplosi subclass untuk tiap kombinasi fitur. Kuncinya: Decorator mengimplementasikan interface yang sama dengan objek yang dibungkus, dan bisa ditumpuk (stacked).

interface ProductRepository {
    public function find(int $id): ?Product;
}

final class CachingProductRepository implements ProductRepository
{
    public function __construct(
        private ProductRepository $inner,
        private CacheInterface $cache,
    ) {}

    public function find(int $id): ?Product
    {
        $key = "product:{$id}";
        if ($cached = $this->cache->get($key)) {
            return $cached;
        }

        $product = $this->inner->find($id);
        if ($product !== null) {
            $this->cache->set($key, $product, ttl: 300);
        }

        return $product;
    }
}

final class LoggingProductRepository implements ProductRepository
{
    public function __construct(
        private ProductRepository $inner,
        private LoggerInterface $logger,
    ) {}

    public function find(int $id): ?Product
    {
        $this->logger->debug("Fetching product {$id}");
        return $this->inner->find($id);
    }
}

Komposisi di container DI (akan dibahas lebih dalam di modul 4):

$repo = new LoggingProductRepository(
    new CachingProductRepository(
        new SqlProductRepository($pdo),
        $cache
    ),
    $logger
);

Urutan stacking penting dan sering jadi bug tersembunyi: kalau LoggingProductRepository dipasang di dalam CachingProductRepository, log hanya tercatat saat cache miss — padahal mungkin Anda butuh log setiap request masuk. Diskusikan urutan ini secara eksplisit di code review, jangan asumsikan "pasti benar" karena compiler tidak akan menangkap kesalahan urutan ini.

Decorator vs Middleware

Middleware (PSR-15) sebenarnya penerapan Decorator pattern pada level request/response HTTP. Memahami Decorator secara eksplisit membantu Anda menulis middleware custom dengan lebih percaya diri, termasuk menentukan kapan middleware harus short-circuit (tidak memanggil $next) — analog dengan Decorator yang memilih tidak memanggil inner.

3. Proxy: Kontrol Akses, Bukan Penambahan Fitur

Proxy terlihat mirip Decorator secara struktur kode, tapi niatnya berbeda: Proxy mengontrol akses ke objek asli — lazy loading, access control, atau representasi objek remote — tanpa mengubah perilaku fungsional objek tersebut.

interface ReportGenerator {
    public function generate(int $reportId): Report;
}

final class LazyReportGeneratorProxy implements ReportGenerator
{
    private ?HeavyReportGenerator $realGenerator = null;

    public function __construct(private ReportGeneratorFactory $factory) {}

    public function generate(int $reportId): Report
    {
        // objek berat hanya dibuat saat benar-benar dibutuhkan
        if ($this->realGenerator === null) {
            $this->realGenerator = $this->factory->createHeavyGenerator();
        }

        return $this->realGenerator->generate($reportId);
    }
}

Contoh lain: Proxy untuk access control berbasis role.

final class AuthorizedReportGenerator implements ReportGenerator
{
    public function __construct(
        private ReportGenerator $inner,
        private AuthContext $auth,
    ) {}

    public function generate(int $reportId): Report
    {
        if (!$this->auth->can('generate_report')) {
            throw new AccessDeniedException();
        }
        return $this->inner->generate($reportId);
    }
}

Pembeda praktis Decorator vs Proxy: Decorator menambah behavior baru yang memang bagian dari fitur (caching adalah fitur), Proxy mengontrol apakah dan bagaimana request sampai ke objek asli tanpa menambah fitur baru pada hasil akhirnya. Secara teknis kode bisa identik, tapi niat desain ini menentukan bagaimana Anda menamai kelas dan menjelaskannya ke tim — penamaan yang tepat mengurangi miskomunikasi saat onboarding developer baru.

4. Composite: Hierarki Objek yang Diperlakukan Seragam

Composite dipakai ketika domain Anda punya struktur pohon (tree) dan klien harus bisa memperlakukan node tunggal maupun grup node dengan interface yang sama. Contoh klasik produksi: sistem diskon bertingkat, struktur organisasi, atau validasi komposit.

interface DiscountRule {
    public function apply(Money $price): Money;
}

final class PercentageDiscount implements DiscountRule
{
    public function __construct(private float $percentage) {}

    public function apply(Money $price): Money
    {
        return $price->subtractPercentage($this->percentage);
    }
}

final class CompositeDiscount implements DiscountRule
{
    /** @var DiscountRule[] */
    private array $rules = [];

    public function add(DiscountRule $rule): void
    {
        $this->rules[] = $rule;
    }

    public function apply(Money $price): Money
    {
        foreach ($this->rules as $rule) {
            $price = $rule->apply($price);
        }
        return $price;
    }
}

Penggunaan:

$discount = new CompositeDiscount();
$discount->add(new PercentageDiscount(10));
$discount->add(new FlatAmountDiscount(Money::fromRupiah(5000)));

$finalPrice = $discount->apply($originalPrice);

Trade-off yang sering diabaikan: urutan penerapan diskon memengaruhi hasil akhir (diskon persentase lalu potongan flat beda hasilnya dengan urutan sebaliknya). Composite pattern tidak menyelesaikan masalah ini secara otomatis — Anda tetap butuh aturan bisnis eksplisit soal urutan rule, dan sebaiknya urutan ini dites dengan unit test berbasis data provider (dibahas detail di modul 7), bukan diasumsikan dari urutan `add()`.

Kombinasi Pattern dalam Satu Alur

Di produksi, keempat pattern ini jarang berdiri sendiri. Satu request bisa melewati: Proxy (cek otorisasi) → Decorator (logging) → Decorator (caching) → Adapter (ke API eksternal), dengan hasil akhirnya diproses oleh Composite (gabungan beberapa aturan bisnis). Memahami niat masing-masing pattern membuat Anda bisa menyusun layer ini tanpa membuat satu kelas "dewa" yang melakukan semuanya sekaligus — yang justru melanggar prinsip yang sudah dibahas di modul SOLID.

Studi Kasus: Sistem Integrasi Multi-Payment Gateway dengan Fallback dan Observabilitas

Konteks: Sebuah platform e-commerce skala menengah-besar memproses ribuan transaksi per menit melalui tiga payment gateway berbeda (Midtrans, Xendit, dan provider bank lokal) karena alasan redundansi dan biaya transaksi. Masalah yang dihadapi tim engineering:

  • Kode awal menaruh logic retry, logging, dan validasi fraud langsung di dalam masing-masing class gateway — menyebabkan duplikasi di tiga tempat dan sulit diaudit saat terjadi insiden.
  • Saat gateway utama down, sistem harus otomatis fallback ke gateway kedua tanpa mengubah kode di use case pemrosesan order.
  • Tim compliance butuh audit trail lengkap (siapa yang akses, kapan, request/response apa) tanpa mengotori logic bisnis pembayaran.

Solusi yang dipilih:

interface PaymentGateway {
    public function charge(Money $amount, string $token): PaymentResult;
}

// 1. Adapter murni per vendor — hanya translasi
final class MidtransAdapter implements PaymentGateway { /* ... */ }
final class XenditAdapter implements PaymentGateway { /* ... */ }

// 2. Decorator untuk audit logging — ditempel ke semua gateway seragam
final class AuditLoggingGateway implements PaymentGateway
{
    public function __construct(
        private PaymentGateway $inner,
        private AuditLogger $audit,
    ) {}

    public function charge(Money $amount, string $token): PaymentResult
    {
        $this->audit->logRequest($amount, $token);
        try {
            $result = $this->inner->charge($amount, $token);
            $this->audit->logResponse($result);
            return $result;
        } catch (PaymentFailedException $e) {
            $this->audit->logFailure($e);
            throw $e;
        }
    }
}

// 3. Proxy untuk fallback otomatis — mengontrol akses ke gateway berikutnya
final class FallbackPaymentGateway implements PaymentGateway
{
    /** @var PaymentGateway[] */
    private array $gateways;

    public function __construct(PaymentGateway ...$gateways)
    {
        $this->gateways = $gateways;
    }

    public function charge(Money $amount, string $token): PaymentResult
    {
        $lastException = null;
        foreach ($this->gateways as $gateway) {
            try {
                return $gateway->charge($amount, $token);
            } catch (PaymentFailedException $e) {
                $lastException = $e;
                continue; // coba gateway berikutnya
            }
        }
        throw $lastException ?? new PaymentFailedException('Semua gateway gagal');
    }
}

// Komposisi akhir
$gateway = new FallbackPaymentGateway(
    new AuditLoggingGateway(new MidtransAdapter($midtransClient), $audit),
    new AuditLoggingGateway(new XenditAdapter($xenditClient), $audit),
);

Kenapa kombinasi ini, bukan yang lain:

  • Adapter dipilih murni untuk isolasi SDK vendor — tanpa ini, ganti SDK Midtrans versi baru akan memaksa perubahan di banyak tempat termasuk logic fallback dan audit.
  • Audit logging dibuat Decorator, bukan ditulis manual di tiap adapter, karena requirement compliance berubah-ubah (misalnya suatu saat harus menambah hashing data sensitif di log). Dengan Decorator, perubahan ini satu tempat, otomatis berlaku untuk semua gateway tanpa menyentuh Adapter.
  • Fallback dipilih sebagai Proxy, bukan Decorator, karena perannya murni mengontrol ke mana request diarahkan — tidak menambah fitur baru pada hasil transaksi. Ini juga alasan kenapa penamaan class FallbackPaymentGateway tetap valid secara konsep meski secara struktur kode mirip Decorator: niatnya kontrol alur, bukan penambahan behavior.
  • Alternatif yang ditolak: tim sempat mempertimbangkan membuat satu PaymentManager besar yang menangani retry, logging, dan pemilihan gateway sekaligus. Ditolak karena class tersebut akan punya terlalu banyak alasan untuk berubah (melanggar SRP), sulit ditest secara terisolasi (setiap unit test harus mock tiga gateway sekaligus), dan tidak bisa dikonfigurasi ulang urutan gateway tanpa redeploy kode inti.

Dampak terukur: waktu investigasi insiden turun signifikan karena audit trail konsisten di semua gateway dari satu titik implementasi. Penambahan gateway keempat (provider bank baru) hanya butuh satu Adapter baru plus satu baris komposisi, tanpa menyentuh logic fallback atau audit yang sudah stabil dan sudah diuji — keuntungan langsung dari pemisahan niat pattern yang jelas sejak awal desain.

Ini modul terakhir yang sudah tersedia — modul berikutnya segera menyusul.
3

Modul berikutnya

Segera hadir — 10 modul lagi menyusul
Progresmu
0 dari 2 modul selesai
  • Arsitektur Lanjut PHP: SOLID, Domain-Driven Design, dan Hexagonal Architecture di Balik Layar
  • Design Pattern Struktural Lanjut: Adapter, Decorator, Proxy, dan Composite dalam Praktik Produksi
  • Modul berikutnya (segera)
Tentang Kelas Ini

Modul baru ditambahkan secara berkala hingga kelas ini lengkap. Simpan halaman ini dan kembali lagi untuk lanjut belajar.

Copyright © 2026 — Made with by Ruang Coding