← Kursa Dön
📄 Text · 15 min

Inversion of Control (IoC) Prensibi

Giriş

Yazılım geliştirmenin en büyük düşmanı sıkı bağımlılık (tight coupling)'tır. Bir sınıf, kullandığı bağımlılığın somut tipini bildiğinde, o bağımlılık değiştiğinde tüm kod etkilenir. Test yazmak zorlaşır, yeni özellik eklemek riskli hale gelir, bakım maliyeti artar. Inversion of Control (IoC) — Kontrolün Tersine Çevrilmesi — bu sorunu kökten çözen yazılım tasarımının en temel prensiplerinden biridir ve Spring Framework'ün tüm yapısının üzerine inşa edildiği kavramdır.

Gerçek Dünya Analojisi

Bir restorana gittiğinizi düşünün. IoC olmadan: Mutfağa girip kendiniz malzemeleri seçer, pişirir ve tabağı hazırlarsınız. IoC ile: Garsona siparişinizi verirsiniz, mutfak sizin yerinize yemeği hazırlar ve size servis eder. Kontrolü siz değil, restoran yönetir. Siz sadece "ne istediğinizi" söylersiniz, "nasıl yapılacağı" restoranın sorumluluğundadır.

Bu kavram Hollywood Principle olarak da bilinir: *"Don't call us, we'll call you."* — Bizi aramayın, biz sizi ararız.


IoC Nedir? — Kontrolün Ters Çevrilmesi

Geleneksel Yaklaşım (IoC Olmadan)

Geleneksel programlamada, siz kütüphaneyi/framework'ü çağırırsınız. Ve nesneler kendi bağımlılıklarını kendileri oluşturur:

// ===== GELENEKSEL YAKLAŞIM — Nesne kendi bağımlılığını oluşturur =====
public class OrderService {
    // Somut sınıflara doğrudan bağımlı → TIGHT COUPLING
    private final EmailService emailService = new GmailEmailService();
    private final InventoryService inventoryService = new WarehouseInventoryService();
    private final PaymentService paymentService = new StripePaymentService();

    public void placeOrder(Order order) {
        paymentService.charge(order.getTotal());
        inventoryService.decreaseStock(order.getItems());
        emailService.sendConfirmation(order);
    }
}

Bu kodun sorunları:

  1. Gmail'den SendGrid'e geçmek istesek? GmailEmailServiceSendGridEmailService değişikliği OrderService'in kodunda yapılmalı

  2. Test yazarken gerçek ödeme almak istemiyoruz. Ama StripePaymentService hard-coded

  3. Farklı ortamlarda farklı servisler kullanmak istesek? Her ortam için kodu değiştirmek gerekir

  4. OrderService, kendi işi olmayan nesne oluşturma sorumluluğunu üstlenmiş

IoC Yaklaşımı

IoC'de kontrol tersine çevrilir: Nesneler bağımlılıklarını kendileri oluşturmaz, dışarıdan alır:

// ===== IoC YAKLAŞIMI — Bağımlılıklar dışarıdan verilir =====
public class OrderService {
    // Interface'lere bağımlı → LOOSE COUPLING
    private final EmailService emailService;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    // Bağımlılıklar constructor ile dışarıdan enjekte edilir
    public OrderService(EmailService emailService,
                        InventoryService inventoryService,
                        PaymentService paymentService) {
        this.emailService = emailService;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }

    public void placeOrder(Order order) {
        paymentService.charge(order.getTotal());
        inventoryService.decreaseStock(order.getItems());
        emailService.sendConfirmation(order);
    }
}

Artık OrderService:

  • Gmail mı SendGrid mi kullanıldığını bilmez — sadece EmailService interface'ini bilir

  • Test'te mock nesneler verilebilir — gerçek ödeme almaya gerek yok

  • Farklı ortamlarda farklı implementasyonlar enjekte edilebilir


Tight Coupling vs Loose Coupling

Tight Coupling (Sıkı Bağlılık) — Kötü Tasarım

public class ReportService {
    // Doğrudan MySQL'e bağımlı!
    private MySqlDatabase database = new MySqlDatabase();

    public Report generate() {
        var data = database.query("SELECT * FROM sales");
        return new Report(data);
    }
}

Sorunlar:

  • PostgreSQL'e geçmek istesek → ReportService kodunu değiştirmemiz gerekir

  • Test yazarken → gerçek MySQL veritabanı lazım (yavaş, kırılgan)

  • Her yeni veritabanı desteği → ReportService'te değişiklik

Bunu bir priz ve fiş analojisiyle düşünün: Tight coupling, elektrik kablosunu direkt duvara lehimlemek gibidir. Cihaz değiştirecekseniz duvarı sökmek zorundasınız.

