← Kursa Dön
📄 Text · 15 min

Dependency Injection Pattern

Kodun büyüyor. Bir sınıf veritabanına bağlanıyor, başka bir sınıf e-posta gönderiyor, üçüncüsü ödeme işliyor. Her şey çalışıyor — ta ki test yazmaya çalışana kadar. OrderService test etmek istiyorsun ama gerçek veritabanına bağlanıyor, gerçek ödeme API'sına istek atıyor. Test ortamında bunlar yok. Ya da var ama her test çalışmasında gerçek para mı çekeceksin?

Dependency Injection (DI) bu sorunu çözer. Bağımlılıkları sınıfın içinde oluşturmak yerine dışarıdan verirsin. Test ederken sahte (mock) bağımlılık verirsin, production'da gerçeğini verirsin. Kod değişmez — sadece bağımlılık değişir.


1. DI Nedir, Neden Gerekli?

🔌 Analoji: Elektrik Prizi

Bir lamba düşün. Lamba içinde kablo direkt duvardaki bakır tele lehimlenmiş olsaydı ne olurdu? Lambayı başka odaya taşıyamazsın. Voltajı değiştiremezsin. Test etmek için tüm binanın elektrik sistemini çalıştırman lazım.

Gerçek dünyada lamba bir prize takılır. Priz bir arayüzdür (interface). Lambanın umurunda değil arkasında ne var — şehir şebekesi mi, jeneratör mü, güneş paneli mi. O sadece "bana 220V ver" der. İstediğin kaynağı prize tak, lamba çalışır.

DI tam olarak bu. Sınıfın bağımlılıklarını kendi içinde oluşturmak yerine, bir "priz" (constructor parametresi) üzerinden dışarıdan alır. Böylece istediğin implementasyonu takarsın.

DI Olmadan: Sıkı Bağlantı (Tight Coupling)

import smtplib

class OrderService:
    def __init__(self):
        # ❌ Bağımlılık sınıfın İÇİNDE oluşturuluyor
        self.email_sender = smtplib.SMTP("smtp.gmail.com", 587)
        self.db = PostgresDatabase("localhost", 5432)
    
    def create_order(self, user_id, items):
        order = self.db.insert("orders", user_id=user_id, items=items)
        self.email_sender.send(f"Siparişiniz alındı: {order.id}")
        return order

Bu kodun sorunları:

  • Test edilemez: Test ederken gerçek SMTP sunucusuna bağlanır, gerçek veritabanı gerekir

  • Değiştirilemez: Gmail yerine AWS SES kullanmak istersen sınıfın kodunu değiştirmen lazım

  • Esnek değil: Farklı ortamlarda (dev, staging, prod) farklı ayar kullanmak zor

DI ile: Gevşek Bağlantı (Loose Coupling)

class OrderService:
    def __init__(self, db, email_sender):
        # ✅ Bağımlılıklar DIŞARIDAN veriliyor
        self.db = db
        self.email_sender = email_sender
    
    def create_order(self, user_id, items):
        order = self.db.insert("orders", user_id=user_id, items=items)
        self.email_sender.send(f"Siparişiniz alındı: {order.id}")
        return order

# Production'da
service = OrderService(
    db=PostgresDatabase("prod-server", 5432),
    email_sender=GmailSender("app@company.com")
)

# Test'te
service = OrderService(
    db=InMemoryDatabase(),
    email_sender=FakeEmailSender()
)

Aynı OrderService sınıfı, hiç değişmeden hem production'da hem test'te çalışıyor. Tek fark: dışarıdan verilen bağımlılıklar.


2. Constructor Injection

DI'ın en yaygın ve önerilen biçimi constructor injection'dır. Bağımlılıklar __init__ parametresi olarak alınır.

Temel Pattern

class NotificationService:
    """Bildirim gönderme servisi."""
    
    def __init__(self, sender, template_engine):
        self.sender = sender
        self.template_engine = template_engine
    
    def notify_user(self, user, event):
        message = self.template_engine.render(event)
        self.sender.send(user.email, message)


