Exception Fırlatmak Yerine Result Nesnesi Kullanmak
Merhabalar,
Bu yazımızın konusu, uygulamalarımızda beklenen hataların yönetilme şekilleriyle ilgili. Aslında iki yöntem üzerinde duracağız. Bir tanesi koddan exception fırlatmak, diğeri ise hatalı durumu belirten daha nazik bir nesne göndermek. Baştan uyarımı yapayım. Bu yazı result pattern lehine, taraflı bir görüş içerebilir. Yazının sonunda ise benchmark sonuçlarını paylaşıyor olacağım.
Mesela bir rezervasyon sistemini ele alalım. Müşteri x ve y tarihleri arasında otel rezervasyonu yapmak istiyor. Ancak müşteriye uygun tarihlerde rezervasyon bulamadık. Burada elbette günün sonunda, müşteriye nazik bir uyarı mesajı gösteriyoruz. Ancak aynı nezaketi kodumuza nasıl uygulamalıyız?

Birinci senaryoyu uyguladığımızı düşünelim. DB’den ve business logiclerimizden hesapladığımız kadarıyla, müşteri için uygun tarih yok. Burada bir exception fırlatacağız. Sonra ise, exception fırlamasından dolayı bu transactionımız anında kesilecek, gerekli loglamalar yapılacak. Aslında development maliyeti açısından bu durum daha kolay yönetilebilir duruyor. Ancak fırlayan hatadan dolayı sisteme yaratılan bir yük de mevcut. Burada hata ayıklama için sistem “stacktrace” dediğimiz bilgileri toplamakta. Bunlar için nesneler yaratılmakta ve CPU’da da bunlar için belli bir süre geçmekte.
Bunlar beklediğimiz exceptionlar. Bir de beklemediğimiz exceptionlar mevcut. Adı üstünde “beklenmedik hata”. Bunlar için “global error handling” methodları mevcut. Sistem hatalarını kullanıcıya göstermemek için bu yöntem tercih edilebilir. Bunlar için zaten yapabileceğimiz bir şey yok. Tespit sonrası gerekli düzenlemelerin yapılması gerekli. .NET 8 ile birlikte gelen IExceptionHandler arayüzü, bu iş için çok uygun.
Peki bir result nesnesi kullanmaya karar verirsek?
Burada da elbette bir nesne yaratma bedeli var. Ancak bu bedel bir exception fırlatmak kadar yüksek değil. Aslında bu yeni bir şey de değil. Çünkü standart olarak bir çok API bize bir result nesnesi, result nesnesinin içinde de istediğimiz sonucu vermekte. Mesele burada biraz daha result nesnesinin nerede oluşturulduğu.

İçeride exception fırladı diye result nesnesi en dış katmanda da oluşturulabilir. Ancak asıl iş kurallarımızın olduğu yerde dönmemiz, bu result nesnesini gerektiği yerde dönmemizi de sağlayacaktır. Aksi halde eğer iş katmanından exception fırlarsa, null nesne dönerse vs. gibi konularda presentation katmanında tekrardan bir kontrol eklemek gerekecek. Zaten iş katmanının yapması gereken kontrolü, bir dış katmanda tekrar yapmamızı gerektirecek.

Peki ne zaman exception fırlatacağım ve ne zaman result nesnesi döneceğim?
Rezervasyon sırasında uygun tarih bulunamamış olması beklenmedik bir durum değil. Ya da sipariş esnasında bir ürünün stoğunun bitmiş olması, bir listenin boş olması gibi durumların hepsi hayatın olağan akışında olabilecek durumlardır. Bu tarz konuların Result nesnesi ile yönetilmesi daha uygun olur.

Exceptionların fırlaması gereken anlar, genellikle bizim elimizle fırlatmamızı gerektirecek durumlar değiller. Exceptionlar yazılımda gerçekten hesap edemediğimiz durumlarda fırlaması gereken nesneler. Mesela 10 elemanlı bir listenin bir hata sonucu 11. elemanıyla ilgili hesap yapmak istediğimiz zaman çalışma zamanında “IndexOutOfRangeException” fırlayacaktır. Bu tarz durumlar ise yazılımcının hatanın kaynağını tespit ederek önleyici aksiyonları almasını gerektiren durumlardır. Yani tekrarının yaşanmaması sağlanmalıdır.
Şimdi de benchmark sonuçlarına bakalım. Benchmark için .NET 8 frameworkü ve BenchmarkDotNet kütüphanesi kullanıldı. İki methodumuz var. Bir methodumuz Null olan durumlarda exception fırlatıyor. Diğer methodumuz ise null olan durumlarda sadece bir result nesnesi dönüyor. Burada tembellikten custom bir exception yazmadım. Fırlattığım exception direkt bizzat System.ArgumentNullException.
using BenchmarkDotNet.Attributes;
namespace ResultPatternBenchmark;
public class ResultPatternAndExceptionBenchmark {
private const int IterationCount = 1000;
[Params(null, "", "short", "this is a longer string input")]
public string Parameter { get; set; }
[Benchmark]
public void ThrowExceptionIfNull() {
for (int i = 0; i < IterationCount; i++) {
try {
var result = ProcessWithException(Parameter);
// Do something with result to prevent optimization
_ = result.Status;
} catch (ArgumentNullException) {
// Expected exception, do nothing
}
}
}
[Benchmark]
public void SendResultObject() {
for (int i = 0; i < IterationCount; i++) {
var result = ProcessWithResultObject(Parameter);
// Do something with result to prevent optimization
_ = result.Status;
}
}
private ResultObject ProcessWithException(string input) {
if (string.IsNullOrEmpty(input)) {
throw new ArgumentNullException(nameof(input));
}
return new ResultObject(200, "Success", input);
}
private ResultObject ProcessWithResultObject(string input) {
if (string.IsNullOrEmpty(input)) {
return new ResultObject(400, "Value cannot be null or empty", "");
}
return new ResultObject(200, "Success", input);
}
}
public record ResultObject(int Status, string Message, object Data);
Bu kodu Release modeda “Just My Code disabled” şeklinde çalıştırıyorum. Çıkan sonuçlar ise aşağıdaki gibi.
| Method | Parameter | Mean | Error | StdDev |
|--------------------- |--------------------- |-------------:|-----------:|-----------:|
| ThrowExceptionIfNull | ? | 2,435.673 us | 11.1429 us | 8.6996 us |
| SendResultObject | ? | 3.072 us | 0.0053 us | 0.0045 us |
| ThrowExceptionIfNull | | 2,428.235 us | 19.3168 us | 16.1304 us |
| SendResultObject | | 3.086 us | 0.0181 us | 0.0170 us |
| ThrowExceptionIfNull | short | 3.579 us | 0.0398 us | 0.0372 us |
| SendResultObject | short | 3.272 us | 0.0095 us | 0.0089 us |
| ThrowExceptionIfNull | this (...)input [29] | 3.557 us | 0.0181 us | 0.0151 us |
| SendResultObject | this (...)input [29] | 3.285 us | 0.0163 us | 0.0144 us |
Görüldüğü üzere, tahmin edilebilir durumları exception ile yönetmek, result nesnesi ile yönetmekten daha maliyetli görünüyor. Hem de 800 kat daha maliyetli.
Konuyla ilgili test için oluşturduğum repoya aşağıdaki linkten ulaşabilirsiniz.
