İş kuralları arttıkça kodun içine daha fazla if eklemek çok kolaydır.

Başlangıçta bir tane kontrol vardır:

if (condition) {
    // işlem
}

Sonra başka bir kural gelir:

if (condition) {
    if (anotherCondition) {
        // işlem
    }
}

Bir süre sonra kod şuna dönüşür:

if
 └── if
      └── if
           └── if
                └── if

Bu yapı Arrow Anti-Pattern olarak bilinir. Kod sağa doğru ilerledikçe okunması zorlaşır ve yeni bir kural eklemek gittikçe daha riskli hale gelir.

Bunun yerine Guard Clauses ve Early Return kullanabiliriz. Temel fikir çok basit:

Geçersiz durumları en başta kontrol et ve hemen çık. Asıl iş mantığını en sona bırak.

Guard Clause nedir?

Guard Clause, bir metodun çalışmasını engelleyen durumları metodun başında kontrol edip hemen return veya throw ile çıkmaktır.

Örneğin:

if (user == null) {
    return;
}

Burada “user varsa ne yapmalıyım?” diye düşünmek yerine:

“User yoksa burada dur.”

diyoruz. Böylece geri kalan kodun user‘ın null olmadığını varsaymasına izin veriyoruz.

Birden fazla kural olduğunda da aynı yaklaşımı kullanabiliriz:

if (user == null) {
    return;
}

if (!user.isActive()) {
    return;
}

if (!user.hasPermission()) {
    return;
}

// Asıl işlem

Kod artık iç içe değildir.

Gerçek bir senaryo: check-in

Bir havayolu şirketinin check-in işlemini düşünelim. Bir yolcunun check-in yapabilmesi için:

  1. Geçerli bir bileti olmalı

    Bilet yoksa ya da iptal edilmişse akışın devam etmesinin anlamı yok.

  2. Kara listede olmamalı

    Uçuşa yasaklı yolcular için harici bir servise soruluyor.

  3. Dış hat uçuşuysa geçerli bir vizesi olmalı

    İç hat uçuşlarında bu kural hiç işletilmiyor.

  4. Bagaj limiti aşılmamalı

    Uçuşun izin verdiği kilo sınırıyla karşılaştırılıyor.

Aynı dört kuralın iki farklı yazılışı:

public CheckInResult processCheckIn(Passenger passenger, Flight flight) {
    // ❌ Kod sürekli sağa doğru ilerliyor
    if (passenger.getTicket() != null && passenger.getTicket().isValid()) {
        if (!blacklistClient.isBanned(passenger.getIdentityNumber())) {
            if (flight.isInternational()) {
                if (passenger.hasValidVisa(flight.getDestinationCountry())) {
                    if (passenger.getBaggageWeight() <= flight.getMaxBaggageAllowance()) {
                        return CheckInResult.success("Boarding pass oluşturuldu.");
                    } else {
                        return CheckInResult.error("Bagaj limiti aşıldı.");
                    }
                } else {
                    return CheckInResult.error("Geçerli bir vize bulunamadı.");
                }
            } else {
                // ❌ İç hat uçuşu için başka bir akış yazmak gerekiyor
            }
        } else {
            return CheckInResult.error("Yolcu uçuşa yasaklı listesinde.");
        }
    } else {
        return CheckInResult.error("Geçerli bir bilet bulunamadı.");
    }
}
public CheckInResult processCheckIn(Passenger passenger, Flight flight) {

    // 🛡️ Guard 1: Bilet geçersizse hemen çık
    if (passenger.getTicket() == null || !passenger.getTicket().isValid()) {
        return CheckInResult.error("Geçerli bir bilet bulunamadı.");
    }

    // 🛡️ Guard 2: Kara listedeyse hemen çık
    if (blacklistClient.isBanned(passenger.getIdentityNumber())) {
        return CheckInResult.error("Yolcu uçuşa yasaklı listesinde.");
    }

    // 🛡️ Guard 3: Dış hat uçuşunda vize yoksa hemen çık
    if (flight.isInternational()
            && !passenger.hasValidVisa(flight.getDestinationCountry())) {
        return CheckInResult.error("Geçerli bir vize bulunamadı.");
    }

    // 🛡️ Guard 4: Bagaj limiti aşılmışsa hemen çık
    if (passenger.getBaggageWeight() > flight.getMaxBaggageAllowance()) {
        return CheckInResult.error("Bagaj limiti aşıldı.");
    }

    // ✅ Happy Path
    return CheckInResult.success("Boarding pass oluşturuldu.");
}