class EmailSender:
    def __init__(self, smtp_host, smtp_port):
        self.smtp_host = smtp_host
        self.smtp_port = smtp_port
    
    def send(self, to, message):
        print(f"E-posta gönderildi: {to} — {message[:50]}...")


class JinjaTemplateEngine:
    def render(self, event):
        return f"Merhaba! {event} olayı gerçekleşti."


# Bağımlılıkları oluştur ve enjekte et
sender = EmailSender("smtp.gmail.com", 587)
engine = JinjaTemplateEngine()

service = NotificationService(sender=sender, template_engine=engine)

Bu yapıda her sınıfın tek bir sorumluluğu var. NotificationService nasıl e-posta gönderileceğini bilmiyor — sadece sender.send() çağırıyor. E-posta yerine SMS göndermek istersen, aynı send() metoduna sahip bir SmsSender sınıfı yazıp verirsin.

Neden Constructor?

DI'ı setter (metod) veya property üzerinden de yapabilirsin ama constructor tercih edilir:

# ❌ Setter injection — nesne yarım oluşturulmuş olabilir
service = NotificationService()
service.set_sender(email_sender)  # Bunu çağırmayı unutursan?
service.notify_user(user, event)  # 💥 AttributeError

# ✅ Constructor injection — nesne tam oluşturulur veya hiç oluşturulmaz
service = NotificationService(sender=email_sender, template_engine=engine)
# Eksik parametre verirsen TypeError — hata anında yakalanır

Constructor injection, sınıfın her zaman geçerli bir durumda olmasını garanti eder. Zorunlu bağımlılıklar constructor'da, opsiyonel olanlar varsayılan değerle veya setter ile verilebilir.


3. ABC ile Arayüz Tanımlama

Python'da Java'daki gibi zorunlu interface yok ama ABC (Abstract Base Class) ile benzer bir yapı kurabilirsin. Bu, bağımlılıkların hangi metotlara sahip olması gerektiğini açıkça belirtir.

ABC + Concrete Sınıflar

from abc import ABC, abstractmethod


# Arayüz (Interface) tanımla
class PaymentGateway(ABC):
    """Ödeme sistemi arayüzü."""
    
    @abstractmethod
    def charge(self, amount: float, currency: str) -> str:
        """Ödeme al, transaction ID döndür."""
        pass
    
    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        """İade yap."""
        pass


# Concrete implementasyon 1: Stripe
class StripeGateway(PaymentGateway):
    def __init__(self, api_key: str):
        self.api_key = api_key
    
    def charge(self, amount: float, currency: str) -> str:
        print(f"Stripe: {amount} {currency} çekildi")
        return f"stripe_txn_{id(self)}"
    
    def refund(self, transaction_id: str) -> bool:
        print(f"Stripe: {transaction_id} iade edildi")
        return True


# Concrete implementasyon 2: PayPal
class PayPalGateway(PaymentGateway):
    def __init__(self, client_id: str, secret: str):
        self.client_id = client_id
        self.secret = secret
    
    def charge(self, amount: float, currency: str) -> str:
        print(f"PayPal: {amount} {currency} çekildi")
        return f"paypal_txn_{id(self)}"
    
    def refund(self, transaction_id: str) -> bool:
        print(f"PayPal: {transaction_id} iade edildi")
        return True


# Servis — hangi gateway olduğunu bilmiyor, umursamıyor
class CheckoutService:
    def __init__(self, payment: PaymentGateway):
        self.payment = payment
    
    def process(self, cart_total: float):
        txn_id = self.payment.charge(cart_total, "TRY")
        print(f"İşlem tamamlandı: {txn_id}")
        return txn_id


# Stripe ile
checkout = CheckoutService(payment=StripeGateway("sk_test_xxx"))
checkout.process(299.99)

# PayPal ile — aynı servis, farklı bağımlılık
checkout = CheckoutService(
    payment=PayPalGateway("client_xxx", "secret_xxx")
)
checkout.process(299.99)

ABC kullanmanın avantajı: birisi PaymentGateway'i implemente ederken refund metodunu yazmayı unutursa, sınıf oluşturulurken TypeError alır — runtime hatası beklemezsin.