Loose Coupling (Gevşek Bağlılık) — İyi Tasarım

// Interface tanımı — sözleşme
public interface Database {
    List<Map<String, Object>> query(String sql);
}

// MySQL implementasyonu
public class MySqlDatabase implements Database {
    @Override
    public List<Map<String, Object>> query(String sql) {
        // MySQL'e özel bağlantı ve sorgu
        return mysqlConnection.execute(sql);
    }
}

// PostgreSQL implementasyonu
public class PostgresDatabase implements Database {
    @Override
    public List<Map<String, Object>> query(String sql) {
        // PostgreSQL'e özel bağlantı ve sorgu
        return pgConnection.execute(sql);
    }
}

// ReportService artık interface'e bağımlı
public class ReportService {
    private final Database database; // Hangi DB olduğunu bilmez

    public ReportService(Database database) {
        this.database = database;
    }

    public Report generate() {
        var data = database.query("SELECT * FROM sales");
        return new Report(data);
    }
}

Loose coupling, standart bir elektrik prizi gibidir: Herhangi bir cihazı takabilir, istediğiniz zaman çıkarabilirsiniz. Duvarı sökmenize gerek yok.


IoC'nin Farklı Formları

IoC geniş bir kavramdır ve birçok formu vardır:

1. Dependency Injection (DI) — En Yaygın

Bağımlılıklar dışarıdan enjekte edilir:

// Constructor Injection
public OrderService(PaymentService paymentService) {
    this.paymentService = paymentService;
}

2. Event-Driven (Olay Tabanlı)

Kontrol akışı olaylara tepki olarak tersine çevrilir:

@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
    // Bu metot, OrderCreatedEvent yayınlandığında Spring tarafından çağrılır
    emailService.sendConfirmation(event.getOrder());
}

3. Template Method Pattern

Framework, algoritmayı tanımlar; siz sadece belirli adımları implement edersiniz:

// JdbcTemplate — SQL çalıştırma framework'ü
jdbcTemplate.query("SELECT * FROM users",
    (rs, rowNum) -> new User(rs.getString("name"), rs.getString("email"))
);
// Siz sadece row mapping yaparsınız, bağlantı yönetimi Spring'e ait

4. Service Locator Pattern

Bir registry'den servisi çekersiniz (DI'ya göre daha az tercih edilir):

// Service locator — bean'i runtime'da iste
ApplicationContext ctx = ...;
EmailService emailService = ctx.getBean(EmailService.class);

Spring'de en yaygın kullanılan form Dependency Injection'dır — bir sonraki derste detaylıca ele alacağız.


Spring IoC Container

Spring'in IoC Container'ı, tüm bean'lerin (nesnelerin) yaşam döngüsünü yöneten merkezdir:

  1. Bean oluşturma: Nesneleri yaratır (instantiation)

  2. Bağımlılık çözme: Hangi bean'in kime enjekte edileceğini belirler

  3. Yaşam döngüsü yönetimi: Başlatma ve sonlandırma callback'leri

  4. Konfigürasyon: Properties dosyalarından değer okuma

  5. AOP: Cross-cutting concern'leri (logging, transaction) yönetme

BeanFactory vs ApplicationContext

Spring'de iki temel IoC container vardır:

// BeanFactory — En temel container
// - Lazy initialization (bean ilk istendiğinde oluşturulur)
// - Düşük kaynak tüketimi
// - Nadiren doğrudan kullanılır
BeanFactory factory = new DefaultListableBeanFactory();

// ApplicationContext — BeanFactory'nin genişletilmiş versiyonu
// - Eager initialization (uygulama başlarken tüm singleton bean'ler oluşturulur)
// - Event publishing, AOP, internationalization (i18n) desteği
// - HER ZAMAN BU KULLANILIR
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
ÖzellikBeanFactoryApplicationContext
Bean initializationLazy (istendiğinde)Eager (başlangıçta)
Event system
AOP desteğiSınırlı✅ Tam
i18n desteği
BeanPostProcessorManuel kayıtOtomatik
KullanımNeredeyse hiçHer zaman

ApplicationContext Türleri

// 1. Annotation-based (modern, en yaygın)
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);

// 2. Spring Boot'ta otomatik oluşturulur
@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(MyApp.class, args);
        // ctx kullanıma hazır — tüm bean'ler yüklendi
        UserService userService = ctx.getBean(UserService.class);
    }
}

// 3. Web uygulamalarında
// WebApplicationContext → Servlet context ile entegre
// Spring Boot'ta otomatik oluşturulur

IoC Container Nasıl Çalışır?

Spring Boot uygulaması başladığında şu adımlar izlenir:

