C#’ta Guard Clause Pattern Nedir? Kod Kalitesini Artıran Basit Bir Yaklaşım konusunu anlatan renkli 3D teknik illüstrasyon

Giriş

Kod yazarken bazen fonksiyonlarımızın başı, uzun ve iç içe geçmiş if bloklarıyla dolu hale gelir. Özellikle hata kontrolleri veya parametre doğrulamaları söz konusu olduğunda kod okunabilirliğini kaybeder.
Bu durumda Guard Clause Pattern, kodunuzu sadeleştirip “happy path”i daha net hale getirmenizi sağlar.

guard_clause_pattern

Bu yazıda, C# özelinde erken dönüş yaklaşımının tanımını ve uygulamasını adım adım inceleyeceğiz.

Guard Clause Pattern Nedir?

Guard Clause, bir metodun başında geçersiz veya istenmeyen durumları erken tespit edip fonksiyondan çıkmayı sağlayan bir yaklaşımdır. Bu sayede, gereksiz else bloklarından kurtulur, kodun ana amacını (happy path) daha net bir şekilde ortaya çıkarırsınız.

Klasik Yaklaşım (Karmaşık Kod)

Aşağıdaki örneğe bakalım:

public IActionResult GetCategory(int id)
{
    if (id <= 0)
    {
        return BadRequest("Geçersiz kategori ID");
    }
    else
    {
        var category = _repository.GetById(id);
        if (category == null)
        {
            return NotFound("Kategori bulunamadı");
        }
        else
        {
            return Ok(category);
        }
    }
}

Bu kod çalışır, ancak iç içe geçmiş if blokları nedeniyle okunabilirlik düşüktür.

Guard Clause ile Basitleştirilmiş Yaklaşım

Aynı kodu erken dönüşle şöyle yazabiliriz:

public IActionResult GetCategory(int id)
{
    if (id <= 0)
        return BadRequest("Geçersiz kategori ID");

    var category = _repository.GetById(id);
    if (category == null)
        return NotFound("Kategori bulunamadı");

    return Ok(category);
}

Gördüğünüz gibi, “hatalı durumları” başta kontrol edip metottan erken çıkıyoruz. Artık fonksiyonun ana akışı (kategori döndürme kısmı) daha net ve sade görünüyor.

Gerçek Hayattan Bir Örnek: MediatR ile Guard Clause Kullanımı

Aşağıdaki örnek, bir MediatR handler içinde Guard Clause kullanımını gösteriyor:

public class GetCategoryQueryHandler
    : IRequestHandler<
        GetCategoryQuery,
        TResult<List<GetCategoryQueryResult>>
    >
{
    private readonly IRepository<Category> _repository;
    private readonly IMapper _mapper;

    public GetCategoryQueryHandler(
        IRepository<Category> repository,
        IMapper mapper
    )
    {
        _repository = repository;
        _mapper = mapper;
    }

    public async Task<TResult<List<GetCategoryQueryResult>>> Handle(
        GetCategoryQuery request,
        CancellationToken cancellationToken
    )
    {
        var categories = await _repository.GetAllAsync(
            cancellationToken
        );
        if (categories == null || !categories.Any())
        {
            return TResult<List<GetCategoryQueryResult>>.Failure(
                ErrorType.NotFound,
                "Category not found"
            );
        }

        var result = _mapper.Map<List<GetCategoryQueryResult>>(
            categories
        );

        return TResult<List<GetCategoryQueryResult>>.Success(result);
    }
}
public class CreateCategoryCommandHandler
    : IRequestHandler<CreateCategoryCommand, TResult<Guid>>
{
    private readonly IRepository<Category> _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;

    public CreateCategoryCommandHandler(
        IRepository<Category> repository,
        IUnitOfWork unitOfWork,
        IMapper mapper
    )
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _mapper = mapper;
    }

    public async Task<TResult<Guid>> Handle(
        CreateCategoryCommand request,
        CancellationToken ct
    )
    {
        var exists = await _repository.GetSingleAsync(
            x => x.Name == request.Name,
            ct
        );
        if (exists is not null)
            return TResult<Guid>.Failure(
                ErrorType.Conflict,
                "Category name already exists"
            );

        var category = _mapper.Map<Category>(request);
        await _repository.AddAsync(category, ct);

        await _unitOfWork.SaveChangesAsync(ct);
        return TResult<Guid>.Success(category.Id);
    }
}

Bu örnekte, önce hatalı durum kontrol ediliyor. Eğer hata varsa erken dönülüyor (return), aksi halde işlem başarıyla devam ediyor. Bu yapı ana akışı iç içe koşullardan ayırır.

Guard Clause ve okunabilirlik

  1. Okunabilirlik Artar: Kodun ana amacı daha belirgin olur.
  2. Bakımı Kolaylaşır: Gereksiz iç içe if blokları ortadan kalkar.
  3. Test Edilebilirlik İyileşir: Hatalı durumlar kolayca izole edilip test edilebilir.
  4. Akışın görünürlüğü: Erken dönüş nesting’i azaltır; SRP ise değişiklik nedeni ve sorumluluk sınırıyla ilgilidir.

Ne Zaman Kullanılmalı?

Erken dönüş özellikle şu durumlarda faydalıdır:

  • Metot başında parametre veya veri doğrulaması yapılacaksa
  • Hatalı senaryoları erken tespit edip akışı durdurmak gerekiyorsa
  • “Happy path”’in net ve sade görünmesi isteniyorsa

Sonuç

Bu yaklaşım, nesting’i azaltıp ana akışı görünür tutar. C#’ta bu yaklaşımı benimseyerek, hem kendiniz hem de ekibiniz için daha temiz, daha okunabilir ve sürdürülebilir kod yazabilirsiniz.

MediatR kesitleri uygulamanın TResult, ErrorType, repository, Unit of Work ve AutoMapper sözleşmelerini varsayıyor. Async metotlar CancellationToken kabul etmelidir; mapper configuration ve Category ID üretimi ayrıca gerekir. İlk iki kesit controller method alternatifleridir.

Empty collection için NotFound bu örneğin seçtiği contract’tır; birçok liste endpoint’i 200 ve boş liste döner. Ayrıca Exists/isim kontrolü ile insert arasında race condition vardır. Database uniqueness constraint’ini ekleyin ve provider’a özgü duplicate hatasını uygun Conflict davranışına çevirin.

Beklenmeyen DB hatasını veya cancellation’ı genel catch ile başarı gibi dönmeyin; sorumluluğu belli global sınırda ele alın. İç exception mesajını client’a taşımadan log context’ini koruyun. Exception Handling rehberi bu sınırı, DDD Validation incelemesi invariant ile request kontrolünü açıklıyor.

Exception ile guard yazarken Microsoft exception best practices rehberindeki argument kontrolü ve cancellation ayrımını dikkate alın.

By tanju.bozok

Senior .NET Engineer | Backend architecture, distributed systems & real-time communication