class BrokenGateway(PaymentGateway):
    def charge(self, amount, currency):
        return "txn_123"
    # refund() eksik!

# gateway = BrokenGateway()
# TypeError: Can't instantiate abstract class BrokenGateway
#            with abstract method refund

4. Protocol ile Duck-Typing DI

Python 3.8+ ile gelen Protocol, ABC'nin daha Pythonic alternatifidir. Kalıtım gerektirmez — sadece doğru metotlara sahip olmak yeterlidir. Bu duck typing'in resmileştirilmiş halidir: "Ördek gibi yürüyorsa ve ördek gibi ötüyorsa, ördektür."

Protocol Tanımlama

from typing import Protocol, runtime_checkable


@runtime_checkable
class EmailSender(Protocol):
    """E-posta gönderebilen herhangi bir nesne."""
    
    def send(self, to: str, subject: str, body: str) -> bool:
        ...


@runtime_checkable
class DataStore(Protocol):
    """Veri saklayabilen herhangi bir nesne."""
    
    def save(self, key: str, data: dict) -> None:
        ...
    
    def load(self, key: str) -> dict | None:
        ...


# Bu sınıf EmailSender Protocol'ünü IMPLEMENTE ETMİYOR (extends yok)
# Ama doğru metoda sahip — yeterli!
class GmailClient:
    def __init__(self, credentials: str):
        self.credentials = credentials
    
    def send(self, to: str, subject: str, body: str) -> bool:
        print(f"Gmail → {to}: {subject}")
        return True


class RedisStore:
    def __init__(self, host: str):
        self.host = host
        self._data = {}
    
    def save(self, key: str, data: dict) -> None:
        self._data[key] = data
    
    def load(self, key: str) -> dict | None:
        return self._data.get(key)


# Protocol uyumu kontrolü
gmail = GmailClient("creds")
print(isinstance(gmail, EmailSender))  # True — metod imzası uyuyor

redis = RedisStore("localhost")
print(isinstance(redis, DataStore))    # True


# Servis — Protocol tipini bekliyor
class UserService:
    def __init__(self, store: DataStore, notifier: EmailSender):
        self.store = store
        self.notifier = notifier
    
    def register(self, email: str, name: str):
        self.store.save(email, {"name": name, "active": True})
        self.notifier.send(email, "Hoş geldiniz!", f"Merhaba {name}")


# Kalıtım yok, Protocol yok — sadece doğru metotlar
service = UserService(store=RedisStore("localhost"), notifier=GmailClient("creds"))
service.register("ali@example.com", "Ali")

ABC vs Protocol: Ne Zaman Hangisi?

ÖzellikABCProtocol
Kalıtım gerekli mi?EvetHayır
Mevcut sınıflarla uyumlu mu?Hayır (extends lazım)Evet (duck typing)
Hata yakalama zamanıSınıf oluşturulurkenTip kontrolü sırasında (mypy)
Python versiyonu3.0+3.8+
Ne zaman kullan?Kendi kodunda sıkı kontrolÜçüncü parti kütüphanelerle çalışırken

Protocol özellikle mevcut sınıfları değiştirmeden DI yapmak istediğinde güçlüdür. Bir kütüphanenin sınıfını extends edemezsin ama Protocol ile uyumluluğunu kontrol edebilirsin.


5. Basit DI Container Yazma

Küçük projelerde bağımlılıkları elle oluşturup geçirmek yeterli. Ama proje büyüdükçe, 10-15 servisin birbirine bağımlı olduğu bir noktaya gelirsin. Her birini elle oluşturmak ve doğru sırada bağlamak zahmetleşir. DI Container bu işi otomatikleştirir.

Minimal Container

from typing import Any, Callable


class Container:
    """Basit Dependency Injection container."""
    
    def __init__(self):
        self._factories: dict[str, Callable] = {}
        self._singletons: dict[str, Any] = {}
        self._singleton_flags: set[str] = set()
    
    def register(self, name: str, factory: Callable, singleton: bool = False):
        """Bir bağımlılık fabrikası kaydet."""
        self._factories[name] = factory
        if singleton:
            self._singleton_flags.add(name)
    
    def resolve(self, name: str) -> Any:
        """Bir bağımlılığı çözümle (oluştur veya cache'den al)."""
        if name in self._singletons:
            return self._singletons[name]
        
        if name not in self._factories:
            raise KeyError(f"Kayıtlı olmayan bağımlılık: {name}")
        
        instance = self._factories[name](self)
        
        if name in self._singleton_flags:
            self._singletons[name] = instance
        
        return instance


