Menü

Neominal

BT Hizmetleri ve Danışmanlığı

5 Mayıs 20265 dk okuma17 görüntüleme
Part 5/5: Dependency Inversion Principle

Şöyle bir senaryo düşünelim. Sisteminiz PostgreSQL kullanıyor, her şey güzel çalışıyor. Bir gün karar verildi: "MongoDB'ye geçiyoruz." TransferOrchestrator sınıfını açıyorsunuz ve şunu görüyorsunuz:

class TransferOrchestrator {
    private PostgreSQLAccountRepository repo = new PostgreSQLAccountRepository();
    private KafkaEventPublisher          publisher = new KafkaEventPublisher();
    private RuleBasedFraudDetector       fraud = new RuleBasedFraudDetector();
}

TransferOrchestrator her somut sınıfı doğrudan içinde yaratmış. MongoDB'ye geçmek için iş mantığına dokunmak zorundasınız. Kafka'yı RabbitMQ ile değiştirmek için yine aynı şekilde iş mantığını değiştirmeliyiz.

Peki, bir iş mantığı sınıfı neden altyapı detaylarını bilsin ki?


Nedir bu prensip aslında?

Dependency Inversion Principle iki şey söylüyor:

Yüksek seviye modüller düşük seviye modüllere bağımlı olmamalı. Her ikisi de soyutlamalara bağımlı olmalı.

Sade bir dille: TransferOrchestrator PostgreSQL'i, Kafka'yı, ML modelini tanımamalı. Yalnızca arayüzleri tanımalı. Hangi somut sınıfın arkada çalıştığını umursamamalı.

Bunu sağladığınızda altyapı değişir ama iş mantığına tek satır dokunmazsınız. Üstelik test yazarken gerçek DB veya Kafka'ya ihtiyaç duymadan sahte implementasyonlarla rahatça çalışırsınız.


❌ Önce yanlışı görelim

TransferOrchestrator her somut sınıfı doğrudan yaratıyor:

class TransferOrchestrator {

    // Somut sınıflara doğrudan bağımlı — DIP ihlali
    private PostgreSQLAccountRepository repo
        = new PostgreSQLAccountRepository();

    private KafkaEventPublisher publisher
        = new KafkaEventPublisher();

    private RuleBasedFraudDetector fraud
        = new RuleBasedFraudDetector();

    void transfer(String fromId, String toId, BigDecimal amount) {
        if (fraud.isSuspicious(amount)) {
            publisher.publish("FRAUD_ALERT: " + amount);
            throw new IllegalStateException("Şüpheli işlem — reddedildi");
        }

        Account source = repo.findById(fromId);
        Account target = repo.findById(toId);

        source.debit(amount);
        target.credit(amount);

        repo.save(source);
        repo.save(target);
        publisher.publish("TRANSFER_OK: " + amount);
    }
}

Bu yapıda ne değişirse TransferOrchestrator'a dokunmak zorundasınız:

  • PostgreSQL → MongoDB geçişi? TransferOrchestrator değişir.

  • Kafka → RabbitMQ geçişi? TransferOrchestrator değişir.

  • Kural tabanlı fraud → ML fraud? TransferOrchestrator değişir.

  • Test yazmak istiyorsunuz? Gerçek DB ve Kafka ayağa kaldırmak zorundasınız.

İş mantığı altyapıya zincirlenmiş. İkisi birbirinden ayrılamıyor.


✅ Şimdi doğrusunu yapalım

Önce soyutlamalarımızı tanımlıyoruz — bunlar artık sistemin kontratları:


// Hesap deposu kontratı
interface AccountRepository {
    Account findById(String id);            // Hesap getir
    void save(Account account);             // Hesap kaydet
}

// Dolandırıcılık dedektörü kontratı
interface FraudDetector {
    boolean isSuspicious(BigDecimal amount); // Şüpheli mi?
}

// Olay yayıncısı kontratı
interface EventPublisher {
    void publish(String event);             // Olay yayınla
}

TransferOrchestrator artık yalnızca bu kontratları biliyor — arkada ne olduğunu bilmiyor:

// DIP: hiçbir somut sınıfı tanımıyor, sadece arayüzlere bağımlı
class TransferOrchestrator {
    private final AccountRepository repo;
    private final FraudDetector     fraud;
    private final EventPublisher    events;
    private final FeeEngine         feeEngine;
    private final AuditLogger       logger;

    // Constructor injection — bağımlılıklar dışarıdan veriliyor
    TransferOrchestrator(
            AccountRepository repo,
            FraudDetector     fraud,
            EventPublisher    events,
            FeeEngine         feeEngine,
            AuditLogger       logger) {
        this.repo      = repo;
        this.fraud     = fraud;
        this.events    = events;
        this.feeEngine = feeEngine;
        this.logger    = logger;
    }

    void transfer(String fromId, String toId,
                  BigDecimal amount, String feeType) {

        if (fraud.isSuspicious(amount)) {               // Fraud kontrolü
            events.publish("FRAUD_ALERT: " + amount);
            throw new IllegalStateException("Şüpheli işlem — reddedildi");
        }

        BigDecimal fee   = feeEngine.calculate(feeType, amount);
        BigDecimal total = amount.add(fee);

        Account source = repo.findById(fromId);
        Account target = repo.findById(toId);

        ((Debitable) source).debit(total);              // Gönderenden çek
        ((Creditable) target).credit(amount);            // Alıcıya yatır

        repo.save(source);
        repo.save(target);

        logger.log(fromId + " → " + toId + " : " + amount + " (ücret: " + fee + ")");
        events.publish("TRANSFER_OK: " + amount);
    }
}

