Injeksi Ketergantungan: Cara Python

Pola DI Python untuk kode yang bersih dan dapat diuji

Konten Halaman

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.

python packages

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

Sumber Daya Eksternal