# Kullanım
container = Container()

# Kayıt — factory fonksiyonları
container.register("config", lambda c: {
    "db_host": "localhost",
    "db_port": 5432,
    "smtp_host": "smtp.gmail.com"
}, singleton=True)

container.register("database", lambda c: {
    "host": c.resolve("config")["db_host"],
    "port": c.resolve("config")["db_port"],
    "connection": "active"
}, singleton=True)

container.register("email_sender", lambda c: {
    "smtp": c.resolve("config")["smtp_host"],
    "type": "gmail"
})

container.register("order_service", lambda c: {
    "db": c.resolve("database"),
    "email": c.resolve("email_sender"),
    "name": "OrderService"
})

# Çözümleme — bağımlılıklar otomatik enjekte edilir
service = container.resolve("order_service")
print(service["db"]["host"])  # localhost
print(service["email"]["type"])  # gmail

# Singleton testi
db1 = container.resolve("database")
db2 = container.resolve("database")
print(db1 is db2)  # True — aynı nesne

Container'ın temel fikri: "neye ihtiyacım var" ile "nasıl oluşturulur" arasındaki bağlantıyı merkezi bir yerde tanımlarsın. resolve() çağrıldığında container bağımlılık ağacını gezer ve her şeyi doğru sırada oluşturur.

⚠️ Dikkat: Kendi DI container'ını yazmak eğitici ama production'da battle-tested kütüphaneleri tercih et. Circular dependency tespiti, scope yönetimi, thread safety gibi konular hızla karmaşıklaşır.


6. dependency-injector Kütüphanesi

Python ekosisteminin en olgun DI kütüphanesi dependency-injector'dır. Tip güvenliği, scope yönetimi, konfigürasyon entegrasyonu ve daha fazlasını sağlar.

Kurulum ve Temel Kullanım

pip install dependency-injector
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide


# Servisler
class DatabaseClient:
    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port
        print(f"DB bağlantısı: {host}:{port}")
    
    def query(self, sql: str):
        return f"Sonuç: {sql}"


class CacheClient:
    def __init__(self, host: str, ttl: int):
        self.host = host
        self.ttl = ttl


class UserRepository:
    def __init__(self, db: DatabaseClient, cache: CacheClient):
        self.db = db
        self.cache = cache
    
    def find_user(self, user_id: int):
        return self.db.query(f"SELECT * FROM users WHERE id={user_id}")


class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo
    
    def get_user(self, user_id: int):
        return self.repo.find_user(user_id)


# DI Container tanımla
class AppContainer(containers.DeclarativeContainer):
    """Uygulama DI container'ı."""
    
    config = providers.Configuration()
    
    # Singleton — uygulama boyunca tek instance
    database = providers.Singleton(
        DatabaseClient,
        host=config.db.host,
        port=config.db.port,
    )
    
    cache = providers.Singleton(
        CacheClient,
        host=config.cache.host,
        ttl=config.cache.ttl,
    )
    
    # Factory — her çağrıda yeni instance
    user_repo = providers.Factory(
        UserRepository,
        db=database,
        cache=cache,
    )
    
    user_service = providers.Factory(
        UserService,
        repo=user_repo,
    )


# Yapılandır ve kullan
container = AppContainer()
container.config.from_dict({
    "db": {"host": "localhost", "port": 5432},
    "cache": {"host": "localhost", "ttl": 300}
})

# Servis al
service = container.user_service()
result = service.get_user(42)
print(result)

dependency-injector'ın gücü provider tiplerinde: Singleton (tek instance), Factory (her seferinde yeni), ThreadLocalSingleton (thread başına tek) gibi scope'lar kullanabilirsin. Konfigürasyon .env, YAML, JSON veya environment variable'lardan okunabilir.