Şimdi production ve test implementasyonlarını yazıyoruz:

// Production — gerçek veritabanı
class JpaAccountRepository implements AccountRepository {
    public Account findById(String id) {
        // DB sorgusu
        return null;
    }
    public void save(Account account) {
        // DB'ye yaz
    }
}

// Production — gerçek Kafka
class KafkaEventPublisher implements EventPublisher {
    public void publish(String event) {
        System.out.println("[KAFKA] " + event);
    }
}

// Production — ML tabanlı fraud dedektörü
class MLFraudDetector implements FraudDetector {
    public boolean isSuspicious(BigDecimal amount) {
        // ML modeli burada çalışır
        return amount.compareTo(new BigDecimal("50000")) > 0;
    }
}

// Test — DB gerektirmez, bellekte çalışır
class InMemoryAccountRepository implements AccountRepository {
    private final Map<String, Account> store = new HashMap<>();

    public void put(String id, Account account) { store.put(id, account); }

    public Account findById(String id) {
        if (!store.containsKey(id))
            throw new IllegalArgumentException("Hesap bulunamadı: " + id);
        return store.get(id);
    }

    public void save(Account account) { /* zaten bellekte */ }
}

// Test — her işlemi güvenli sayar
class AlwaysSafeFraudDetector implements FraudDetector {
    public boolean isSuspicious(BigDecimal amount) { return false; }
}

// Test — sadece konsola yazar
class LogEventPublisher implements EventPublisher {
    public void publish(String event) {
        System.out.println("[EVENT] " + event);
    }
}


Kullanalım

Production ortamı — gerçek DB ve Kafka:

TransferOrchestrator prod = new TransferOrchestrator(
    new JpaAccountRepository(),
    new MLFraudDetector(),
    new KafkaEventPublisher(),
    feeEngine,
    new AuditLogger()
);

Test ortamı — hiçbir altyapıya ihtiyaç yok:

InMemoryAccountRepository testRepo = new InMemoryAccountRepository();
testRepo.put("A1", new CheckingAccount(new BigDecimal("2000"), new BigDecimal("500")));
testRepo.put("A2", new SavingsAccount(new BigDecimal("1000")));

FeeEngine feeEngine = new FeeEngine();
feeEngine.register("STANDARD", new StandardFee());

TransferOrchestrator test = new TransferOrchestrator(
    testRepo,
    new AlwaysSafeFraudDetector(),     // DB yok, Kafka yok
    new LogEventPublisher(),            // gerçek altyapı yok
    feeEngine,
    new AuditLogger()
);

test.transfer("A1", "A2", new BigDecimal("300"), "STANDARD");

// [LOG]   A1 → A2 : 300 (ücret: 0.30)
// [EVENT] TRANSFER_OK: 300

System.out.println(testRepo.findById("A1").getBalance()); // 1699.70
System.out.println(testRepo.findById("A2").getBalance()); // 1300.00

MongoDB'ye geçiş senaryosu

Karar verildi, PostgreSQL'den MongoDB'ye geçiyorsunuz. Tek yapmanız gereken:

// Yeni implementasyon — başka hiçbir şeye dokunmadık
class MongoAccountRepository implements AccountRepository {
    public Account findById(String id) {
        // MongoDB sorgusu
        return null;
    }
    public void save(Account account) {
        // MongoDB'ye yaz
    }
}

// Production'da sadece bu satır değişti
TransferOrchestrator prod = new TransferOrchestrator(
    new MongoAccountRepository(),      // ← tek değişiklik
    new MLFraudDetector(),
    new KafkaEventPublisher(),
    feeEngine,
    new AuditLogger()
);

TransferOrchestrator'a dokunmadınız. MLFraudDetector'a dokunmadınız. KafkaEventPublisher'a dokunmadınız. Mevcut testleriniz hâlâ yeşil.

Kısaca

Yüksek seviye iş mantığı altyapı detaylarını bilmemeli. Arayüzler bu ikisi arasındaki duvarı oluşturuyor. Duvar sayesinde her iki taraf bağımsızca değişebiliyor.

Seriyi tamamladık

Beş bölüm boyunca tek bir banking modülü üzerinden SOLID'in her prensibini gördük:

S — Her sınıf tek iş yapar. E-posta şablonu değişince Account'a dokunmayız.

O — Yeni ücret modeli için FeeEngine'e dokunmayız, yeni bir strateji sınıfı yazarız.

LSavingsAccount ve CheckingAccount her yerde Account yerine sorunsuz geçer.

IInvestmentAccount kullanmadığı metodları implement etmek zorunda kalmaz.

DTransferOrchestrator PostgreSQL'i, Kafka'yı, ML modelini tanımaz. Yalnızca arayüzleri tanır.

Bu prensipler tek tek de değerli ama hepsi birlikte uygulandığında asıl gücünü gösteriyor. Değişim izole kalıyor, test kolaylaşıyor, ekip büyüdükçe çakışmalar azalıyor.

Umarım bu seri işe yarar. Sorularınız varsa yorumlarda buluşalım.