Bir iş akışında arka arkaya uygulanması gereken kurallar arttıkça kodun tek bir servisin içinde büyümesi kaçınılmaz hale gelebilir. Özellikle validation ağırlıklı sistemlerde bunu çok sık görürüz.
Bir havayolu rezervasyonunu düşünün. Bilet kesilmeden önce:
- Uçakta yeterli koltuk var mı?
- Yolcu no-fly listesinde mi?
- Pasaport geçerli mi?
- Rezervasyon tutarı belirli bir sınırı aşıyor mu?
- Promosyon kodu geçerli mi?
- Vize gerekiyor mu?
- Fraud kontrolünden geçiyor mu?
İlk başta bunları birkaç if ile kontrol etmek kolaydır. Ama kurallar 5’ten 10’a, 10’dan 20’ye çıktığında BookingService yavaş yavaş dev bir metoda dönüşmeye başlar.
İşte Chain of Responsibility burada devreye girer. Temel fikir:
Her kuralı zincirin bağımsız bir halkası yap ve request’i bu halkalardan sırayla geçir.
Bir kural başarısız olursa zincir durur. Başarılı olursa request bir sonraki kurala geçer.
Senaryo: Uçak bileti rezervasyonu
Rezervasyon akışımızı kabaca şöyle düşünelim:
graph TD
R[Booking Request] --> F[Flight Check]
F --> S[Seat Check]
S --> N[No-Fly Check]
N --> P[Passport Check]
P --> A[Total Amount Check]
A --> C[Promo Check]
C --> B([Booking Created])Buradaki her kutu bağımsız bir kural. SeatAvailabilityCheck sadece koltuk sayısıyla ilgileniyor, PassportValidityCheck sadece pasaportla, PromoCodeCheck sadece promosyonla. Birbirlerinin iç işleyişini bilmiyorlar.
🔴 Kötü yaklaşım: Her şeyi BookingService’e koymak
Önce klasik yaklaşımı görelim. Bütün kontrolleri tek bir servise koyarsak:
@Service
public class BookingService {
public void createBooking(BookingRequest req) {
// 1. Koltuk müsaitliği
int available = seatRepository.getAvailableSeats(
req.flightId(), req.cabinClass());
if (available < req.passengers().size()) {
throw new BookingException("Yetersiz koltuk: " + req.flightId());
}
// 2. No-fly kontrolü
for (Passenger p : req.passengers()) {
if (noFlyListRepository.isBanned(p.identityNumber())) {
throw new BookingException(
"Yolcu uçuş yasaklılar listesinde: " + p.fullName());
}
}
// 3. Pasaport kontrolü
Flight flight = flightRepository.find(req.flightId());
if (flight.isInternational()) {
for (Passenger p : req.passengers()) {
LocalDate requiredUntil = flight.arrivalDate().plusMonths(6);
if (p.passportExpiry().isBefore(requiredUntil)) {
throw new BookingException(
"Pasaport geçerlilik süresi yetersiz: " + p.fullName());
}
}
}
// 4. Tutar kontrolü
BigDecimal total = calculateTotal(req);
if (total.compareTo(new BigDecimal("100000")) > 0) {
if (!req.customer().isVerified()) {
throw new BookingException(
"100.000 TL üzeri rezervasyon için doğrulanmış hesap gerekli");
}
}
// 5. Promosyon kontrolü
if (req.promoCode() != null) {
PromoCode promo = promoRepository.find(req.promoCode());
if (promo == null || promo.isExpired()) {
throw new BookingException("Geçersiz promosyon kodu");
}
if (!promo.validRoutes().contains(flight.route())) {
throw new BookingException("Promosyon kodu bu rota için geçerli değil");
}
}
// 6. Bebek/refakatsiz çocuk
// 7. Vize
// 8. Fraud
// 9. ...
bookingRepository.save(new Booking(...));
}
}
Kod henüz çok büyük görünmeyebilir. Ama gerçek sistemlerde bu liste burada bitmez. Bir süre sonra:
BookingService
├── Seat check
├── No-fly check
├── Passport check
├── Amount check
├── Promo check
├── Visa check
├── Fraud check
├── Child passenger check
├── Document check
├── Country restriction
└── ...
gibi bir yapı oluşur. Ve BookingService giderek bir God Object haline gelir.
Buradaki asıl problemler
Yeni kural eklemek zorlaşıyor
Yeni bir ülke için vize kuralı geldiğinde mevcut servise girip yeni bir
ifeklemeniz gerekiyor. Kural sayısı arttıkça servis büyüyor.Testler birbirine bağımlı hale geliyor
Sadece pasaport kontrolünü test etmek istiyorsanız bile
BookingService‘in diğer bütün dependency’leriyle uğraşmak zorunda kalabilirsiniz.Sıralamayı değiştirmek riskli
Örneğin pasaport kontrolünün uçuş bilgisine ihtiyacı varsa önce uçuşun yüklenmiş olması gerekir. Bu bağımlılıkları büyük bir metodun içinde takip etmek zorlaşır.
Aynı dosyaya herkes dokunuyor
Bir geliştirici promosyon kontrolü ekliyor, başka biri fraud kontrolü, üçüncü kişi vize kuralını değiştiriyor. Hepsi aynı
BookingServiceüzerinde çalışınca merge conflict ihtimali de artıyor.
🟢 İyi yaklaşım: Chain of Responsibility
Şimdi kuralları bağımsız halkalara bölelim. Her kural aynı interface’i uygulayacak:
public interface BookingCheck {
void check(BookingContext ctx);
}
Bir kural başarısız olursa exception fırlatacak. Başarılıysa hiçbir şey yapmadan devam edecek.
Aynı koltuk kontrolünün iki hali:
// BookingService'in içinde, diğer beş kuralın arasında
int available = seatRepository.getAvailableSeats(
req.flightId(), req.cabinClass());
if (available < req.passengers().size()) {
throw new BookingException("Yetersiz koltuk: " + req.flightId());
}
@Component
@Order(20)
public class SeatAvailabilityCheck implements BookingCheck {
private final SeatRepository seatRepository;
public SeatAvailabilityCheck(SeatRepository seatRepository) {
this.seatRepository = seatRepository;
}
@Override
public void check(BookingContext ctx) {
int requested = ctx.getRequest().passengers().size();
int available = seatRepository.getAvailableSeats(
ctx.getFlight().id(), ctx.getRequest().cabinClass());
if (available < requested) {
throw new BookingRejectedException(
RejectReason.NO_SEATS, "Seçilen kabinde yeterli koltuk yok");
}
}
}
Bu yapının en güzel tarafı, her halkanın kendi sorumluluğuna sahip olması.
1. Context: Halkalar arasında veri taşımak
Bazı kuralların, kendisinden önce çalışan kuralların ürettiği verilere ihtiyacı olabilir:
FlightLoadCheckdatabase’den uçuşu bulur.SeatAvailabilityCheckbu uçuşu kullanır.TotalAmountChecktoplam tutarı hesaplar.PromoCodeCheckhem uçuşu hem de toplam tutarı kullanır.
Bu bilgileri her metoda ayrı ayrı geçirmek yerine bir Context nesnesinde tutabiliriz:
public class BookingContext {
private final BookingRequest request;
private Flight flight;
private BigDecimal totalAmount;
private PromoCode appliedPromo;
public BookingContext(BookingRequest request) {
this.request = request;
}
// getter / setter
}
Böylece zincirin bir halkası Context’i zenginleştirir, sonraki halka da bunu kullanabilir:
FlightLoadCheck
│
│ ctx.setFlight(...)
▼
SeatAvailabilityCheck
│
▼
TotalAmountCheck
│
│ ctx.setTotalAmount(...)
▼
PromoCodeCheck
Bu yüzden Context sadece request’i taşımıyor. Pipeline boyunca üretilen bilgileri de taşıyor.
2. Her kural kendi sınıfında
FlightLoadCheck
@Component
@Order(10)
public class FlightLoadCheck implements BookingCheck {
private final FlightRepository flightRepository;
public FlightLoadCheck(FlightRepository flightRepository) {
this.flightRepository = flightRepository;
}
@Override
public void check(BookingContext ctx) {
Flight flight = flightRepository
.find(ctx.getRequest().flightId())
.orElseThrow(() -> new BookingRejectedException(
RejectReason.FLIGHT_NOT_FOUND, "Uçuş bulunamadı"));
if (flight.departureTime().isBefore(LocalDateTime.now().plusHours(2))) {
throw new BookingRejectedException(
RejectReason.CHECK_IN_CLOSED,
"Kalkışa 2 saatten az kaldığı için online satış kapandı");
}
ctx.setFlight(flight);
}
}
Bu sınıfın tek işi uçuşu kontrol etmek. Başka kuralları bilmiyor.
NoFlyListCheck
@Component
@Order(30)
public class NoFlyListCheck implements BookingCheck {
private final NoFlyListRepository noFlyListRepository;
public NoFlyListCheck(NoFlyListRepository noFlyListRepository) {
this.noFlyListRepository = noFlyListRepository;
}
@Override
public void check(BookingContext ctx) {
for (Passenger passenger : ctx.getRequest().passengers()) {
if (noFlyListRepository.isBanned(passenger.identityNumber())) {
throw new BookingRejectedException(
RejectReason.NO_FLY_LIST,
"Yolcu uçuşa kabul edilemez: " + passenger.fullName());
}
}
}
}
Yukarıdaki SeatAvailabilityCheck‘te dikkat edilmesi gereken bir detay vardı: uçuşu kendisi database’den çekmiyor, önceki halkanın Context’e koyduğu Flight nesnesini kullanıyordu (ctx.getFlight()). Bu da zincirdeki adımlar arasında kontrollü bir veri akışı oluşturuyor.
PassportValidityCheck
@Component
@Order(40)
public class PassportValidityCheck implements BookingCheck {
@Override
public void check(BookingContext ctx) {
if (!ctx.getFlight().isInternational()) {
return;
}
LocalDate requiredUntil = ctx.getFlight().arrivalDate().plusMonths(6);
for (Passenger passenger : ctx.getRequest().passengers()) {
if (passenger.passportExpiry().isBefore(requiredUntil)) {
throw new BookingRejectedException(
RejectReason.PASSPORT_EXPIRING,
"Pasaportun varıştan itibaren en az 6 ay geçerli olması gerekir: "
+ passenger.fullName());
}
}
}
}
Burada return kullanılması önemli. Uçuş iç hat ise bu kontrolün yapacağı bir şey yok, dolayısıyla zincirin bir sonraki halkasına geçiyoruz. Bu, Chain of Responsibility’nin doğal davranışlarından biri:
“Bu kural bu request için geçerli değil, devam et.”
3. Bir halka Context’i zenginleştirebilir
Şimdi toplam tutarı hesaplayan kurala bakalım:
@Component
@Order(50)
public class TotalAmountCheck implements BookingCheck {
private static final BigDecimal THRESHOLD = new BigDecimal("100000");
private final FareCalculator fareCalculator;
public TotalAmountCheck(FareCalculator fareCalculator) {
this.fareCalculator = fareCalculator;
}
@Override
public void check(BookingContext ctx) {
BigDecimal total = fareCalculator.calculate(
ctx.getRequest(), ctx.getFlight());
ctx.setTotalAmount(total);
if (total.compareTo(THRESHOLD) > 0
&& !ctx.getRequest().customer().isVerified()) {
throw new BookingRejectedException(
RejectReason.VERIFICATION_REQUIRED,
"100.000 TL üzeri rezervasyonlar için doğrulanmış hesap gerekli");
}
}
}
Burada iki iş yapıyoruz: toplam tutarı hesaplıyoruz ve hesaplanan tutarı Context’e koyuyoruz. Sonraki halkalar artık ctx.getTotalAmount() ile bunu kullanabilir.
4. PromoCodeCheck
Şimdi önceki halkaların ürettiği verileri kullanan bir kural yazalım:
@Component
@Order(60)
public class PromoCodeCheck implements BookingCheck {
private final PromoRepository promoRepository;
public PromoCodeCheck(PromoRepository promoRepository) {
this.promoRepository = promoRepository;
}
@Override
public void check(BookingContext ctx) {
String code = ctx.getRequest().promoCode();
if (code == null) {
return;
}
PromoCode promo = promoRepository
.find(code)
.filter(p -> !p.isExpired())
.orElseThrow(() -> new BookingRejectedException(
RejectReason.INVALID_PROMO, "Geçersiz promosyon kodu"));
if (!promo.validRoutes().contains(ctx.getFlight().route())) {
throw new BookingRejectedException(
RejectReason.PROMO_ROUTE_MISMATCH,
"Promosyon kodu bu rota için geçerli değil");
}
if (promo.minAmount().compareTo(ctx.getTotalAmount()) > 0) {
throw new BookingRejectedException(
RejectReason.PROMO_MIN_NOT_MET,
"Promosyon için minimum bilet tutarı karşılanmıyor");
}
ctx.setAppliedPromo(promo);
}
}
Burada PromoCodeCheck iki farklı bilgiyi önceki halkalardan alıyor:
graph LR
F[FlightLoadCheck] -->|"ctx.flight"| P[PromoCodeCheck]
T[TotalAmountCheck] -->|"ctx.totalAmount"| PBu yüzden zincirdeki sıra rastgele değil. Önce uçuş yüklenmeli, sonra tutar hesaplanmalı, daha sonra promosyon kontrolü yapılmalı.
5. Zinciri çalıştıran Pipeline
Şimdi bütün bu halkaları bir araya getirmemiz gerekiyor. Spring burada işimizi oldukça kolaylaştırıyor: BookingCheck interface’ini uygulayan bütün bean’leri List<BookingCheck> olarak inject edebiliriz. @Order sayesinde Spring bunları doğru sırada verir:
@Service
public class BookingCheckPipeline {
private final List<BookingCheck> checks;
public BookingCheckPipeline(List<BookingCheck> checks) {
this.checks = checks;
}
public BookingContext run(BookingRequest request) {
BookingContext ctx = new BookingContext(request);
for (BookingCheck check : checks) {
check.check(ctx);
}
return ctx;
}
}
Spring tarafında elimizde kabaca şu liste oluşur:
@Order(10) FlightLoadCheck
@Order(20) SeatAvailabilityCheck
@Order(30) NoFlyListCheck
@Order(40) PassportValidityCheck
@Order(50) TotalAmountCheck
@Order(60) PromoCodeCheck
Pipeline’ın tek sorumluluğu zinciri çalıştırmak. Bu kadar.
Ya ilk hatada durmak yerine bütün hataları toplamak istersek?
Yukarıdaki pipeline fail-fast çalışıyor: ilk başarısız kural exception fırlatıyor ve zincir orada duruyor. Kullanıcı hatayı düzeltip tekrar gönderiyor, bu sefer bir sonraki hataya çarpıyor.
Form doğrulaması gibi senaryolarda genellikle bütün ihlalleri bir kerede göstermek istersiniz. Bunun için kuralları exception yerine ihlal listesi döndürecek şekilde yazmak gerekir:
public interface BookingCheck {
List<Violation> check(BookingContext ctx);
}
Pipeline da toplayarak ilerler:
public BookingContext run(BookingRequest request) {
BookingContext ctx = new BookingContext(request);
List<Violation> violations = new ArrayList<>();
for (BookingCheck check : checks) {
violations.addAll(check.check(ctx));
}
if (!violations.isEmpty()) {
throw new BookingRejectedException(violations);
}
return ctx;
}
Ama bir uyarı: bu ancak kurallar birbirinden bağımsızsa güvenlidir. FlightLoadCheck başarısız olduğunda ctx.getFlight() null kalır ve sonraki kurallar NullPointerException ile patlar. Pratikte iyi çalışan çözüm melez oluyor: veri yükleyen ve kritik olan adımlar fail-fast, saf doğrulama yapan adımlar toplayarak.
@Order neden önemli?
Burada @Order sadece estetik bir detay değil. Zincirdeki bazı kurallar birbirlerine bağımlı:
FlightLoadCheck TotalAmountCheck
↓ ↓
ctx.flight ctx.totalAmount
↓ ↓
PassportValidityCheck PromoCodeCheck
Pasaport kontrolü çalışmadan önce flight Context’e konmuş olmalı. Aynı şekilde promosyon kontrolü çalışmadan önce toplam tutarın hesaplanmış olması gerekiyor. Bu yüzden sıralama, veri bağımlılıkları tarafından belirleniyor.
Pratikte 10, 20, 30... şeklinde gitmek de faydalı. Yarın araya yeni bir kontrol koymanız gerekirse @Order(45) gibi bir değer kullanabilirsiniz.
Sıralama gizli bir bağımlılık yaratıyor
Bu yaklaşımın bedeli şu: SeatAvailabilityCheck, ctx.getFlight()‘in dolu olmasına güveniyor ama bunu hiçbir yerde beyan etmiyor. Bağımlılık yalnızca iki @Order sayısı arasındaki farkta yaşıyor.
Biri yeni bir kontrolü @Order(5) ile eklerse ya da mevcut sayıları düzenlerse, derleyici uyarmaz — çalışma zamanında NullPointerException alırsınız.
İki basit önlem işe yarar:
Context getter’larını sessiz bırakmayın.
getFlight()null yerine anlamlı bir hata versin:public Flight getFlight() { return Objects.requireNonNull(flight, "FlightLoadCheck bu kuraldan önce çalışmalı"); }Zincirin sırasını test edin. Aşağıdaki
hintbunun nasıl yapıldığını gösteriyor.
6. BookingService artık küçücük
Bütün bu kurallar ayrı sınıflara taşındığında asıl servisimizin ne kadar sadeleştiğine bakın:
@Service
public class BookingService {
private final BookingCheckPipeline pipeline;
private final BookingRepository bookingRepository;
public BookingService(BookingCheckPipeline pipeline,
BookingRepository bookingRepository) {
this.pipeline = pipeline;
this.bookingRepository = bookingRepository;
}
@Transactional
public Booking createBooking(BookingRequest request) {
BookingContext ctx = pipeline.run(request);
return bookingRepository.save(Booking.from(ctx));
}
}
300 satırlık validation mantığı artık burada yok. Servis sadece şunu yapıyor:
Request
↓
Pipeline
↓
BookingContext
↓
Save
Daha önemlisi, BookingService hangi kuralların var olduğunu bile bilmiyor. Bu önemli bir ayrım.
Yeni bir kural eklemek ne kadar zor?
Diyelim ki yeni bir kural geldi:
İngiltere uçuşlarında ETA zorunlu.
Eski yaklaşımda BookingService‘e girip yeni bir if eklemeniz gerekir. Chain of Responsibility’de ise yeni bir sınıf ekliyorsunuz:
@Component
@Order(45)
public class EtaRequirementCheck implements BookingCheck {
@Override
public void check(BookingContext ctx) {
if (!ctx.getFlight().isToUnitedKingdom()) {
return;
}
if (!ctx.getRequest().customer().hasValidEta()) {
throw new BookingRejectedException(
RejectReason.ETA_REQUIRED, "İngiltere uçuşu için ETA gerekli");
}
}
}
Ve bitti. BookingService değişmedi, BookingCheckPipeline değişmedi, diğer kurallar değişmedi. Sadece yeni bir halka eklendi. Bu, pattern’in en güçlü taraflarından biri.
Unit test: Her kuralı ayrı ayrı test etmek
Chain of Responsibility’nin en güzel avantajlarından biri de test edilebilirlik. Sadece SeatAvailabilityCheck‘i test etmek istiyorsak diğer kuralların repository’lerini mock’lamamıza gerek yok. Sadece bu sınıfın ihtiyacı olan dependency’yi veriyoruz:
@Test
void yeterliKoltukYoksaRezervasyonuReddeder() {
when(seatRepository.getAvailableSeats("TK1923", CabinClass.ECONOMY))
.thenReturn(1);
var check = new SeatAvailabilityCheck(seatRepository);
BookingContext context = contextWithPassengers("TK1923", 3);
assertThatThrownBy(() -> check.check(context))
.isInstanceOf(BookingRejectedException.class);
}
Testin yaptığı şey çok net:
3 yolcu
↓
1 koltuk
↓
SeatAvailabilityCheck
↓
BookingRejectedException
No-fly kontrolü yok, promosyon kontrolü yok, pasaport kontrolü yok. Çünkü bunların bu testle ilgisi yok. Bu da testlerin hem daha hızlı hem de daha anlaşılır olmasını sağlıyor.
Halkaları tek tek test etmek yetmez; sıranın kendisi de bir davranıştır ve test edilmeyi hak eder:
@Test
void zincirDogruSiradaCalisir() {
List<String> order = pipeline.getChecks().stream()
.map(c -> c.getClass().getSimpleName())
.toList();
assertThat(order).containsExactly(
"FlightLoadCheck",
"SeatAvailabilityCheck",
"NoFlyListCheck",
"PassportValidityCheck",
"TotalAmountCheck",
"PromoCodeCheck");
}
Biri @Order değerini bozduğunda ya da yeni bir kuralı yanlış yere koyduğunda bu test kırılır — production’da null bir Flight ile karşılaşmadan önce.
Guard Clauses ile birlikte kullanmak
Burada Guard Clauses ve Early Return yazısıyla güzel bir bağlantı var. Örneğin:
@Override
public void check(BookingContext ctx) {
if (!ctx.getFlight().isInternational()) {
return;
}
// Asıl passport kontrolü
}
Burada guard clause kullanıyoruz. Yani:
- Chain of Responsibility, büyük akışı parçalara ayırıyor.
- Guard Clause, her halkanın kendi içindeki akışı sade tutuyor.
İki teknik birbirinin alternatifi değil; birlikte gayet güzel çalışabilirler.
Feature flag ile dinamik kurallar
Chain of Responsibility’nin bir başka güzel tarafı da Spring’in conditional bean özellikleriyle birlikte kullanılabilmesi. Örneğin fraud kontrolünü sadece belirli ortamlarda çalıştırmak istediğimizi düşünelim:
@Component
@Order(70)
@ConditionalOnProperty(name = "checks.fraud.enabled", havingValue = "true")
public class FraudCheck implements BookingCheck {
@Override
public void check(BookingContext ctx) {
// Fraud kontrolü
}
}
Konfigürasyon:
checks.fraud.enabled=true
checks:
fraud:
enabled: true
Property açık olduğunda bean zincire dahil edilir. Kapalı olduğunda ise bean hiç oluşturulmaz ve pipeline açısından fraud kontrolü hiç yokmuş gibi davranılır. Bu, feature flag mantığını zincire oldukça temiz bir şekilde eklemenizi sağlar.
Bu, Adapter ve Strategy yazısındaki single-tenant kurulumla aynı mekanizma: @ConditionalOnProperty ile bean’i açılışta zincire dahil etmek ya da hiç yaratmamak. Orada hangi adaptörün çalışacağını, burada hangi kuralın zincirde olacağını belirliyor.
Ne zaman Chain of Responsibility kullanmalı?
Her if bloğu için Chain of Responsibility kullanmak gerekmiyor. 5 tane basit kontrolünüz varsa guard clause gayet yeterli olabilir. Ama kurallar büyümeye başladıysa:
- Her kuralın ayrı dependency’leri varsa.
- Kurallar farklı ekipler tarafından geliştiriliyorsa.
- Yeni kurallar sık sık ekleniyorsa.
- Kuralların sırası önemliyse.
- Bazı kurallar feature flag ile açılıp kapanıyorsa.
- Aynı kurallar farklı akışlarda tekrar kullanılabiliyorsa.
- Tek bir servis yüzlerce satırlık validation koduna dönüşüyorsa.
Chain of Responsibility ciddi şekilde anlam kazanmaya başlar.
| Durum | Yaklaşım |
|---|---|
| Az sayıda, basit, birbirinden bağımsız kontrol | Guard clauses |
| Çok sayıda kural, ama hepsi aynı veriyle çalışıyor | Önce metodu sadeleştir |
| Kuralların ayrı dependency’leri, sırası ve feature flag’leri var | Chain of Responsibility / Pipeline |
Buradaki sınır kesin değil. “5 kural oldu, artık kesin Chain of Responsibility” diye bir matematik yok. Asıl soru:
Bu kurallar hala tek bir metodun içinde kolayca anlaşılabiliyor mu?
Cevap hayırsa parçalamayı düşünmenin zamanı gelmiştir.
Sektörde nerelerde görürüz?
Bu pattern özellikle ardışık kontrollerin olduğu sistemlerde oldukça doğal. Havayolu sistemlerinde:
Booking Validation
↓
Check-in Validation
↓
Document Check
↓
Boarding Rules
gibi akışlar Chain of Responsibility için çok uygun. Ama bununla sınırlı değil. Spring Security’nin filter chain yapısı da aynı temel fikri taşır:
graph LR
R[Request] --> A[Authentication Filter]
A --> Z[Authorization Filter]
Z --> C[CSRF Filter]
C --> D["…"]
D --> App([Application])API gateway filtreleri, HTTP middleware’leri, request validation pipeline’ları ve çeşitli fraud/risk kontrolleri de aynı düşünceye oldukça yakındır.
Altta yatan fikir
Chain of Responsibility’nin özünü tek cümlede anlatmak gerekirse:
Büyük bir kural setini tek bir yerde yönetmek yerine, her kuralı bağımsız bir halka yap ve request’i zincir boyunca ilerlet.
Sonuçta elimizde şöyle bir yapı kalıyor:
- Bir kural başarısızsa zincir orada duruyor.
- Bir kuralın request için yapacağı bir şey yoksa sessizce devam ediyor.
- Her kural kendi sınıfında test ediliyor.
- Yeni bir kural geldiğinde mevcut servisleri değiştirmek yerine yeni bir halka ekleniyor.
Yani amaç sadece kodu bölmek değil. Amaç, değişen kuralları birbirinden bağımsız hale getirmek.
Böylece 300 satırlık bir BookingService yerine, her biri tek bir işi yapan küçük ve test edilebilir kurallardan oluşan bir pipeline elde ediyoruz.

Yorumlar