1. @SpringBootApplication taranır
   ↓
2. @ComponentScan ile bean adayları bulunur
   (@Component, @Service, @Repository, @Controller)
   ↓
3. @Configuration sınıflarındaki @Bean metotları taranır
   ↓
4. Bean definition'lar oluşturulur (henüz nesne yok)
   ↓
5. BeanFactoryPostProcessor'lar çalışır
   (property placeholder çözümleme, vb.)
   ↓
6. Bean'ler oluşturulur (instantiation)
   ↓
7. Dependency Injection yapılır
   ↓
8. BeanPostProcessor'lar çalışır
   (@Autowired çözümleme, AOP proxy, @Scheduled, vb.)
   ↓
9. Initialization callback'ler çalışır
   (@PostConstruct, InitializingBean, initMethod)
   ↓
★ ★ ★  APPLICATION READY  ★ ★ ★

IoC Olmadan vs IoC İle — Tam Karşılaştırma

// ===== IoC OLMADAN — Tightly coupled, test edilemez =====
public class UserController {
    // Her bağımlılık somut sınıfa bağlı
    private UserService userService = new UserService(
        new MySqlUserRepository(
            new HikariDataSource("jdbc:mysql://localhost:3306/mydb")
        ),
        new BcryptPasswordEncoder(12),
        new SmtpEmailService("smtp.gmail.com", 587)
    );
    // Sorunlar:
    // - UserController, veritabanı URL'ini bilmek zorunda
    // - Test'te gerçek MySQL ve SMTP sunucusu gerekli
    // - Herhangi bir değişiklik zincirleme etki yaratır
}

// ===== IoC İLE — Loosely coupled, test edilebilir =====
@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService userService; // Spring enjekte eder

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

// Test:
@Test
void shouldGetUser() {
    var mockService = mock(UserService.class);
    when(mockService.findById(1L)).thenReturn(new User("Ali"));

    var controller = new UserController(mockService);
    User user = controller.getUser(1L);

    assertEquals("Ali", user.getName());
    // Veritabanı yok, SMTP yok, Spring context yok — saf birim testi!
}

IoC'nin Sağladığı Faydalar

FaydaAçıklamaÖrnek
TestabilityMock nesneler kolayca enjekte edilebilirGerçek DB yerine mock repository
ModularityBileşenler bağımsız geliştirilebilirEmail servisi ayrı, payment servisi ayrı
MaintainabilityBir değişiklik diğer sınıfları etkilemezMySQL → PostgreSQL geçişi sadece config'te
Flexibilityİmplementasyonlar runtime'da değiştirilebilirDev'de mock, prod'da gerçek servis
Separation of ConcernsHer sınıf sadece kendi işine odaklanırOrderService ödeme detaylarını bilmez
Open/Closed PrincipleMevcut kodu değiştirmeden yeni özellik eklenebilirYeni NotificationService implementasyonu

Yaygın Hatalar ve Çözümleri

Hata 1: Somut Sınıfa Bağımlılık

// ❌ YANLIŞ — Somut sınıfa bağımlı
@Service
public class NotificationService {
    private final GmailEmailSender emailSender = new GmailEmailSender();
}

// ✅ DOĞRU — Interface'e bağımlı
@Service
public class NotificationService {
    private final EmailSender emailSender; // Interface

    public NotificationService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }
}

Hata 2: new Keyword ile Bean Oluşturma

// ❌ YANLIŞ — Spring container bypass ediliyor
@Service
public class OrderService {
    public void process() {
        // Spring bean'i new ile oluşturuyorsunuz → DI çalışmaz!
        EmailService emailService = new EmailService();
        emailService.send("..."); // @Autowired alanları null!
    }
}

// ✅ DOĞRU — Inject edin
@Service
public class OrderService {
    private final EmailService emailService;

    public OrderService(EmailService emailService) {
        this.emailService = emailService; // Spring tarafından yönetilen bean
    }
}

Hata 3: ApplicationContext'i Her Yerde Kullanmak

// ❌ YANLIŞ — Service Locator anti-pattern
@Service
public class OrderService {
    @Autowired
    private ApplicationContext ctx;

    public void process() {
        EmailService email = ctx.getBean(EmailService.class); // DI kullanın!
    }
}

// ✅ DOĞRU — Constructor injection
@Service
public class OrderService {
    private final EmailService emailService;

    public OrderService(EmailService emailService) {
        this.emailService = emailService;
    }
}