İlk versiyondaki asıl problem sadece kodun uzun olması değil. Her yeni kural, mevcut if bloklarının içine bir katman daha ekliyor. Kodun hangi durumda çalıştığını anlamak için sürekli yukarıdaki koşulları takip etmeniz gerekiyor. Bir noktadan sonra geliştirici artık iş kuralını değil, parantezleri takip etmeye başlıyor.

İç hat uçuşları için else bloğunun boş kalması tesadüf değil. İç içe yapıda “bu kural sadece bazı durumlarda geçerli” demek, akışı ikiye bölmek anlamına geliyor. Guard clause versiyonunda ise aynı şey tek satıra iniyor: flight.isInternational() && !passenger.hasValidVisa(...).

İkinci versiyonda kodu yukarıdan aşağıya okumak çok daha kolay. Her if tek bir soruya cevap veriyor:

Bilet geçerli mi?
    Hayır → çık

Kara listede mi?
    Evet → çık

Dış hat + vize yok mu?
    Evet → çık

Bagaj limiti aşıldı mı?
    Evet → çık

Hepsi geçti mi?
    Evet → check-in başarılı

En güzel tarafı ise Happy Path’in artık çok net olması. Metodun sonunda kalan kod, bütün kontrollerin geçildiği başarılı senaryodur.

Guard Clause ile Happy Path’i ayırmak

Bu yaklaşımın önemli bir avantajı da hata durumlarını başta toplamasıdır. Kodun ilk bölümü:

// Invalid cases
if (...) return error;
if (...) return error;
if (...) return error;
if (...) return error;

Sonra:

// Happy path
return success(...);

Bu ayrım özellikle uzun business method’larında kodun okunmasını ciddi şekilde kolaylaştırır.

Unit test ile bu kuralları test etmek

Guard clause kullandığımızda unit test yazmak da oldukça doğal hale gelir. Her guard’ın bir davranışı var, dolayısıyla her kural için bir test yazabiliriz.

JUnit 5 ve Mockito ile:

class CheckInServiceTest {

    private BlacklistClient blacklistClient;
    private CheckInService checkInService;

    @BeforeEach
    void setUp() {
        blacklistClient = mock(BlacklistClient.class);
        checkInService = new CheckInService(blacklistClient);
    }

    @Test
    void gecersizBiletVarsaCheckInYapilamaz() {
        Passenger passenger = passengerWithInvalidTicket();
        Flight flight = domesticFlight();

        CheckInResult result =
                checkInService.processCheckIn(passenger, flight);

        assertThat(result.isSuccess()).isFalse();
        assertThat(result.message())
                .isEqualTo("Geçerli bir bilet bulunamadı.");
    }

    @Test
    void karaListedekiYolcuCheckInYapamaz() {
        Passenger passenger = passengerWithValidTicket();
        Flight flight = domesticFlight();

        when(blacklistClient.isBanned(passenger.getIdentityNumber()))
                .thenReturn(true);

        CheckInResult result =
                checkInService.processCheckIn(passenger, flight);

        assertThat(result.isSuccess()).isFalse();
        assertThat(result.message())
                .isEqualTo("Yolcu uçuşa yasaklı listesinde.");
    }

    @Test
    void disHatUcusuVeGecersizVizeVarsaCheckInYapilamaz() {
        // Vizesiz bir yolcu kuruyoruz — stub'a gerek yok
        Passenger passenger = passengerWithValidTicket().withoutVisas();
        Flight flight = internationalFlight("DE");

        CheckInResult result =
                checkInService.processCheckIn(passenger, flight);

        assertThat(result.isSuccess()).isFalse();
        assertThat(result.message())
                .isEqualTo("Geçerli bir vize bulunamadı.");
    }

