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ı:
Gmail'den SendGrid'e geçmek istesek?
GmailEmailService→SendGridEmailServicedeğişikliğiOrderService'in kodunda yapılmalıTest yazarken gerçek ödeme almak istemiyoruz. Ama
StripePaymentServicehard-codedFarklı ortamlarda farklı servisler kullanmak istesek? Her ortam için kodu değiştirmek gerekir
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
EmailServiceinterface'ini bilirTest'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 →
ReportServicekodunu değiştirmemiz gerekirTest 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 ait4. 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:
Bean oluşturma: Nesneleri yaratır (instantiation)
Bağımlılık çözme: Hangi bean'in kime enjekte edileceğini belirler
Yaşam döngüsü yönetimi: Başlatma ve sonlandırma callback'leri
Konfigürasyon: Properties dosyalarından değer okuma
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);| Özellik | BeanFactory | ApplicationContext |
|---|---|---|
| Bean initialization | Lazy (istendiğinde) | Eager (başlangıçta) |
| Event system | ❌ | ✅ |
| AOP desteği | Sınırlı | ✅ Tam |
| i18n desteği | ❌ | ✅ |
| BeanPostProcessor | Manuel kayıt | Otomatik |
| Kullanım | Neredeyse 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şturulurIoC 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
| Fayda | Açıklama | Örnek |
|---|---|---|
| Testability | Mock nesneler kolayca enjekte edilebilir | Gerçek DB yerine mock repository |
| Modularity | Bileşenler bağımsız geliştirilebilir | Email servisi ayrı, payment servisi ayrı |
| Maintainability | Bir değişiklik diğer sınıfları etkilemez | MySQL → PostgreSQL geçişi sadece config'te |
| Flexibility | İmplementasyonlar runtime'da değiştirilebilir | Dev'de mock, prod'da gerçek servis |
| Separation of Concerns | Her sınıf sadece kendi işine odaklanır | OrderService ödeme detaylarını bilmez |
| Open/Closed Principle | Mevcut kodu değiştirmeden yeni özellik eklenebilir | Yeni 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
newkeyword ile Spring bean'i oluşturmayın — container'ın yönetmesine izin verinIoC'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 →
PaymentGatewayinterface'i üzerinden çalışırBildirim nasıl gönderilir bilmez →
NotificationSenderinterface'i üzerinden çalışırStok nasıl yönetilir bilmez →
StockManagerinterface'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/Dil | IoC Container |
|---|---|
| Spring (Java) | ApplicationContext |
| Google Guice (Java) | Injector |
| CDI / Jakarta EE | Weld, 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.
AI Asistan
Sorularını yanıtlamaya hazır