Pendahuluan
Halo rekan developer! Senang sekali bisa bertemu kembali di seri design patterns kita. Pada artikel sebelumnya, kita sudah membahas bagaimana cara mengamankan satu instance tunggal menggunakan Singleton Pattern. Kita belajar bahwa dalam sebuah sistem yang kompleks, seperti arsitektur plugin, menjaga konsistensi satu instance sangatlah krusial.
Namun, ada satu masalah besar yang sering kita hadapi setelah kita berhasil mengelola instance: Bagaimana cara membuat objek baru tanpa harus membuat kode kita menjadi kaku (tightly coupled)?
Bayangkan Anda sedang membangun sistem pembayaran untuk aplikasi e-commerce seperti Tokopedia. Awalnya, Anda hanya mendukung pembayaran via BCA. Lalu, tiba-tiba bisnis meminta penambahan GoPay, OVO, dan ShopeePay. Jika Anda menggunakan cara konvensional dengan banyak if-else atau switch-case yang tersebar di seluruh kode, aplikasi Anda akan menjadi “rapuh”. Setiap kali ada metode pembayaran baru, Anda harus membongkar banyak file.
Di sinilah Factory Method Pattern hadir sebagai pahlawan. Dalam artikel ini, kita akan mempelajari bagaimana pola ini memungkinkan kita untuk mendefinisikan sebuah interface untuk membuat objek, tetapi membiarkan subclass memutuskan kelas mana yang akan diinstansiasi. Kita akan mengaplikasikan konsep ini ke dalam proyek akhir kita: Plugin System, agar sistem kita benar-benar extensible dan siap menerima fitur baru kapan saja tanpa merusak kode yang sudah ada.
Konsep Utama 1: Inti dari Factory Method
Secara fundamental, Factory Method adalah sebuah creational pattern yang memindahkan tanggung jawab instansiasi objek dari pemanggil (client) ke sebuah metode khusus.
Dalam pemrograman prosedural, kita sering melakukan ini:
# Cara konvensional (Bad Practice untuk sistem yang dinamis)
def proses_pembayaran(tipe):
if tipe == "BCA":
payment = BCAPayment()
elif tipe == "GOPAY":
payment = GoPayPayment()
# Setiap tambah tipe baru, fungsi ini harus diubah!
payment.bayar(15000)
Masalahnya, fungsi proses_pembayaran di atas “mengetahui” terlalu banyak hal tentang kelas-kelas spesifik (BCAPayment, GoPayPayment). Jika kita menambah OVO, kita harus mengubah fungsi ini. Ini melanggar prinsip Open/Closed Principle (Software entities should be open for extension, but closed for modification).
Dengan Factory Method, kita tidak lagi memanggil BCAPayment() secara langsung. Kita memanggil sebuah “pabrik” (factory) yang akan memberikan kita objek yang sesuai dengan permintaan, tanpa kita perlu tahu detail kelas di balksngnya.
Konsep Utama 2: Komponen Utama Pattern
Untuk mengimplementasikan Factory Method dengan benar, kita perlu memahami empat komponen utama yang terlibat:
- Product (Interface/Abstract Class): Menentukan interface yang harus dimiliki oleh semua objek yang akan dibuat oleh factory.
- Concrete Product: Implementasi nyata dari Product (misalnya: kelas
BCA, kelasGoPay). - Creator (Abstract Class): Kelas yang mendeklarasikan metode factory. Metode ini biasanya mengembalikan objek bertipe
Product. - Concrete Creator: Kelas yang mengimplementasikan metode factory untuk menghasilkan instance
Concrete Producttertentu.
Mari kita lihat contoh dalam konteks sistem notifikasi sederhana di Indonesia:
from abc import ABC, abstractmethod
# 1. Product Interface
class Notifikasi(ABC):
@abstractmethod
def kirim(self, pesan: str):
pass
# 2. Concrete Products
class NotifikasiWhatsApp(Notifikasi):
def kirim(self, pesan: str):
print(f"Mengirim WhatsApp: {pesan}")
class NotifikasiSMS(Notifikasi):
def kirim(self, pesan: str):
print(f"Mengirim SMS: {pesan}")
# 3. Creator (Abstract Class)
class NotifikasiService(ABC):
@abstractmethod
def buat_notifikasi(self) -> Notifikasi:
"""Ini adalah Factory Method"""
pass
def kirim_pesan_ke_user(self, pesan: str):
# Logika bisnis menggunakan objek yang dibuat oleh factory
notif = self.buat_notifikasi()
notif.kirim(pesan)
# 4. Concrete Creators
class WhatsAppService(NotifikasiService):
def buat_notifikasi(self) -> Notifikasi:
return NotifikasiWhatsApp()
class SMSService(NotifikasiService):
def buat_notifikasi(self) -> Notifikasi:
return NotifikasiSMS()
# Penggunaan (Client Code)
def jalankan_aplikasi(service: NotifikasiService):
service.kirim_pesan_ke_user("Halo, paket Anda sudah sampai!")
# Kita bisa mengganti service tanpa mengubah fungsi jalankan_aplikasi
service_wa = WhatsAppService()
jalankan_aplikasi(service_wa)
service_sms = SMSService()
jalankan_aplikasi(service_sms)
Konsep Utama 3: Keuntungan dan Kapan Harus Digunakan
Sebagai developer senior, saya selalu menekankan bahwa design pattern bukan untuk digunakan di setiap situasi. Jangan sampai kita melakukan over-engineering.
Kapan menggunakan Factory Method?
- Ketika sebuah kelas tidak bisa mengantisipasi kelas objek yang harus dibuat.
- Ketika Anda ingin menghemat sumber daya sistem dengan membagi tugas pembuatan objek ke subclass. ical
- Ketika Anda ingin menyediakan library atau framework yang memungkinkan user untuk memperluas komponen internalnya (seperti sistem plugin).
Keuntungan:
- Decoupling: Kode client tidak bergantung pada kelas konkret.
- Single Responsibility Principle: Logika pembuatan objek dipisahkan dari logika bisnis utama.
- Open/Closed Principle: Anda bisa menambah jenis produk baru tanpa mengubah kode yang sudah ada.
Implementasi dalam Proyek: Plugin System
Sekarang, mari kita terapkan ke proyek utama kita: Plugin System. Kita ingin membuat sebuah sistem yang bisa menjalankan “Plugin Diskon” dan “Plugin Ongkir” secara dinamis.
Dalam skenario ini, kita akan membuat sebuah PluginFactory yang akan menentukan plugin mana yang akan dimuat berdasarkan konfigurasi.
from abc import ABC, abstractmethod
# Interface untuk semua Plugin
class Plugin(ABC):
@abstractmethod
def execute(self, data: dict) -> dict:
pass
# Concrete Plugin 1: Plugin Diskon (Contoh: Diskon Tanggal Kembar)
class DiskonTanggalKembarPlugin(Plugin):
def execute(self, data: dict) -> dict:
total_awal = data['total_belanja']
# Diskon 10% jika belanja di tanggal kembar (misal 10/10)
diskon = total_awal * 0.1
print(f"Applying Tanggal Kembar Discount: -Rp{diskon:,.0f}")
data['total_setelah_diskon'] = total_awal - diskon
return data
# Concrete Plugin 2: Plugin Ongkir (Contoh: Ongkir Flat Jakarta)
class OngkirJakartaPlugin(Plugin):
def execute(self, data: dict) -> dict:
biaya_ongkir = 15000
print(f"Adding Jakarta shipping fee: Rp{biaya_ongkir}")
data['total_akhir'] = data['total_akhir'] + biaya_ongkir
return data
# The Factory (The Core logic)
class PluginFactory:
def __init__(self):
self._plugins = {
"diskon_kembar": DiskonTanggalKembarPlugin(),
"ongkir_jakarta": OngkirJakartaPlugin()
}
def get_plugin(self, plugin_name: str) -> Plugin:
plugin = self._plugins.get(plugin_name)
if not plugin:
raise ValueError(f"Plugin '{plugin_name}' tidak ditemukan!")
return plugin
# Client Code (Simulasi Transaksi)
def proses_checkout(nama_plugin: str, data_belanja: dict):
factory = PluginFactory()
print(f"--- Memulai Checkout ---")
print(f"Total Awal: Rp{data_belanja['total_awal']}")
try:
plugin = factory.get_plugin(nama_plugin)
# Kita asumsikan data_belanja punya field total_akhir untuk plugin ongkir
if "total_akhir" not in data_belanja:
data_belanja["total_akhir"] = data_belanja["total_awal"]
data_hasil = plugin.execute(data_belanja)
print(f"Hasil Akhir: Rp{data_hasil.get('total_akhir', data_belanja['total_awal'])}")
except ValueError as e:
print(f"Error: {e}")
print(f"--- Selesai ---\n")
# Simulasi Penggunaan
keranjang_belanja = {"total_awal": 100000, "total_akhir": 100000}
# 1. Pakai plugin diskon
proses_checkout("diskon_kembar", keranjang_belanja.copy())
# 2. Pakai plugin ongkir
proses_checkout("ongkir_jakarta", keranjang_belanja.copy())
# 3. Pakai plugin yang tidak ada
proses_checkout("plugin_gaib", keranjang_belanja.copy())
Pada contoh di atas, jika besok perusahaan ingin menambah “Plugin Cashback”, kita cukup membuat kelas baru dan mendaftarkannya di PluginFactory tanpa perlu mengubah logika proses_checkout. Inilah kekuatan dari decoupling.
Kesimpulan
Factory Pattern telah membantu kita mengubah kode yang tadinya “kaku” menjadi sangat fleksibel. Kita tidak lagi melakukan if-else yang panjang di dalam logika bisnis utama, melainkan memindahkan tanggung jawab pembuatan objek ke sebuah entitas khusus (Factory).
Dalam pengembangan perangkat lunak skala besar, pola ini adalah kunci untuk menjaga agar kode tetap maintainable saat fitur terus bertambah.
Tips Pro: Jangan gunakan Factory jika aplikasi Anda sangat sederhana. Terlalu banyak abstraksi pada masalah yang sederhana hanya akan membuat kode Anda sulit dibaca (over-engineering). Gunakan saat Anda sudah mulai melihat pola repetisi dalam pembuatan objek.