    @Test
    void bagajLimitiAsilirsaCheckInYapilamaz() {
        Passenger passenger = passengerWithValidTicket().withBaggageWeight(40);
        Flight flight = domesticFlight().withMaxBaggageAllowance(30);

        CheckInResult result =
                checkInService.processCheckIn(passenger, flight);

        assertThat(result.isSuccess()).isFalse();
        assertThat(result.message())
                .isEqualTo("Bagaj limiti aşıldı.");
    }

    @Test
    void tumKontrollerBasariliysaCheckInTamamlanir() {
        Passenger passenger = passengerWithValidTicket();
        Flight flight = domesticFlight();

        CheckInResult result =
                checkInService.processCheckIn(passenger, flight);

        assertThat(result.isSuccess()).isTrue();
        assertThat(result.message())
                .isEqualTo("Boarding pass oluşturuldu.");
    }
}

Burada testlerin yapısına dikkat edin. Her test tek bir business rule’u kontrol ediyor:

Invalid Ticket
    Test

Blacklist
    Test

Invalid Visa
    Test

Baggage Limit
    Test

Happy Path
    Test

Bu da testlerin okunmasını kolaylaştırıyor.

Kendi domain nesnenizi mock'lamayın

Testlerde yalnızca BlacklistClient mock’landı. Passenger ve Flight gerçek nesneler.

Bunun teknik bir zorunluluğu da var: passengerWithValidTicket() gerçek bir nesne döndürüyorsa when(passenger.hasValidVisa("DE")) çalışmaz, Mockito MissingMethodInvocationException fırlatır — when() yalnızca mock üzerinde çağrılabilir.

Ama asıl sebep tasarımsal: mock, bir sınır çizmek içindir. BlacklistClient ağın öbür ucundaki bir servis, testte onu ayağa kaldıramazsınız. Passenger ise sizin kendi domain nesneniz; onu gerçek veriyle kurmak hem daha hızlı hem de gerçeğe daha yakın bir test verir. Kendi nesnenizi mock’ladığınızda testiniz kodun davranışını değil, kendi varsayımlarınızı doğrulamış olur.

Bu yüzden yukarıdaki örneklerde stub yerine withoutVisas() ve withBaggageWeight(40) gibi küçük test data builder’ları kullandım.

Sadece geçersiz senaryoları test edip happy path’i atlamak sık yapılan bir hata. Guard’lardan biri yanlışlıkla fazla katı yazıldığında (>= yerine > gibi) bunu yalnızca happy path testi yakalar — hata senaryoları hala geçmeye devam eder.

Guard Clause sadece return demek değildir

Guard clause deyince sadece return düşünmeyin. Bazı durumlarda exception fırlatmak daha doğru olabilir:

public Booking createBooking(Customer customer) {

    if (customer == null) {
        throw new IllegalArgumentException("Customer cannot be null");
    }

    if (!customer.isActive()) {
        throw new CustomerNotActiveException(customer.getId());
    }

    // Happy Path
    return bookingRepository.save(...);
}

Yani temel fikir:

Geçersiz durumu erken yakala ve normal akışa sokma.

Bunun return, throw veya bazı durumlarda başka bir erken çıkış mekanizması olması probleme göre değişebilir.

Her if için Guard Clause kullanmalı mıyız?

Hayır. Guard Clause güçlü bir araç ama her yerde kullanmak zorunda değilsiniz.

Bölmeye gerek yok

İki koşul gerçekten aynı business logic’in parçasıysa birlikte tutmak daha okunabilir:

if (order.isPaid() && order.isConfirmed()) {
    shipOrder(order);
}

Sırf “guard clause kullanmalıyım” diye kodu parçalamak anlamlı değil.

Bölmeye değer

İç içe geçmiş üç kontrol varsa:

if (user != null) {
    if (user.isActive()) {
        if (user.hasPermission()) {
            // ...
        }
    }
}

guard clause çok daha okunabilir bir sonuç verir.

