Product Entity - Stok Kontrolü Dahil
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
private int stockQuantity;
public void decreaseStock(int quantity) {
if (stockQuantity < quantity) {
throw new IllegalStateException("Not enough stock");
}
stockQuantity -= quantity;
}
public void increaseStock(int quantity) {
stockQuantity += quantity;
}
// Diğer alanlar ve metodlar
}
Order Entity - Sipariş Durumu Değişiklikleri Dahil
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderLine> orderLines;
public void place() {
if (status != OrderStatus.NEW) {
throw new IllegalStateException("Can only place a new order");
}
status = OrderStatus.PLACED;
}
public void ship() {
if (status != OrderStatus.PLACED) {
throw new IllegalStateException("Can only ship a placed order");
}
status = OrderStatus.SHIPPED;
for (OrderLine line : orderLines) {
line.getProduct().decreaseStock(line.getQuantity());
}
}
public void cancel() {
if (status != OrderStatus.PLACED) {
throw new IllegalStateException("Can only cancel a placed order");
}
status = OrderStatus.CANCELLED;
for (OrderLine line : orderLines) {
line.getProduct().increaseStock(line.getQuantity());
}
}
// Diğer alanlar ve metodlar
}
Bu, daha zengin bir domain modeli oluşturur. İş mantığı, servis katmanından alınıp model nesnelerine yerleştirilmiştir. Bu yaklaşım, daha temiz bir kod ve model içerisinde daha çok iş mantığını ifade eder.
Tamam, bu zengin domain modeli etrafında servis ve repository katmanlarını oluşturalım.
Repository Katmanı
Önceki açıklamalarda zaten tanımlanan repository sınıflarını kullanabiliriz (ProductRepository, OrderRepository vb.). Fakat bu modelde iş mantığı daha çok domain katmanında olduğu için, repository'nin rolü veritabanı işlemlerini gerçekleştirmekle sınırlıdır.
Servis Katmanı
Zengin domain modeliyle birlikte servis katmanının karmaşıklığı azalır. İş mantığı artık modelde yer aldığı için servis katmanı daha çok depolama ve altyapı işlevleriyle ilgilenir.
ProductService
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public Product findById(Long id) {
return productRepository.findById(id).orElse(null);
}
public Product save(Product product) {
return productRepository.save(product);
}
}
OrderService
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductService productService;
public List<Order> findAll() {
return orderRepository.findAll();
}
public Order findById(Long id) {
return orderRepository.findById(id).orElse(null);
}
public Order placeOrder(Order order) {
for (OrderLine line : order.getOrderLines()) {
Product product = productService.findById(line.getProduct().getId());
if (product == null) {
throw new IllegalArgumentException("Product not found");
}
product.decreaseStock(line.getQuantity());
productService.save(product);
}
order.place();
return orderRepository.save(order);
}
public void shipOrder(Long orderId) {
Order order = findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
order.ship();
orderRepository.save(order);
}
public void cancelOrder(Long orderId) {
Order order = findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
for (OrderLine line : order.getOrderLines()) {
Product product = productService.findById(line.getProduct().getId());
product.increaseStock(line.getQuantity());
productService.save(product);
}
order.cancel();
orderRepository.save(order);
}
}
Bu yapıda, OrderService ve ProductService sınıfları daha sade bir yapıya sahip. Modelde bulunan iş mantığı sayesinde servisler daha az sorumluluğa sahip.
Controller Katmanı
Controller katmanı da önceki şekilde kalabilir. Fakat artık bazı endpoint'lerde doğrudan servislerin yerine modelde tanımlanan iş mantığına dayalı olarak işlemleri gerçekleştirebilirsiniz.
Sonuç olarak, bu yaklaşımla, iş mantığı daha çok domain katmanında bulunurken, servis ve controller katmanları daha az karmaşık ve daha az işlevsellik taşır. Bu sayede kodun okunabilirliği, bakımı ve genişletilmesi daha kolay olur.
Bu aşamadan sonra eklememi veya açıklamamı istediğiniz başka bir konu var mı?
Bu uygulamaya adaptörler ve portlar kullanarak apache kafka entegre edebilir misin?
Elbette, Domain-Driven Design (DDD) yaklaşımında kullanılan hexagonal architecture (ya da ports and adapters architecture) sayesinde, uygulamanın çekirdeğini (domain) dış dünyadan (örneğin, Kafka gibi mesajlaşma sistemlerinden) izole edebiliriz.
Apache Kafka, veri yayını ve tüketimi için kullanılan açık kaynaklı bir stream-processing yazılım platformudur. Bu örnek için, sipariş durumlarının değişikliklerini Kafka'ya göndermek ve bu değişiklikler üzerinde dinleyiciler oluşturmak istediğimizi varsayalım.
1. Ports Tanımlama
Portlar, uygulamanın iç ve dış dünyası arasında bir sınır oluşturur. İlk olarak, bir port tanımlayarak bu sınırı ifade edelim:
public interface OrderStatusChangedPort {
void orderStatusChanged(Long orderId, OrderStatus status);
}
Bu port, sipariş durumu değiştiğinde çağrılacak olan metodu tanımlar.
2. Kafka Adapter Oluşturma
Bu port için Kafka ile entegrasyon sağlayan bir adapter yazmalıyız. Bu adapter, Kafka'ya mesaj gönderecek olan bileşendir.
@Service
public class KafkaOrderStatusChangedAdapter implements OrderStatusChangedPort {
private final KafkaTemplate<String, String> kafkaTemplate;
@Value("${kafka.topic.order-status}")
private String orderStatusTopic;
@Autowired
public KafkaOrderStatusChangedAdapter(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public void orderStatusChanged(Long orderId, OrderStatus status) {
String message = "Order ID: " + orderId + ", Status: " + status.name();
kafkaTemplate.send(orderStatusTopic, message);
}
}
3. Domain Katmanında Port Kullanma
OrderService içerisinde, sipariş durumu değiştikçe port'u kullanarak bu durumu dış dünyaya bildireceğiz:
@Service
public class OrderService {
@Autowired
private OrderStatusChangedPort orderStatusChangedPort;
// Diğer metotlar
public void shipOrder(Long orderId) {
Order order = findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
order.ship();
orderRepository.save(order);
orderStatusChangedPort.orderStatusChanged(orderId, OrderStatus.SHIPPED);
}
public void cancelOrder(Long orderId) {
Order order = findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
order.cancel();
orderRepository.save(order);
orderStatusChangedPort.orderStatusChanged(orderId, OrderStatus.CANCELLED);
}
}
4. Kafka Konfigürasyonu
Son olarak, Kafka'yı Spring uygulamanızda kullanmak için gerekli konfigürasyonları eklemelisiniz. application.properties dosyasına şunları ekleyebilirsiniz:
spring.kafka.bootstrap-servers=localhost:9092
kafka.topic.order-status=order-status-topic
Ve bir KafkaConfig sınıfı oluşturarak gereken bean'leri tanımlayabilirsiniz:
@Configuration
public class KafkaConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
}
Bu yapı, Apache Kafka ile entegre bir şekilde sipariş durumu değişikliklerini izlemenizi sağlar. Bu durum değişikliklerine dinleyiciler eklemek veya daha fazla entegrasyon oluşturmak da mümkündür.
Bu yaklaşımın güzel yanı, Kafka ile entegrasyonun tamamen izole edilmesi ve domain katmanının bu ayrıntılardan haberdar olmamasıdır. Bu, kodun daha temiz, bakımı daha kolay ve değişikliklere karşı daha esnek olmasına yardımcı olur.
Projeye port ve adaptör ile Hazelcast cache ekle
Tabii ki! Hazelcast, dağıtık bir veri ızgarası ve önbellekleme çözümüdür. Hexagonal mimaride, Hazelcast ile entegrasyonu port ve adaptörler aracılığıyla yapabiliriz. Bu sayede, iş mantığınızın önbellekleme mantığından tamamen izole edilmesi sağlanır.
1. Cache Portu Tanımlama
İlk olarak, önbellekleme işlemlerini tanımlayacak bir port oluşturmalıyız. Bu port, önbelleğe erişmek istediğimiz işlemleri ifade eder.
public interface CachePort {
<T> T get(String key);
void put(String key, Object value);
void remove(String key);
}
2. Hazelcast Cache Adaptörü Oluşturma
Bu port için Hazelcast ile entegrasyon sağlayan bir adaptör yazmalıyız. Adaptör, portun metotlarını Hazelcast'a özgü çağrılara dönüştürür.
@Service
public class HazelcastCacheAdapter implements CachePort {
private final HazelcastInstance hazelcastInstance;
@Autowired
public HazelcastCacheAdapter(HazelcastInstance hazelcastInstance) {
this.hazelcastInstance = hazelcastInstance;
}
@Override
public <T> T get(String key) {
return (T) hazelcastInstance.getMap("default").get(key);
}
@Override
public void put(String key, Object value) {
hazelcastInstance.getMap("default").put(key, value);
}
@Override
public void remove(String key) {
hazelcastInstance.getMap("default").remove(key);
}
}
3. Hazelcast Konfigürasyonu
HazelcastInstance bean'ini oluşturmak için bir konfigürasyon sınıfı eklemeliyiz:
@Configuration
public class HazelcastConfig {
@Bean
public Config hazelCastConfig() {
Config config = new Config();
config.setInstanceName("hazelcast-instance")
.addMapConfig(new MapConfig()
.setName("default")
.setTimeToLiveSeconds(20));
return config;
}
@Bean
public HazelcastInstance hazelcastInstance(Config hazelCastConfig) {
return Hazelcast.newHazelcastInstance(hazelCastConfig);
}
}
4. Servis Katmanında Cache Kullanma
Şimdi, önbellek portunu servis katmanında kullanabiliriz. Örnek olarak, siparişlerin detaylarını önbelleklemek isteyebiliriz:
@Service
public class OrderService {
@Autowired
private CachePort cachePort;
public OrderDetail getOrderDetail(Long orderId) {
String key = "orderDetail:" + orderId;
OrderDetail orderDetail = cachePort.get(key);
if (orderDetail == null) {
orderDetail = // Veritabanından sipariş detaylarını alın.
cachePort.put(key, orderDetail);
}
return orderDetail;
}
}
Bu yapı sayesinde, Hazelcast veya başka bir önbellekleme çözümüne geçiş yapmak istediğinizde, yalnızca yeni bir adaptör yazmanız gerekir. Bu, uygulamanın diğer kısımlarının değişiklik yapmadan aynı port üzerinden çalışmasına devam etmesini sağlar.
Event Sourcing de ekler misin?
Tabii ki, Event Sourcing'u bu mimariye eklemek ilginç ve karmaşık bir yön olabilir. Event Sourcing, her değişikliği olay (event) olarak saklamak yerine, uygulamanın durumunu bu olaylardan yeniden oluşturmanızı sağlar. Bu, daha güçlü bir denetim günlüğü ve karmaşık iş süreçlerinde daha fazla esneklik sağlayabilir.
Bu örnekte, sipariş durum değişikliklerinin olaylarını saklayacağımızı varsayalım.
1. Olay Tanımları
Her bir olay, belirli bir değişikliği temsil eder. Bu örnekte, sipariş durumlarının değiştiği olayları tanımlayalım:
public abstract class OrderEvent {
private final Long orderId;
private final LocalDateTime timestamp;
protected OrderEvent(Long orderId) {
this.orderId = orderId;
this.timestamp = LocalDateTime.now();
}
public Long getOrderId() {
return orderId;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
}
public class OrderPlacedEvent extends OrderEvent {
public OrderPlacedEvent(Long orderId) {
super(orderId);
}
}
public class OrderShippedEvent extends OrderEvent {
public OrderShippedEvent(Long orderId) {
super(orderId);
}
}
public class OrderCancelledEvent extends OrderEvent {
public OrderCancelledEvent(Long orderId) {
super(orderId);
}
}
2. Event Store
Olayları saklamak için bir olay deposu oluşturmalıyız. Bu depo, olayların saklanmasından ve sorgulanmasından sorumlu olacak.
@Repository
public interface EventStore extends JpaRepository<OrderEvent, Long> {
List<OrderEvent> findByOrderIdOrderByTimestampAsc(Long orderId);
}
3. Event Publisher
Olayları yayınlayan bir sınıf oluşturacağız:
@Service
public class EventPublisher {
@Autowired
private EventStore eventStore;
public void publish(OrderEvent event) {
eventStore.save(event);
}
}
4. Aggregate Root İle İntegrasyon
Order sınıfı, siparişle ilgili olayları yayınlamalı. Bu, Aggregate Root'un görevidir.
@Entity
public class Order {
// Diğer alanlar
@Transient
private List<OrderEvent> newEvents = new ArrayList<>();
public void place() {
// Logic
newEvents.add(new OrderPlacedEvent(this.id));
}
public void ship() {
// Logic
newEvents.add(new OrderShippedEvent(this.id));
}
public void cancel() {
// Logic
newEvents.add(new OrderCancelledEvent(this.id));
}
public List<OrderEvent> getNewEvents() {
return Collections.unmodifiableList(newEvents);
}
}
5. Event Sourcing ile Servis Katmanı İntegrasyonu
Servis katmanında, olayları toplayıp yayınlamalıyız:
@Service
public class OrderService {
@Autowired
private EventPublisher eventPublisher;
// Diğer metotlar
@Transactional
public void shipOrder(Long orderId) {
Order order = findById(orderId);
order.ship();
orderRepository.save(order);
order.getNewEvents().forEach(eventPublisher::publish);
// Diğer işlemler
}
}
Bu yapı, her sipariş durum değişikliğinin bir olay olarak saklanmasını ve daha sonra bu olayların sorgulanmasını veya farklı hedeflere (örneğin, bir mesaj kuyruğuna) yayınlanmasını sağlar.
Event Sourcing, karmaşık iş süreçlerinin ve denetim gereksinimlerinin olduğu durumlarda çok yararlı olabilir. Bununla birlikte, olayların tutarlı bir şekilde yayınlanması ve saklanması gibi bazı zorluklar da getirir.