
Zelkulon14 Mart 20251 dk okuma
Robert C. Martin'in Clean Architecture'ı Java/Spring Boot'ta tutarlı şekilde uygulandı: katmanlar, bağımlılıklar ve domain'in neden Spring'den habersiz olması gerektiği.
Dört Katman
┌──────────────────────────────────┐
│ Frameworks & Drivers │ (Spring, JPA, REST)
│ ┌─────────────────────────────┐ │
│ │ Interface Adapters │ │ (Controller'lar, Repository'ler)
│ │ ┌───────────────────────┐ │ │
│ │ │ Application/UseCases│ │ │ (İş Akışı)
│ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ Domain │ │ │ │ (Varlıklar, Kurallar)
│ │ │ └─────────────────┘ │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────┘
Bağımlılık kuralı: oklar HER ZAMAN içe doğru gösterir.
Domain, Spring'i de JPA'yı da tanımaz.Proje Yapısı
order-service/
└── src/main/java/com/zelkulon/order/
├── domain/ ← En iç katman (Spring yok!)
│ ├── model/
│ │ └── Order.java
│ └── valueobject/
│ └── Money.java
│
├── application/ ← Use Case'ler
│ ├── port/
│ │ ├── in/ ← Gelen portlar
│ │ │ └── PlaceOrderUseCase.java
│ │ └── out/ ← Giden portlar
│ │ └── OrderRepository.java
│ └── service/
│ └── PlaceOrderService.java
│
└── adapter/ ← En dış katman
├── in/web/
│ └── OrderController.java
└── out/persistence/
├── OrderJpaEntity.java ← Yalnızca burada JPA!
└── OrderPersistenceAdapter.javaDomain: Spring'siz
// Spring anotasyonu YOK, JPA YOK!
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderItem> items;
private OrderStatus status;
public Order(OrderId id, CustomerId customerId, List<OrderItem> items) {
if (items == null || items.isEmpty())
throw new OrderException("Mindestens ein Artikel nötig");
this.id = id;
this.items = List.copyOf(items);
this.status = OrderStatus.PENDING;
}
// Domain mantığı domain'de olmalı!
public void confirm() {
if (this.status != OrderStatus.PENDING)
throw new OrderException("Nur PENDING kann bestätigt werden");
this.status = OrderStatus.CONFIRMED;
}
}
// Value Object (Record)
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Betrag darf nicht negativ sein");
}
public Money add(Money other) {
return new Money(amount.add(other.amount), currency);
}
}Port'lar ve Use Case
// Gelen port (Use Case Arayüzü)
public interface PlaceOrderUseCase {
OrderId placeOrder(PlaceOrderCommand command);
}
// Use Case Uygulaması
@UseCase // özel anotasyon, @Service'ten anlamlı
@Transactional
@RequiredArgsConstructor
public class PlaceOrderService implements PlaceOrderUseCase {
private final OrderRepository orderRepository; // Arayüz!
private final PaymentGateway paymentGateway; // Arayüz!
@Override
public OrderId placeOrder(PlaceOrderCommand command) {
var order = new Order(
OrderId.newId(), command.customerId(), mapItems(command)
);
OrderId saved = orderRepository.save(order);
paymentGateway.initiatePayment(order);
return saved;
}
}Bağımlılık Tersine Çevirme
Clean Architecture olmadan: Clean Architecture ile:
Controller Controller
│ │
▼ ▼ (uygular)
OrderService ◄── JPA/DB PlaceOrderUseCase (Port)
│ ▲ (uygular)
▼ │
JpaRepository PlaceOrderService
│
OrderRepository (Port)
▲ (uygular)
│
OrderPersistenceAdapter
│
JpaRepositoryÖzet
- Test edilebilirlik: Domain ve use case'ler Spring'siz test edilebilir (saf JUnit)
- Sürdürülebilirlik: Veritabanı değişiklikleri domain'i etkilemez
- Değiştirilebilirlik: Framework göçü domain değişikliği gerektirmez
- Okunabilirlik: Her sınıfın net bir sorumluluğu vardır
- Uzun ömür: İyi yapılandırılmış projeler yıllarca sürdürülebilir kalır