Jelajahi pola injeksi dependensi tingkat lanjut di FastAPI untuk membangun aplikasi yang skalabel, dapat dipelihara, dan teruji. Pelajari cara menyusun kontainer DI yang kuat.
Injeksi Dependensi FastAPI: Arsitektur Kontainer DI Tingkat Lanjut
FastAPI, dengan desain intuitif dan fitur-fitur canggihnya, telah menjadi favorit untuk membangun API web modern dengan Python. Salah satu kekuatan utamanya terletak pada integrasi yang mulus dengan injeksi dependensi (DI), yang memungkinkan pengembang untuk membuat aplikasi yang loosely coupled, dapat diuji, dan mudah dipelihara. Meskipun sistem DI bawaan FastAPI sangat baik untuk kasus penggunaan sederhana, proyek yang lebih kompleks sering kali mendapat manfaat dari arsitektur kontainer DI yang lebih terstruktur dan canggih. Artikel ini akan mengeksplorasi berbagai strategi untuk membangun arsitektur semacam itu, memberikan contoh praktis dan wawasan untuk merancang aplikasi yang kuat dan skalabel.
Memahami Injeksi Dependensi (DI) dan Inversi Kontrol (IoC)
Sebelum menyelami arsitektur kontainer DI tingkat lanjut, mari kita perjelas konsep-konsep dasarnya:
- Injeksi Dependensi (DI): Sebuah pola desain di mana dependensi disediakan untuk sebuah komponen dari sumber eksternal alih-alih dibuat secara internal. Ini mendorong loose coupling, membuat komponen lebih mudah diuji dan digunakan kembali.
- Inversi Kontrol (IoC): Prinsip yang lebih luas di mana kontrol pembuatan dan manajemen objek dibalik – didelegasikan ke sebuah kerangka kerja atau kontainer. DI adalah jenis spesifik dari IoC.
FastAPI secara inheren mendukung DI melalui sistem dependensinya. Anda mendefinisikan dependensi sebagai objek yang dapat dipanggil (fungsi, kelas, dll.), dan FastAPI secara otomatis menyelesaikan dan menyuntikkannya ke dalam fungsi endpoint Anda atau dependensi lainnya.
Contoh (DI Dasar FastAPI):
from fastapi import FastAPI, Depends
app = FastAPI()
# Dependensi
def get_db():
db = {"items": []} # Mensimulasikan koneksi basis data
try:
yield db
finally:
# Tutup koneksi basis data (jika perlu)
pass
# Endpoint dengan injeksi dependensi
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
Dalam contoh ini, get_db adalah sebuah dependensi yang menyediakan koneksi basis data. FastAPI secara otomatis memanggil get_db dan menyuntikkan hasilnya (kamus db) ke dalam fungsi endpoint read_items.
Mengapa Perlu Kontainer DI Tingkat Lanjut?
DI bawaan FastAPI bekerja dengan baik untuk proyek-proyek sederhana, tetapi seiring dengan meningkatnya kompleksitas aplikasi, kontainer DI yang lebih canggih menawarkan beberapa keuntungan:
- Manajemen Dependensi Terpusat: Kontainer khusus menyediakan satu sumber kebenaran untuk semua dependensi, membuatnya lebih mudah untuk mengelola dan memahami dependensi aplikasi.
- Manajemen Konfigurasi dan Siklus Hidup: Kontainer dapat menangani konfigurasi dan siklus hidup dependensi, seperti membuat singleton, mengelola koneksi, dan membuang sumber daya.
- Keterujian (Testability): Kontainer tingkat lanjut menyederhanakan pengujian dengan memungkinkan Anda untuk dengan mudah mengganti dependensi dengan objek tiruan atau test doubles.
- Pemisahan (Decoupling): Mendorong pemisahan yang lebih besar antar komponen, mengurangi ketergantungan dan meningkatkan keterpeliharaan kode.
- Ekstensibilitas: Kontainer yang dapat diperluas memungkinkan Anda untuk menambahkan fitur dan integrasi kustom sesuai kebutuhan.
Strategi untuk Membangun Kontainer DI Tingkat Lanjut
Ada beberapa pendekatan untuk membangun kontainer DI tingkat lanjut di FastAPI. Berikut adalah beberapa strategi umum:
1. Menggunakan Pustaka DI Khusus (misalnya, `injector`, `dependency_injector`)
Beberapa pustaka DI yang kuat tersedia untuk Python, seperti injector dan dependency_injector. Pustaka-pustaka ini menyediakan serangkaian fitur komprehensif untuk mengelola dependensi, termasuk:
- Binding: Mendefinisikan bagaimana dependensi diselesaikan dan disuntikkan.
- Cakupan (Scopes): Mengontrol siklus hidup dependensi (misalnya, singleton, transient).
- Konfigurasi: Mengelola pengaturan konfigurasi untuk dependensi.
- AOP (Aspect-Oriented Programming): Mencegat panggilan metode untuk kepentingan lintas-sektoral (cross-cutting concerns).
Contoh dengan `dependency_injector`
dependency_injector adalah pilihan populer untuk membangun kontainer DI. Mari kita ilustrasikan penggunaannya dengan sebuah contoh:
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
# Definisikan dependensi
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
# Inisialisasi koneksi basis data
print(f"Menghubungkan ke basis data: {self.connection_string}")
def get_items(self):
# Mensimulasikan pengambilan item dari basis data
return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
class UserRepository:
def __init__(self, database: Database):
self.database = database
def get_all_users(self):
# Mensimulasikan permintaan basis data untuk mendapatkan semua pengguna
return [{"id": "user1", "name": "Alice"},{"id": "user2", "name": "Bob"}]
class Settings:
def __init__(self, database_url):
self.database_url = database_url
# Definisikan kontainer
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
settings = providers.Singleton(Settings, database_url = config.database_url)
database = providers.Singleton(Database, connection_string=config.database_url)
user_repository = providers.Factory(UserRepository, database=database)
# Buat aplikasi FastAPI
app = FastAPI()
# Konfigurasikan kontainer (dari variabel lingkungan)
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__]) # memungkinkan injeksi dependensi ke dalam endpoint FastAPI
# Dependensi untuk FastAPI
def get_user_repository(user_repository: UserRepository = Depends(container.user_repository.provided)) -> UserRepository:
return user_repository
# Endpoint menggunakan dependensi yang diinjeksikan
@app.get("/users/")
async def read_users(user_repository: UserRepository = Depends(get_user_repository)):
return user_repository.get_all_users()
@app.on_event("startup")
async def startup_event():
# Inisialisasi kontainer
container.init_resources()
Penjelasan:
- Kita mendefinisikan dependensi kita (
Database,UserRepository,Settings) sebagai kelas Python biasa. - Kita membuat kelas
Containeryang mewarisi daricontainers.DeclarativeContainer. Kelas ini mendefinisikan dependensi dan penyedianya (misalnya,providers.Singletonuntuk singleton,providers.Factoryuntuk membuat instance baru setiap kali). - Baris
container.wire([__name__])memungkinkan injeksi dependensi ke dalam endpoint FastAPI. - Fungsi
get_user_repositoryadalah dependensi FastAPI yang menggunakancontainer.user_repository.provideduntuk mengambil instance UserRepository dari kontainer. - Fungsi endpoint
read_usersmenyuntikkan dependensiUserRepository. configmemungkinkan Anda untuk mengeksternalisasi konfigurasi dependensi. Konfigurasi ini kemudian dapat berasal dari variabel lingkungan, file konfigurasi, dll.startup_eventdigunakan untuk menginisialisasi sumber daya yang dikelola di dalam kontainer
2. Mengimplementasikan Kontainer DI Kustom
Untuk kontrol lebih besar atas proses DI, Anda dapat mengimplementasikan kontainer DI kustom. Pendekatan ini membutuhkan lebih banyak usaha tetapi memungkinkan Anda untuk menyesuaikan kontainer dengan kebutuhan spesifik Anda.
Contoh Kontainer DI Kustom Dasar:
from typing import Callable, Dict, Type, Any
from fastapi import FastAPI, Depends
class Container:
def __init__(self):
self.dependencies: Dict[Type[Any], Callable[..., Any]] = {}
self.instances: Dict[Type[Any], Any] = {}
def register(self, dependency_type: Type[Any], provider: Callable[..., Any]):
self.dependencies[dependency_type] = provider
def resolve(self, dependency_type: Type[Any]) -> Any:
if dependency_type in self.instances:
return self.instances[dependency_type]
if dependency_type not in self.dependencies:
raise Exception(f"Dependensi {dependency_type} tidak terdaftar.")
provider = self.dependencies[dependency_type]
instance = provider()
return instance
def singleton(self, dependency_type: Type[Any], provider: Callable[..., Any]):
self.register(dependency_type, provider)
self.instances[dependency_type] = provider()
# Contoh Dependensi
class PaymentGateway:
def process_payment(self, amount: float) -> bool:
print(f"Memproses pembayaran sebesar ${amount}")
return True # Mensimulasikan pembayaran berhasil
class NotificationService:
def send_notification(self, message: str):
print(f"Mengirim notifikasi: {message}")
# Contoh Penggunaan
container = Container()
container.singleton(PaymentGateway, PaymentGateway)
container.singleton(NotificationService, NotificationService)
app = FastAPI()
# Dependensi FastAPI
def get_payment_gateway(payment_gateway: PaymentGateway = Depends(lambda: container.resolve(PaymentGateway))):
return payment_gateway
def get_notification_service(notification_service: NotificationService = Depends(lambda: container.resolve(NotificationService))):
return notification_service
@app.post("/purchase/")
async def purchase_item(payment_gateway: PaymentGateway = Depends(get_payment_gateway), notification_service: NotificationService = Depends(get_notification_service)):
if payment_gateway.process_payment(100.0):
notification_service.send_notification("Pembelian berhasil!")
return {"message": "Pembelian berhasil"}
else:
return {"message": "Pembelian gagal"}
Penjelasan:
- Kelas
Containermengelola kamus dependensi dan penyedianya. - Metode
registermendaftarkan dependensi dengan penyedianya. - Metode
resolvemenyelesaikan dependensi dengan memanggil penyedianya. - Metode
singletonmendaftarkan dependensi dan membuat satu instance darinya. - Dependensi FastAPI dibuat menggunakan fungsi lambda untuk menyelesaikan dependensi dari kontainer.
3. Menggunakan `Depends` FastAPI dengan Fungsi Pabrik (Factory Function)
Alih-alih kontainer DI yang lengkap, Anda dapat menggunakan Depends dari FastAPI bersama dengan fungsi pabrik untuk mencapai tingkat manajemen dependensi tertentu. Pendekatan ini lebih sederhana daripada mengimplementasikan kontainer kustom tetapi masih memberikan beberapa manfaat dibandingkan dengan membuat instance dependensi secara langsung di dalam fungsi endpoint.
from fastapi import FastAPI, Depends
from typing import Callable
# Definisikan Dependensi
class EmailService:
def __init__(self, smtp_server: str):
self.smtp_server = smtp_server
def send_email(self, recipient: str, subject: str, body: str):
print(f"Mengirim email ke {recipient} via {self.smtp_server}: {subject} - {body}")
# Fungsi pabrik untuk EmailService
def create_email_service(smtp_server: str) -> EmailService:
return EmailService(smtp_server=smtp_server)
# FastAPI
app = FastAPI()
# Dependensi FastAPI, memanfaatkan fungsi pabrik dan Depends
def get_email_service(email_service: EmailService = Depends(lambda: create_email_service(smtp_server="smtp.example.com"))):
return email_service
@app.post("/send-email/")
async def send_email(recipient: str, subject: str, body: str, email_service: EmailService = Depends(get_email_service)):
email_service.send_email(recipient=recipient, subject=subject, body=body)
return {"message": "Email terkirim!"}
Penjelasan:
- Kita mendefinisikan fungsi pabrik (
create_email_service) yang membuat instance dari dependensiEmailService. - Dependensi
get_email_servicemenggunakanDependsdan lambda untuk memanggil fungsi pabrik dan menyediakan instance dariEmailService. - Fungsi endpoint
send_emailmenyuntikkan dependensiEmailService.
Pertimbangan Tingkat Lanjut
1. Cakupan (Scopes) dan Siklus Hidup
Kontainer DI sering kali menyediakan fitur untuk mengelola siklus hidup dependensi. Cakupan umum meliputi:
- Singleton: Satu instance dari dependensi dibuat dan digunakan kembali sepanjang siklus hidup aplikasi. Ini cocok untuk dependensi yang tidak memiliki state (stateless) atau memiliki cakupan global.
- Transient: Instance baru dari dependensi dibuat setiap kali diminta. Ini cocok untuk dependensi yang memiliki state (stateful) atau perlu diisolasi satu sama lain.
- Request: Satu instance dari dependensi dibuat untuk setiap permintaan yang masuk. Ini cocok untuk dependensi yang perlu mempertahankan state dalam konteks satu permintaan.
Pustaka dependency_injector menyediakan dukungan bawaan untuk cakupan. Untuk kontainer kustom, Anda perlu mengimplementasikan logika manajemen cakupan sendiri.
2. Konfigurasi
Dependensi seringkali memerlukan pengaturan konfigurasi, seperti string koneksi basis data, kunci API, dan feature flags. Kontainer DI dapat membantu mengelola pengaturan ini dengan menyediakan cara terpusat untuk mengakses dan menyuntikkan nilai konfigurasi.
Dalam contoh dependency_injector, penyedia config memungkinkan konfigurasi dari variabel lingkungan. Untuk kontainer kustom, Anda dapat memuat konfigurasi dari file atau variabel lingkungan dan menyimpannya di dalam kontainer.
3. Pengujian
Salah satu manfaat utama DI adalah peningkatan keterujian (testability). Dengan kontainer DI, Anda dapat dengan mudah mengganti dependensi nyata dengan objek tiruan atau test doubles selama pengujian.
Contoh (Pengujian dengan `dependency_injector`):
import pytest
from unittest.mock import MagicMock
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
# Definisikan dependensi (sama seperti sebelumnya)
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
def get_items(self):
return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
class UserRepository:
def __init__(self, database: Database):
self.database = database
def get_all_users(self):
return [{"id": "user1", "name": "Alice"},{"id": "user2", "name": "Bob"}]
class Settings:
def __init__(self, database_url):
self.database_url = database_url
# Definisikan kontainer (sama seperti sebelumnya)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
settings = providers.Singleton(Settings, database_url = config.database_url)
database = providers.Singleton(Database, connection_string=config.database_url)
user_repository = providers.Factory(UserRepository, database=database)
# Buat aplikasi FastAPI (sama seperti sebelumnya)
app = FastAPI()
# Konfigurasikan kontainer (dari variabel lingkungan)
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__]) # memungkinkan injeksi dependensi ke dalam endpoint FastAPI
# Dependensi untuk FastAPI
def get_user_repository(user_repository: UserRepository = Depends(container.user_repository.provided)) -> UserRepository:
return user_repository
# Endpoint menggunakan dependensi yang diinjeksikan (sama seperti sebelumnya)
@app.get("/users/")
async def read_users(user_repository: UserRepository = Depends(get_user_repository)):
return user_repository.get_all_users()
@app.on_event("startup")
async def startup_event():
# Inisialisasi kontainer
container.init_resources()
# Uji
@pytest.fixture
def test_client():
# Timpa dependensi basis data dengan mock
database_mock = MagicMock(spec=Database)
database_mock.get_items.return_value = [{"id": 3, "name": "Test Item"}]
user_repository_mock = MagicMock(spec = UserRepository)
user_repository_mock.get_all_users.return_value = [{"id": "test_user", "name": "Test User"}]
# Timpa kontainer dengan dependensi mock
container.user_repository.override(providers.Factory(lambda: user_repository_mock))
with TestClient(app) as client:
yield client
container.user_repository.reset()
def test_read_users(test_client: TestClient):
response = test_client.get("/users/")
assert response.status_code == 200
assert response.json() == [{"id": "test_user", "name": "Test User"}]
Penjelasan:
- Kita membuat objek tiruan untuk dependensi
DatabasemenggunakanMagicMock. - Kita menimpa penyedia
databasedi dalam kontainer dengan objek tiruan menggunakancontainer.database.override(). - Fungsi pengujian
test_read_itemssekarang menggunakan dependensi basis data tiruan. - Setelah eksekusi pengujian, dependensi yang ditimpa pada kontainer akan direset.
4. Dependensi Asinkron
FastAPI dibangun di atas pemrograman asinkron (async/await). Saat bekerja dengan dependensi asinkron (misalnya, koneksi basis data asinkron), pastikan kontainer DI dan penyedia dependensi Anda mendukung operasi asinkron.
Contoh (Dependensi Asinkron dengan `dependency_injector`):
import asyncio
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
# Definisikan dependensi asinkron
class AsyncDatabase:
def __init__(self, connection_string: str):
self.connection_string = connection_string
async def connect(self):
print(f"Menghubungkan ke basis data: {self.connection_string}")
await asyncio.sleep(0.1) # Mensimulasikan waktu koneksi
async def fetch_data(self):
await asyncio.sleep(0.1) # Mensimulasikan kueri basis data
return [{"id": 1, "name": "Async Item 1"}]
# Definisikan kontainer
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
database = providers.Singleton(AsyncDatabase, connection_string=config.database_url)
# Buat aplikasi FastAPI
app = FastAPI()
# Konfigurasikan kontainer
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__])
# Dependensi untuk FastAPI
async def get_async_database(database: AsyncDatabase = Depends(container.database.provided)) -> AsyncDatabase:
await database.connect()
return database
# Endpoint menggunakan dependensi yang diinjeksikan
@app.get("/async-items/")
async def read_async_items(database: AsyncDatabase = Depends(get_async_database)):
data = await database.fetch_data()
return data
@app.on_event("startup")
async def startup_event():
# Inisialisasi kontainer
container.init_resources()
Penjelasan:
- Kelas
AsyncDatabasemendefinisikan metode asinkron menggunakanasyncdanawait. - Dependensi
get_async_databasejuga didefinisikan sebagai fungsi asinkron. - Fungsi endpoint
read_async_itemsditandai sebagaiasyncdan menunggu hasil daridatabase.fetch_data().
Memilih Pendekatan yang Tepat
Pendekatan terbaik untuk membangun kontainer DI tingkat lanjut bergantung pada kompleksitas aplikasi Anda dan persyaratan spesifik Anda:
- Untuk proyek skala kecil hingga menengah: DI bawaan FastAPI atau pendekatan fungsi pabrik dengan
Dependsmungkin sudah cukup. - Untuk proyek yang lebih besar dan kompleks: Pustaka DI khusus seperti
dependency_injectormenyediakan serangkaian fitur komprehensif untuk mengelola dependensi. - Untuk proyek yang memerlukan kontrol terperinci atas proses DI: Mengimplementasikan kontainer DI kustom mungkin menjadi pilihan terbaik.
Kesimpulan
Injeksi dependensi adalah teknik yang kuat untuk membangun aplikasi yang skalabel, dapat dipelihara, dan dapat diuji. Meskipun sistem DI bawaan FastAPI sangat baik untuk kasus penggunaan sederhana, arsitektur kontainer DI tingkat lanjut dapat memberikan manfaat signifikan untuk proyek yang lebih kompleks. Dengan memilih pendekatan yang tepat dan memanfaatkan fitur-fitur pustaka DI atau mengimplementasikan kontainer kustom, Anda dapat membuat sistem manajemen dependensi yang kuat dan fleksibel yang meningkatkan kualitas dan keterpeliharaan aplikasi FastAPI Anda secara keseluruhan.
Pertimbangan Global
Saat merancang kontainer DI untuk aplikasi global, penting untuk mempertimbangkan hal-hal berikut:
- Lokalisasi: Dependensi yang terkait dengan lokalisasi (misalnya, pengaturan bahasa, format tanggal) harus dikelola oleh kontainer DI untuk memastikan konsistensi di berbagai wilayah.
- Zona Waktu: Dependensi yang menangani konversi zona waktu harus disuntikkan untuk menghindari hardcoding informasi zona waktu.
- Mata Uang: Dependensi untuk konversi dan pemformatan mata uang harus dikelola oleh kontainer untuk mendukung berbagai mata uang.
- Pengaturan Regional: Pengaturan regional lainnya, seperti format angka dan format alamat, juga harus dikelola oleh kontainer DI.
- Multi-tenancy: Untuk aplikasi multi-tenant, kontainer DI harus dapat menyediakan dependensi yang berbeda untuk tenant yang berbeda. Hal ini dapat dicapai dengan menggunakan cakupan atau logika penyelesaian dependensi kustom.
- Kepatuhan dan Keamanan: Pastikan strategi manajemen dependensi Anda mematuhi peraturan privasi data yang relevan (misalnya, GDPR, CCPA) dan praktik terbaik keamanan di berbagai wilayah. Tangani kredensial dan konfigurasi sensitif secara aman di dalam kontainer.
Dengan mempertimbangkan faktor-faktor global ini, Anda dapat membuat kontainer DI yang sangat cocok untuk membangun aplikasi yang beroperasi di lingkungan global.