dependency-injector'ın override() mekanizması ile test'te herhangi bir provider'ı mock ile değiştirebilirsin — container.database.override(providers.Object(MockDB())) gibi. Test bitince reset_override() ile temizlersin.


7. FastAPI'nin DI Sistemi (Depends)

FastAPI, Python ekosistemindeki en zarif DI sistemlerinden birine sahiptir. Depends() fonksiyonu ile bağımlılıkları route handler'lara enjekte edersin.

Temel Kullanım

from fastapi import FastAPI, Depends, HTTPException

app = FastAPI()


# Bağımlılık fonksiyonu
def get_database():
    """Veritabanı bağlantısı sağlar."""
    db = {"connection": "active", "host": "localhost"}
    try:
        yield db  # Generator — cleanup için
    finally:
        print("DB bağlantısı kapatıldı")


def get_current_user(token: str = None):
    """Mevcut kullanıcıyı döndürür."""
    if not token:
        raise HTTPException(status_code=401, detail="Token gerekli")
    return {"id": 1, "name": "Ali", "token": token}


# Route'ta bağımlılık kullanımı
@app.get("/users/{user_id}")
def get_user(
    user_id: int,
    db=Depends(get_database),
    current_user=Depends(get_current_user)
):
    return {
        "requested_id": user_id,
        "db_status": db["connection"],
        "requested_by": current_user["name"]
    }

FastAPI bağımlılık fonksiyonunu otomatik çağırır, sonucunu parametre olarak geçirir. yield kullanıldığında cleanup kodu (finally bloğu) response gönderildikten sonra çalışır — veritabanı bağlantılarını kapatmak için mükemmel.

Sınıf Bazlı Bağımlılıklar

from fastapi import FastAPI, Depends

app = FastAPI()


class UserRepository:
    """Kullanıcı veritabanı erişim katmanı."""
    
    def __init__(self):
        # Gerçek uygulamada DB connection alırsın
        self.users = {
            1: {"id": 1, "name": "Ali", "email": "ali@example.com"},
            2: {"id": 2, "name": "Ayşe", "email": "ayse@example.com"},
        }
    
    def find_by_id(self, user_id: int):
        return self.users.get(user_id)
    
    def find_all(self):
        return list(self.users.values())


class UserService:
    """Kullanıcı iş mantığı katmanı."""
    
    def __init__(self, repo: UserRepository = Depends()):
        self.repo = repo
    
    def get_user(self, user_id: int):
        user = self.repo.find_by_id(user_id)
        if not user:
            return None
        return {**user, "display": f"{user['name']} <{user['email']}>"}


@app.get("/users/{user_id}")
def get_user(user_id: int, service: UserService = Depends()):
    user = service.get_user(user_id)
    if not user:
        return {"error": "Kullanıcı bulunamadı"}
    return user


@app.get("/users")
def list_users(repo: UserRepository = Depends()):
    return repo.find_all()

Depends() parantez içi boş olduğunda, FastAPI parametrenin tip annotation'ına bakar ve o sınıfı oluşturur. Sınıfın __init__'inde de Depends() varsa, zincirleme çözümleme yapar. Bu Java Spring'in DI'ına çok benzer ama çok daha az boilerplate ile.

Test'te Override

from fastapi.testclient import TestClient


class FakeUserRepository:
    """Test için sahte repository."""
    
    def find_by_id(self, user_id: int):
        return {"id": user_id, "name": "Test User", "email": "test@test.com"}
    
    def find_all(self):
        return [{"id": 1, "name": "Test User"}]


# Bağımlılığı override et
app.dependency_overrides[UserRepository] = FakeUserRepository

client = TestClient(app)
response = client.get("/users/99")
print(response.json())
# {"id": 99, "name": "Test User", "email": "test@test.com", "display": "..."}

# Testi bitirince temizle
app.dependency_overrides.clear()

dependency_overrides dict'i ile herhangi bir bağımlılığı test sırasında değiştirebilirsin. Gerçek veritabanı, e-posta servisi veya ödeme sistemi yerine sahte implementasyonlar kullanırsın.