Özet

  • IoC, nesne oluşturma ve bağımlılık yönetimi kontrolünü geliştiriciden framework'e devreder

  • Tight coupling → somut sınıfa bağımlılık (kötü), Loose coupling → interface'e bağımlılık (iyi)

  • Spring'in ApplicationContext'i IoC prensibini uygulayan güçlü bir container'dır

  • IoC sayesinde kodunuz test edilebilir, modüler, bakımı kolay ve esnek olur

  • Her zaman interface'lere bağımlı olun, somut sınıflara değil

  • new keyword ile Spring bean'i oluşturmayın — container'ın yönetmesine izin verin

  • IoC'nin en yaygın implementasyonu Dependency Injection'dır (sonraki ders)


Bütünleşik Gerçek Dünya Örneği: Sipariş İşleme Sistemi

IoC prensiplerini tam olarak uygulayan kapsamlı bir örnek:

// === Interface'ler — Sözleşmeler ===
public interface PaymentGateway {
    PaymentResult charge(BigDecimal amount, String currency, String token);
}

public interface NotificationSender {
    void send(String recipient, String subject, String body);
}

public interface StockManager {
    boolean reserveStock(Long productId, int quantity);
    void releaseStock(Long productId, int quantity);
}

// === Implementasyonlar ===
@Service
@Profile("prod")
public class StripePaymentGateway implements PaymentGateway {
    private final String apiKey;

    public StripePaymentGateway(@Value("${stripe.api-key}") String apiKey) {
        this.apiKey = apiKey;
    }

    @Override
    public PaymentResult charge(BigDecimal amount, String currency, String token) {
        // Stripe API çağrısı
        return new PaymentResult(true, "ch_" + UUID.randomUUID());
    }
}

@Service
@Profile("dev")
public class MockPaymentGateway implements PaymentGateway {
    @Override
    public PaymentResult charge(BigDecimal amount, String currency, String token) {
        System.out.println("💳 [MOCK] Ödeme: " + amount + " " + currency);
        return new PaymentResult(true, "MOCK-" + System.currentTimeMillis());
    }
}

// === Ana Service — Bağımlılıkları bilmez ===
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderProcessingService {
    private final PaymentGateway paymentGateway;       // Stripe? Mock? Bilmez.
    private final NotificationSender notificationSender; // Email? SMS? Bilmez.
    private final StockManager stockManager;             // DB? Redis? Bilmez.
    private final OrderRepository orderRepository;

    @Transactional
    public Order processOrder(OrderRequest request) {
        // 1. Stok kontrol
        boolean stockReserved = stockManager.reserveStock(
            request.getProductId(), request.getQuantity());
        if (!stockReserved) {
            throw new InsufficientStockException("Stok yetersiz");
        }

        try {
            // 2. Ödeme al
            PaymentResult payment = paymentGateway.charge(
                request.getTotalAmount(), "TRY", request.getPaymentToken());
            if (!payment.isSuccess()) {
                stockManager.releaseStock(request.getProductId(), request.getQuantity());
                throw new PaymentFailedException("Ödeme başarısız");
            }

            // 3. Sipariş kaydet
            Order order = new Order(request, payment.getTransactionId());
            orderRepository.save(order);

            // 4. Bildirim gönder
            notificationSender.send(
                request.getCustomerEmail(),
                "Sipariş Onayı",
                "Siparişiniz #" + order.getId() + " başarıyla oluşturuldu."
            );

            return order;
        } catch (Exception e) {
            stockManager.releaseStock(request.getProductId(), request.getQuantity());
            throw e;
        }
    }
}

Bu örnekte OrderProcessingService:

  • Ödeme nasıl alınır bilmez → PaymentGateway interface'i üzerinden çalışır

  • Bildirim nasıl gönderilir bilmez → NotificationSender interface'i üzerinden çalışır

  • Stok nasıl yönetilir bilmez → StockManager interface'i üzerinden çalışır

Bu sayede:

  • Test'te tüm bağımlılıklar mock'lanabilir → hızlı, güvenilir birim testi

  • Dev ortamında mock implementasyonlar, prod'da gerçek servisler kullanılır

  • Yeni bir ödeme sağlayıcısı eklemek mevcut kodu değiştirmez


IoC Dışındaki Dünyada Ne Var?

IoC sadece Spring'e özgü değildir. Birçok modern framework IoC prensibini benimser:

Framework/DilIoC Container
Spring (Java)ApplicationContext
Google Guice (Java)Injector
CDI / Jakarta EEWeld, OpenWebBeans
ASP.NET Core (C#)Built-in DI Container
Angular (TypeScript)Hierarchical Injector
NestJS (TypeScript)Module-based DI

IoC, dile veya framework'e bağlı bir konsept değildir — evrensel bir yazılım tasarım prensibidir. Spring onu en olgun ve en kapsamlı şekilde uygulayan framework'tür.