İkinci örneğin guard clause hali:

if (user == null) {
    return;
}

if (!user.isActive()) {
    return;
}

if (!user.hasPermission()) {
    return;
}

// ...

Amaç daha fazla if yazmak değil. Amaç nested if sayısını azaltmak.

Peki ne zaman Chain of Responsibility?

Guard Clause sayısı arttıkça başka bir problem ortaya çıkabilir. Örneğin:

1. Ticket kontrolü
2. Blacklist kontrolü
3. Visa kontrolü
4. Baggage kontrolü
5. Payment kontrolü
6. Age kontrolü
7. Loyalty kontrolü
8. Document kontrolü
9. Security kontrolü
10. Airport restriction kontrolü
...

Metod artık onlarca guard’dan oluşmaya başladıysa problem sadece indentation değildir. Bu noktada her kuralı ayrı bir sınıfa taşımak daha mantıklı olabilir:

CheckInService
BookingCheckPipeline
 ┌───────────────┐
 │ TicketCheck   │
 ├───────────────┤
 │ BlacklistCheck│
 ├───────────────┤
 │ VisaCheck     │
 ├───────────────┤
 │ BaggageCheck  │
 └───────────────┘

Bu durumda Chain of Responsibility veya pipeline yaklaşımı daha uygun hale gelir. Her kural kendi sınıfına ayrılır:

public interface BookingCheck {
    void check(BookingContext context);
}

Sonra:

public class TicketCheck implements BookingCheck {

    @Override
    public void check(BookingContext context) {
        // ticket validation
    }
}

Böylece business rule’lar bağımsız hale gelir — ve tek tek test edilebilir.

Karar ağacı

Pratikte şöyle düşünebilirsiniz:

graph TD
    A{"Kaç tane kural var?"} -->|"1 – 5"| B["Guard Clauses"]
    A -->|"5+"| C{"Kurallar tekrar<br/>kullanılıyor mu?"}
    C -->|Hayır| D["Önce metodu sadeleştir"]
    C -->|"Evet / dinamik"| E["Chain of Responsibility<br/>veya Pipeline"]

Bu kesin bir matematiksel sınır değil. “5 kural oldu, artık kesin Chain of Responsibility kullanmalıyım” diye bir kural yok. Buradaki sayı sadece düşünmek için bir sinyal.

Asıl soru şu:

Bu kurallar hala tek bir metodun içinde anlaşılır şekilde yönetilebiliyor mu?

Cevap evetse guard clause gayet yeterli olabilir. Cevap hayırsa kuralları ayrı bileşenlere bölmenin zamanı gelmiş olabilir.

Guard Clauses’ın asıl amacı

Guard Clause sadece kodu kısaltmak için kullanılan bir teknik değil. Asıl amacı kodun zihinsel modelini basitleştirmek.

İç içe yapıda

Eğer A doğruysa
  ve B doğruysa
    ve C doğruysa
      ve D doğruysa
        işlemi yap
      değilse...
    değilse...
  değilse...
değilse...

Guard Clause ile

A yanlış → çık
B yanlış → çık
C yanlış → çık
D yanlış → çık

Hepsi geçti → işlemi yap

İkincisi çok daha kolay okunur. Çünkü her satırı okurken zihninizde tutmanız gereken koşul sayısı sabit kalıyor; ilkinde ise her satırda bir tane daha artıyor.

Altta yatan kural

Clean Code açısından akılda tutulması gereken basit kural şu:

Önce geçersiz durumları temizle, sonra asıl işi yap.

İç içe if-else blokları büyümeye başladığında ilk refleksiniz daha fazla abstraction eklemek olmak zorunda değil. Bazen çözüm çok daha basittir:

if (invalidCondition) {
    return;
}

if (anotherInvalidCondition) {
    return;
}

// Happy Path
doTheRealWork();

Kodun amacı, geliştiriciyi mümkün olduğunca az şey düşünmeye zorlamaktır.

Guard Clause bunu sağlar: hataları ve istisnaları kenara iter, asıl iş akışını düz ve okunabilir bırakır.