8. Test'te Mock Injection

DI'ın en büyük faydası test edilebilirlik. Bağımlılıkları dışarıdan aldığın için, test sırasında mock nesneler enjekte edebilirsin.

unittest.mock ile

from unittest.mock import Mock, MagicMock, patch
from abc import ABC, abstractmethod


# Arayüzler
class NotificationSender(ABC):
    @abstractmethod
    def send(self, to: str, message: str) -> bool: ...

class PaymentProcessor(ABC):
    @abstractmethod
    def charge(self, amount: float) -> str: ...


# Servis
class OrderService:
    def __init__(self, notifier: NotificationSender, payment: PaymentProcessor):
        self.notifier = notifier
        self.payment = payment
    
    def place_order(self, user_email: str, amount: float):
        # Ödeme al
        txn_id = self.payment.charge(amount)
        
        # Bildirim gönder
        self.notifier.send(user_email, f"Ödeme alındı: {txn_id}")
        
        return {"txn_id": txn_id, "status": "completed"}


# TEST
def test_place_order():
    # Mock bağımlılıklar oluştur
    mock_notifier = Mock(spec=NotificationSender)
    mock_notifier.send.return_value = True
    
    mock_payment = Mock(spec=PaymentProcessor)
    mock_payment.charge.return_value = "TXN-12345"
    
    # Servise mock'ları enjekte et
    service = OrderService(
        notifier=mock_notifier,
        payment=mock_payment
    )
    
    # Test et
    result = service.place_order("ali@example.com", 99.99)
    
    # Doğrula
    assert result["txn_id"] == "TXN-12345"
    assert result["status"] == "completed"
    
    # Mock'ların doğru çağrıldığını kontrol et
    mock_payment.charge.assert_called_once_with(99.99)
    mock_notifier.send.assert_called_once_with(
        "ali@example.com", "Ödeme alındı: TXN-12345"
    )
    
    print("✅ Test geçti!")


test_place_order()

Mock(spec=NotificationSender) ile mock nesne, gerçek arayüzün metotlarıyla sınırlandırılır. Yanlışlıkla mock_notifier.sned() (typo) çağırırsan AttributeError alırsın — bu hataları erken yakalar.

Hata senaryolarını test etmek de kolay: mock_payment.charge.side_effect = ConnectionError(...) ile ödeme servisinin çökmesini simüle edip, bildirimin gönderilmediğini mock_notifier.send.assert_not_called() ile doğrularsın. DI olmadan böyle bir testi yazmak çok zor olurdu.


9. Anti-Pattern: Service Locator vs DI

DI ile karıştırılan bir pattern var: Service Locator. Benzer görünür ama önemli farkları var.

Service Locator Nedir?

# ❌ Service Locator — anti-pattern
class ServiceLocator:
    _services = {}
    
    @classmethod
    def register(cls, name, instance):
        cls._services[name] = instance
    
    @classmethod
    def get(cls, name):
        return cls._services[name]


# Kayıt
ServiceLocator.register("database", PostgresDB())
ServiceLocator.register("email", EmailSender())


class OrderService:
    def create_order(self, user_id, items):
        # ❌ Bağımlılık İÇERİDE çözümleniyor
        db = ServiceLocator.get("database")
        email = ServiceLocator.get("email")
        
        order = db.insert("orders", user_id=user_id)
        email.send(f"Sipariş: {order.id}")
        return order

Neden Anti-Pattern?

# Problem 1: Gizli bağımlılıklar
service = OrderService()  # Ne bağımlılığı var? Belli değil!
# Constructor'a baksan hiç parametre yok — ama ServiceLocator'a bağımlı

# Problem 2: Test zorluğu
# Test için global state'i değiştirmen lazım
ServiceLocator.register("database", MockDB())  # Global state kirletme
service.create_order(1, [])
ServiceLocator.register("database", RealDB())  # Geri al — ya unutursan?

# Problem 3: Sıra bağımlılığı
# ServiceLocator.register() çağrılmadan OrderService kullanırsan → KeyError
# Runtime hatası — compile time'da yakalanamaz

