Injeksi Ketergantungan: Cara Python
Pola DI Python untuk kode yang bersih dan dapat diuji
Injeksi ketergantungan (DI) adalah pola desain dasar yang mendorong kode bersih, dapat diuji, dan dapat dipelihara dalam aplikasi Python.
Apakah Anda sedang membangun REST API dengan FastAPI, menerapkan unit tests, atau bekerja dengan AWS Lambda functions, memahami injeksi ketergantungan akan secara signifikan meningkatkan kualitas kode Anda.

Apa itu Injeksi Ketergantungan?
Injeksi ketergantungan adalah pola desain di mana komponen menerima ketergantungan dari sumber eksternal daripada menciptakannya secara internal. Pendekatan ini memisahkan komponen, membuat kode Anda lebih modular, dapat diuji, dan dapat dipelihara.
Di Python, injeksi ketergantungan sangat kuat karena sifat dinamis bahasa dan dukungan untuk protokol, kelas dasar abstrak, dan tipe burung. Fleksibilitas Python berarti Anda dapat menerapkan pola DI tanpa kerangka kerja berat, meskipun kerangka kerja tersedia ketika diperlukan.
Mengapa Menggunakan Injeksi Ketergantungan di Python?
Kemudahan Pengujian: Dengan menginjeksikan ketergantungan, Anda dapat dengan mudah mengganti implementasi nyata dengan mock atau ganda pengujian. Ini memungkinkan Anda menulis pengujian unit yang cepat, terisolasi, dan tidak memerlukan layanan eksternal seperti database atau API. Ketika menulis pengujian unit menyeluruh, injeksi ketergantungan membuatnya mudah untuk mengganti ketergantungan nyata dengan ganda pengujian.
Pemeliharaan yang Lebih Baik: Ketergantungan menjadi eksplisit dalam kode Anda. Ketika Anda melihat konstruktor, Anda segera melihat apa yang diperlukan oleh komponen. Ini membuat kodebase lebih mudah dipahami dan dimodifikasi.
Ketergantungan Longgar: Komponen bergantung pada abstraksi (protokol atau ABC) daripada implementasi konkret. Ini berarti Anda dapat mengubah implementasi tanpa memengaruhi kode yang bergantung.
Fleksibilitas: Anda dapat mengatur implementasi berbeda untuk lingkungan berbeda (pengembangan, pengujian, produksi) tanpa mengubah logika bisnis Anda. Ini sangat berguna ketika mendeploy aplikasi Python ke berbagai platform, baik itu AWS Lambda atau server tradisional.
Injeksi Konstruktor: Cara Python
Cara paling umum dan idiomatic untuk menerapkan injeksi ketergantungan di Python adalah melalui injeksi konstruktor—menerima ketergantungan sebagai parameter dalam metode __init__.
Contoh Dasar
Berikut adalah contoh sederhana yang menunjukkan injeksi konstruktor:
from typing import Protocol
from abc import ABC, abstractmethod
# Mendefinisikan protokol untuk repositori
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# Layanan bergantung pada protokol repositori
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, user_id: int) -> 'User | None':
return self.repo.find_by_id(user_id)
Polanya ini membuat jelas bahwa UserService memerlukan UserRepository. Anda tidak dapat menciptakan UserService tanpa menyediakan repositori, yang mencegah kesalahan runtime dari ketergantungan yang hilang.
Banyak Ketergantungan
Ketika komponen memiliki banyak ketergantungan, cukup tambahkan sebagai parameter konstruktor:
class EmailService(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
class Logger(Protocol):
def info(self, msg: str) -> None:
...
def error(self, msg: str, err: Exception) -> None:
...
class OrderService:
def __init__(
self,
repo: OrderRepository,
email_svc: EmailService,
logger: Logger,
payment_svc: PaymentService,
):
self.repo = repo
self.email_svc = email_svc
self.logger = logger
self.payment_svc = payment_svc
Menggunakan Protokol dan Kelas Dasar Abstrak
Salah satu prinsip kunci ketika menerapkan injeksi ketergantungan adalah Prinsip Inversi Ketergantungan (DIP): modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah; keduanya harus bergantung pada abstraksi.
Di Python, Anda dapat mendefinisikan abstraksi menggunakan Protokol (pengetikan struktural) atau Kelas Dasar Abstrak (ABC) (pengetikan nominal).
Protokol (Python 3.8+)
Protokol menggunakan pengetikan struktural—jika objek memiliki metode yang diperlukan, maka memenuhi protokol:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Setiap kelas dengan metode process_payment memenuhi protokol ini
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Logika kartu kredit
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Logika PayPal
return True
# Layanan menerima setiap PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Kelas Dasar Abstrak
ABC menggunakan pengetikan nominal—kelas harus secara eksplisit mewarisi dari ABC:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
return True
Kapan menggunakan Protokol vs ABC: Gunakan Protokol ketika Anda ingin pengetikan struktural dan fleksibilitas. Gunakan ABC ketika Anda perlu memaksa hierarki pewarisan atau menyediakan implementasi default.
Contoh Nyata: Abstraksi Database
Ketika bekerja dengan operasi database di aplikasi Python, Anda sering kali perlu mengabstraksi operasi database. Berikut bagaimana injeksi ketergantungan membantu:
from typing import Protocol, Optional
from contextlib import contextmanager
class Database(Protocol):
@contextmanager
def transaction(self):
...
def execute(self, query: str, params: dict) -> None:
...
def fetch_one(self, query: str, params: dict) -> Optional[dict]:
...
# Repositori bergantung pada abstraksi
class UserRepository:
def __init__(self, db: Database):
self.db = db
def find_by_id(self, user_id: int) -> Optional['User']:
result = self.db.fetch_one(
"SELECT * FROM users WHERE id = :id",
{"id": user_id}
)
if result:
return User(**result)
return None
Polanya ini memungkinkan Anda mengganti implementasi database (PostgreSQL, SQLite, MongoDB) tanpa mengubah kode repositori Anda.
Pola Akar Komposisi
Akar Komposisi adalah tempat Anda menggabungkan semua ketergantungan di titik masuk aplikasi (biasanya main.py atau pabrik aplikasi Anda). Ini memusatkan konfigurasi ketergantungan dan membuat grafik ketergantungan eksplisit.
def create_app() -> FastAPI:
app = FastAPI()
# Inisialisasi ketergantungan infrastruktur
db = init_database()
logger = init_logger()
# Inisialisasi repositori
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Inisialisasi layanan dengan ketergantungan
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Inisialisasi penangan HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Hubungkan rute
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Pendekatan ini membuat jelas bagaimana aplikasi Anda terstruktur dan dari mana ketergantungan berasal. Ini sangat bernilai ketika membangun aplikasi berdasarkan prinsip arsitektur bersih, di mana Anda perlu mengkoordinasikan berbagai lapisan ketergantungan.
Kerangka Kerja Injeksi Ketergantungan
Untuk aplikasi besar dengan grafik ketergantungan kompleks, mengelola ketergantungan secara manual dapat menjadi melelahkan. Python memiliki beberapa kerangka kerja DI yang dapat membantu:
Dependency Injector
Dependency Injector adalah kerangka kerja populer yang menyediakan pendekatan berbasis wadah untuk injeksi ketergantungan.
Instalasi:
pip install dependency-injector
Contoh:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Konfigurasi
config = providers.Configuration()
# Database
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Repositori
user_repository = providers.Factory(
UserRepository,
db=db
)
# Layanan
user_service = providers.Factory(
UserService,
repo=user_repository
)
# Penggunaan
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector adalah perpustakaan ringan yang terinspirasi oleh Guice Google, fokus pada kesederhanaan.
Instalasi:
pip install injector
Contoh:
from injector import Injector, inject, Module, provider
class DatabaseModule(Module):
@provider
def provide_db(self) -> Database:
return Database(connection_string="...")
class UserModule(Module):
@inject
def __init__(self, repo: UserRepository):
self.repo = repo
injector = Injector([DatabaseModule()])
user_service = injector.get(UserService)
Kapan Menggunakan Kerangka Kerja
Gunakan kerangka kerja ketika:
- Grafik ketergantungan Anda kompleks dengan banyak komponen yang saling bergantung
- Anda memiliki banyak implementasi dari antarmuka yang sama yang perlu dipilih berdasarkan konfigurasi
- Anda ingin resolusi ketergantungan otomatis
- Anda membangun aplikasi besar di mana wiring manual menjadi rentan terhadap kesalahan
Tetapkan dengan DI manual ketika:
- Aplikasi Anda kecil hingga sedang
- Grafik ketergantungan Anda sederhana dan mudah diikuti
- Anda ingin menjaga ketergantungan minimal dan eksplisit
- Anda lebih memilih kode eksplisit daripada ajaib kerangka kerja
Pengujian dengan Injeksi Ketergantungan
Salah satu manfaat utama dari injeksi ketergantungan adalah peningkatan kemudahan pengujian. Berikut cara DI membuat pengujian lebih mudah:
Contoh Pengujian Unit
from unittest.mock import Mock
import pytest
# Implementasi mock untuk pengujian
class MockUserRepository:
def __init__(self):
self.users = {}
self.error = None
def find_by_id(self, user_id: int) -> Optional['User']:
if self.error:
raise self.error
return self.users.get(user_id)
def save(self, user: 'User') -> 'User':
if self.error:
raise self.error
self.users[user.id] = user
return user
# Uji menggunakan mock
def test_user_service_get_user():
mock_repo = MockUserRepository()
mock_repo.users[1] = User(id=1, name="John", email="john@example.com")
service = UserService(mock_repo)
user = service.get_user(1)
assert user is not None
assert user.name == "John"
Uji ini berjalan cepat, tidak memerlukan database, dan menguji logika bisnis Anda secara terisolasi. Ketika bekerja dengan pengujian unit di Python, injeksi ketergantungan membuat mudah untuk menciptakan ganda pengujian dan memverifikasi interaksi.
Menggunakan Fixtures pytest
Fixtures pytest bekerja sangat baik dengan injeksi ketergantungan:
@pytest.fixture
def mock_user_repository():
return MockUserRepository()
@pytest.fixture
def user_service(mock_user_repository):
return UserService(mock_user_repository)
def test_user_service_get_user(user_service, mock_user_repository):
user = User(id=1, name="John", email="john@example.com")
mock_user_repository.users[1] = user
result = user_service.get_user(1)
assert result.name == "John"
Pola Umum dan Praktik Terbaik
1. Gunakan Segregasi Antarmuka
Jaga protokol dan antarmuka kecil dan fokus pada apa yang sebenarnya dibutuhkan klien:
# Baik: Klien hanya perlu membaca pengguna
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Antarmuka terpisah untuk menulis
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Validasi Ketergantungan dalam Konstruktor
Konstruktor harus memvalidasi ketergantungan dan menaikkan kesalahan yang jelas jika inisialisasi gagal:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("user repository cannot be None")
self.repo = repo
3. Gunakan Petunjuk Tipe
Petunjuk tipe membuat ketergantungan eksplisit dan membantu dengan dukungan IDE dan pemeriksaan tipe statis:
from typing import Protocol, Optional
class UserService:
def __init__(
self,
repo: UserRepository,
logger: Logger,
email_service: EmailService,
) -> None:
self.repo = repo
self.logger = logger
self.email_service = email_service
4. Hindari Over-Injeksi
Jangan injeksikan ketergantungan yang benar-benar detail implementasi internal. Jika komponen menciptakan dan mengelola objek bantuan sendiri, itu baik-baik saja:
# Baik: Objek bantuan internal tidak perlu injeksi
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Cache internal - tidak perlu injeksi
self._cache: dict[int, User] = {}
5. Dokumentasikan Ketergantungan
Gunakan docstring untuk mendokumentasikan mengapa ketergantungan diperlukan dan setiap batasan:
class UserService:
"""UserService menangani logika bisnis terkait pengguna.
Args:
repo: UserRepository untuk akses data. Harus aman untuk thread
jika digunakan dalam konteks konkuren.
logger: Logger untuk pelacakan kesalahan dan debugging.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Injeksi Ketergantungan dengan FastAPI
FastAPI memiliki dukungan bawaan untuk injeksi ketergantungan melalui mekanisme Depends-nya:
from fastapi import FastAPI, Depends
from typing import Annotated
app = FastAPI()
def get_user_repository() -> UserRepository:
db = get_database()
return UserRepository(db)
def get_user_service(
repo: Annotated[UserRepository, Depends(get_user_repository)]
) -> UserService:
return UserService(repo)
@app.get("/users/{user_id}")
def get_user(
user_id: int,
service: Annotated[UserService, Depends(get_user_service)]
):
user = service.get_user(user_id)
if not user:
raise HTTPException(status_code=404)
return user
Sistem injeksi ketergantungan FastAPI menangani grafik ketergantungan secara otomatis, membuat mudah untuk membangun API bersih, dapat dipelihara.
Kapan TIDAK Menggunakan Injeksi Ketergantungan
Injeksi ketergantungan adalah alat yang kuat, tetapi tidak selalu diperlukan:
Lewati DI untuk:
- Objek nilai sederhana atau kelas data
- Fungsi bantuan internal atau utilitas
- Skrip sederhana atau utilitas kecil
- Ketika instansiasi langsung lebih jelas dan sederhana
Contoh kapan TIDAK menggunakan DI:
# Kelas data sederhana - tidak perlu DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Utilitas sederhana - tidak perlu DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Integrasi dengan Ekosistem Python
Injeksi ketergantungan bekerja dengan lancar dengan pola dan alat Python lainnya. Ketika membangun aplikasi yang menggunakan paket Python atau kerangka pengujian unit, Anda dapat menyuntikkan layanan ini ke dalam logika bisnis Anda:
class ReportService:
def __init__(
self,
pdf_generator: PDFGenerator,
repo: ReportRepository,
logger: Logger,
):
self.pdf_generator = pdf_generator
self.repo = repo
self.logger = logger
def generate_report(self, report_id: int) -> bytes:
report_data = self.repo.get_by_id(report_id)
pdf = self.pdf_generator.generate(report_data)
self.logger.info(f"Generated report {report_id}")
return pdf
Ini memungkinkan Anda mengganti implementasi atau menggunakan mock selama pengujian.
Injeksi Ketergantungan Async
Sintaks async/await Python bekerja dengan baik dengan injeksi ketergantungan:
from typing import Protocol
import asyncio
class AsyncUserRepository(Protocol):
async def find_by_id(self, user_id: int) -> Optional['User']:
...
async def save(self, user: 'User') -> 'User':
...
class AsyncUserService:
def __init__(self, repo: AsyncUserRepository):
self.repo = repo
async def get_user(self, user_id: int) -> Optional['User']:
return await self.repo.find_by_id(user_id)
async def get_users_batch(self, user_ids: list[int]) -> list['User']:
tasks = [self.repo.find_by_id(uid) for uid in user_ids]
results = await asyncio.gather(*tasks)
return [u for u in results if u is not None]
Kesimpulan
Injeksi ketergantungan adalah fondasi dari menulis kode Python yang dapat dipelihara dan dapat diuji. Dengan mengikuti pola yang dijelaskan dalam artikel ini—penginjeksian konstruktor, desain berbasis protokol, dan pola akar komposisi—Anda akan menciptakan aplikasi yang lebih mudah dipahami, diuji, dan dimodifikasi.
Mulailah dengan injeksi konstruktor manual untuk aplikasi kecil hingga sedang, dan pertimbangkan kerangka kerja seperti dependency-injector atau injector ketika grafik ketergantungan Anda berkembang. Ingat bahwa tujuannya adalah kejelasan dan kemudahan pengujian, bukan kompleksitas untuk sendiri.
Untuk sumber daya pengembangan Python tambahan, periksa Python Cheatsheet kami untuk referensi cepat tentang sintaks Python dan pola umum.
Tautan Berguna
- Python Cheatsheet
- Unit Testing in Python
- Python Design Patterns for Clean Architecture
- Structured Output - LLMs on Ollama with Qwen3 - Python and Go
- Perbandingan Output Terstruktur di Berbagai Penyedia LLM Populer - OpenAI, Gemini, Anthropic, Mistral dan AWS Bedrock
- Membangun AWS Lambda Dual-Mode dengan Python dan Terraform
Sumber Daya Eksternal
- Injeksi Ketergantungan dalam Python - Real Python
- Bagaimana Injeksi Ketergantungan dalam Python Meningkatkan Struktur Kode - Volito Digital
- Tutorial Injeksi Ketergantungan Python - DataCamp
- Dependency Injector - Dokumentasi Resmi
- Injector - Kerangka Kerja DI Ringan
- Ketergantungan FastAPI - Dokumentasi Resmi
- Prinsip SOLID dalam Python - Lexikon Pola Perangkat Lunak
- Protokol Python dan Subtyping Struktural - PEP 544