DI ile Doğru Yol

# ✅ Dependency Injection — bağımlılıklar açık ve net
class OrderService:
    def __init__(self, db, email):
        self.db = db
        self.email = email
    
    def create_order(self, user_id, items):
        order = self.db.insert("orders", user_id=user_id)
        self.email.send(f"Sipariş: {order.id}")
        return order


# Bağımlılıklar açıkça görünüyor
service = OrderService(db=postgres_db, email=email_sender)

# Test — temiz, izole, global state yok
test_service = OrderService(db=MockDB(), email=MockEmail())
ÖzellikService LocatorDependency Injection
BağımlılıklarGizli (sınıf içinde)Açık (constructor'da)
TestGlobal state kirletirİzole, temiz
Hata zamanıRuntime (KeyError)Compile/init time
OkunabilirlikDüşükYüksek

💡 İpucu: "Bağımlılıklarım constructor'da mı, yoksa sınıfın içinde mi oluşturuluyor?" sorusunu sor. İçeride oluşturuluyorsa, ya hard-coded bağımlılık ya da Service Locator kullanıyorsundur — ikisi de DI'a dönüştürülebilir.


10. DI Uygulama Rehberi

Ne Zaman DI Kullanmalı?

DI her yerde kullanılmaz. Bazı durumlarda gereksiz karmaşıklık ekler:

# ❌ Gereksiz DI — basit utility fonksiyonu
class MathService:
    def __init__(self, adder, multiplier):
        self.adder = adder
        self.multiplier = multiplier
    
    def calculate(self, a, b):
        return self.adder.add(a, b)

# ✅ Basit tut
def calculate(a, b):
    return a + b

# ✅ DI kullan — dış servislerle etkileşim, I/O, state
class OrderService:
    def __init__(self, db, payment_gateway, notifier):
        self.db = db
        self.payment = payment_gateway
        self.notifier = notifier

DI kullan: Veritabanı, API, dosya sistemi, e-posta gibi dış bağımlılıklar olduğunda. DI kullanma: Saf hesaplama, string manipülasyonu gibi yan etkisi olmayan işlerde.

Composition Root

Tüm bağımlılıkların oluşturulup birbirine bağlandığı tek yer Composition Root olarak adlandırılır — genellikle uygulamanın giriş noktasıdır (main.py):

# main.py — Composition Root
def create_app():
    """Tüm bağımlılıkları oluştur ve bağla."""
    # Infrastructure
    db = PostgresDatabase("postgresql://localhost/mydb")
    cache = RedisCache("localhost:6379")
    email = SmtpEmailSender("smtp.gmail.com", 587)
    
    # Repositories
    user_repo = UserRepository(db=db, cache=cache)
    order_repo = OrderRepository(db=db)
    
    # Services
    user_service = UserService(repo=user_repo)
    order_service = OrderService(
        repo=order_repo,
        notifier=email,
        user_service=user_service
    )
    
    return order_service

# Test için ayrı Composition Root
def create_test_app():
    return OrderService(
        repo=InMemoryOrderRepo(),
        notifier=FakeEmailSender(),
        user_service=FakeUserService()
    )

Geri kalan tüm kod bağımlılıklarını constructor'dan alır — hiçbir yerde new Database() veya import settings ile doğrudan bağımlılık oluşturmaz.


Özet

  • Dependency Injection, bağımlılıkları sınıf içinde oluşturmak yerine dışarıdan verme prensibidir — test edilebilirlik ve esneklik sağlar

  • Constructor injection en yaygın ve önerilen DI biçimidir; nesnenin her zaman geçerli durumda olmasını garanti eder

  • ABC sıkı arayüz kontrolü, Protocol duck-typing uyumu sağlar — projenin ihtiyacına göre seç

  • DI Container bağımlılık ağacını otomatik çözümler; küçük projelerde gereksiz, büyük projelerde vazgeçilmez

  • FastAPI'nin Depends() sistemi web uygulamalarında zarif ve Pythonic DI sunar

  • Service Locator anti-pattern'dir — bağımlılıkları gizler, test ve okunabilirliği zorlaştırır; DI'ı tercih et