Selamlar,
Bugün yazılım dünyasında sıkça karşılaştığımız ama bazen farkına bile varmadığımız bir konu olan Primitive Obsession’dan bahsedeceğiz. Türkçe’ye “ilkel takıntı” olarak da çevirebiliriz. Peki, bu ne anlama geliyor? Yazılımda neden önemli? Önce ufak bir giriş yapıp ardında kod örnekleri ile konuyu inceleyelim.
Primitive Obsession Nedir?
Yazılım dünyasında en çok kullandığımız şey nedir diye sorsak, muhtemelen “veri tipleri” cevabını veririz. int, string, bool gibi ilkel (primitive) tipleri sürekli kullanıyoruz. “Primitive Obsession” dediğimiz kavram ise, daha anlamlı ve düzenli bir veri modeli oluşturmak yerine her şeyi bu temel tiplerle çözmeye çalışmak anlamına geliyor.
Örneğin elimizde bir telefon numarası var ve bunu basitçe string olarak saklıyoruz. Ancak bu numara yalnızca rastgele bir metin parçası değil belirli bir ülkeye, alan koduna sahip olabilir, geçerli olup olmadığının kontrol edilmesi gerekebilir. Bu gibi durumlarda ilgiliyi veriyi string olarak saklarsak Primitive Obsession karşımıza çıkmış oluyor.
Daha somut bir örnek üzerinden gidelim. Diyelim ki bir kullanıcı kayıt sistemi geliştiriyoruz. Kullanıcının adını, e-posta adresini ve yaşını saklamak istiyoruz. İlk aklımıza gelen “string name, string email, int age” gibi temel tipleri kullanmak olabilir fakat bu kadar basit bir yapı, uzun vadede karşımıza pek çok sorun çıkarabilir. Kullanıcının adının belli bir formatı varsa, e-posta adresinin gerçekten geçerli olup olmadığını kontrol etmek istiyorsak veya yaş değerine göre farklı işlemler yapmak gerekiyorsa, elimizdeki verilere özgü kurallar bir anda karmaşık hale gelir. Halbuki her bir alanı temsil eden, daha anlamlı ve özelleştirilmiş veri tipleri kullanarak kodumuzu çok daha anlaşılır ve bakımı kolay bir hale getirebiliriz.
Şimdi yukarıdaki senaryoyu kod ile inceleyip Primitive Obsession ı gözlemleyelim;
public class User
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
Şimdi bu kullanıcıyı kullanalım:
static void Main(string[] args)
{
var user = new User
{
Name = "Can",
Email = "can@test.com",
Age = 27
};
Console.WriteLine($"User: {user.Name}, Email: {user.Email}, Age: {user.Age}");
}
Buraya kadar her şey normal gözüküyor aslında ama şimdi şunu düşünelim. E-posta adresinin gerçekten geçerli bir formatta olup olmadığını nasıl kontrol edeceğiz? Ya da yaşın negatif bir sayı olmaması gerektiğini nasıl garanti edeceğiz? Şu an bu kodda hiçbir kontrol yok. Her şeyi string ve int gibi ilkel tiplere emanet ettik.
Primitive Obsession Olmadığında Yaşanan Zorluklar
Şimdi bu yapıyı biraz zorlayalım. Diyelim ki e-posta adresinin “@” işareti içermesi ve yaşın 0–100 arasında olması gerekiyor. Bunu mevcut yapıda nasıl yaparız?
public class User
{
private string _email;
private int _age;
public string Name { get; set; }
public string Email
{
get { return _email; }
set
{
if (!value.Contains("@"))
throw new ArgumentException("Invalid email address!");
_email = value;
}
}
public int Age
{
get { return _age; }
set
{
if (value < 0 || value > 150)
throw new ArgumentException("Age must be between 0 and 150!");
_age = value;
}
}
}
static void Main(string[] args)
{
try
{
var user = new User
{
Name = "Can",
Email = "can.test.com", // Hata verecek, @ yok
Age = -5 // Hata verecek, negatif
};
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
}
Evet, kontrol ekledik ama buda başka bir soruna sebep oluyor. Bu kontrolleri her yerde tekrar tekrar yazmamız gerekebilir. Mesela başka bir sınıfta da e-posta kontrolü lazımsa nasıl bir yol izleyeceğiz? Aynı kodu kopyala-yapıştır mı yapacağız? Ayrıca, string tipi bize e-posta adresinin bir anlamı olduğunu söylemiyor. Bu sadece bir metin. Biri yanlışlıkla Email alanına bir telefon numarası yazarsa kod bunu engelleyemiyor.
Bir başka örnek: Diyelim ki bir para birimi tutuyoruz:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
static void Main(string[] args)
{
var product = new Product
{
Name = "Book",
Price = 29.99m
};
Console.WriteLine($"{product.Name} price: {product.Price}");
}
Peki bu decimal TL mi, dolar mı, euro mu? Bilmiyoruz! decimal bize sadece bir sayı veriyor, ama bu sayının bağlamını anlamak için ekstra çaba harcamamız lazım. İşte bu tür belirsizlikler ve kontrol eksiklikleri, Primitive Obsession yüzünden başımıza bela oluyor.
Bu Sorunu Nasıl Çözüyoruz?
Primitive Obsession’dan kurtulmanın yolu, ilkel tipler yerine daha anlamlı, kendi tiplerimizi (yani sınıflar veya yapılar) oluşturmak. Buna “değer nesneleri” (Value Objects) diyoruz. Şimdi yukarıdaki örnekleri yeniden ele alalım.
1. E-posta İçin Özel Bir Tür
E-posta adresini string olarak tutmak yerine, bir Email sınıfı yapalım:
public class Email
{
public string Value { get; }
public Email(string value)
{
if (string.IsNullOrEmpty(value) || !value.Contains("@"))
throw new ArgumentException("Invalid email address!");
Value = value;
}
public override string ToString() => Value;
}
public class User
{
public string Name { get; set; }
public Email Email { get; set; }
public int Age { get; set; }
}
static void Main(string[] args)
{
try
{
var user = new User
{
Name = "Can",
Email = new Email("can@test.com"),
Age = 30
};
Console.WriteLine($"User: {user.Name}, Email: {user.Email}");
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
}
Eğer geçersiz bir e-posta girersek (mesela can.test.com):
Invalid email address!
Şuanda, artık Email bir anlam taşıyor. Kod okuyan biri “Bu bir e-posta adresi” diye hemen anlıyor ve geçerlilik kontrolü de tek bir yerde tanımlı.
2. Yaş İçin Özel Bir Tür
Yaşı da int olarak bırakmayalım:
public class Age
{
public int Value { get; }
public Age(int value)
{
if (value < 0 || value > 150)
throw new ArgumentException("Age must be between 0 and 100!");
Value = value;
}
public override string ToString() => Value.ToString();
}
public class User
{
public string Name { get; set; }
public Email Email { get; set; }
public Age Age { get; set; }
}
static void Main(string[] args)
{
try
{
var user = new User
{
Name = "Can",
Email = new Email("can@test.com"),
Age = new Age(28)
};
Console.WriteLine($"User: {user.Name}, Email: {user.Email}, Age: {user.Age}");
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
}
Negatif bir yaş girersek:
Age must be between 0 and 100!
3. Para Birimi İçin Özel Bir Tür
Son olarak, para birimi örneğini düzeltelim:
public class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative!");
if (string.IsNullOrEmpty(currency))
throw new ArgumentException("Currency must be specified!");
Amount = amount;
Currency = currency;
}
public override string ToString() => $"{Amount} {Currency}";
}
public class Product
{
public string Name { get; set; }
public Money Price { get; set; }
}
static void Main(string[] args)
{
Product product = new Product
{
Name = "Notebook",
Price = new Money(15.50m, "USD")
};
Console.WriteLine($"{product.Name} price: {product.Price}");
}
Artık fiyatın birimi de belli, negatif olup olmadığı da kontrol edildi.
Peki, Validasyonları Tek Metotta Toplasak?
Burada akla tüm validasyonları tek bir metotda toplayıp kullanılması gereken yerde bu metodu çağırmak gelebilir. Şimdi bunuda örnekle inceleyelim.
Diyelim ki e-posta kontrolünü bir metotta topluyoruz:
public class ValidationHelper
{
public static void ValidateEmail(string email)
{
if (string.IsNullOrEmpty(email) || !email.Contains("@"))
throw new ArgumentException("Invalid email address!");
}
}
public class User
{
private string _email;
public string Name { get; set; }
public string Email
{
get { return _email; }
set
{
ValidationHelper.ValidateEmail(value);
_email = value;
}
}
public int Age { get; set; }
}
static void Main(string[] args)
{
try
{
User user = new User
{
Name = "Can",
Email = "can@test.com",
Age = 35
};
Console.WriteLine($"User: {user.Name}, Email: {user.Email}, Age: {user.Age}");
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
}
Geçersiz bir e-posta girersek (mesela can.test.com):
Invalid email address!
Evet, validasyonu tek bir metotta topladık ve her yerde kullanabiliriz. Ama bu, Primitive Obsession’dan tamamen kurtulduğumuz anlamına gelmez. Neden? Çünkü Email hâlâ bir string. Yani, bu alanın bir “e-posta adresi” olduğunu kod düzeyinde açıkça ifade etmiyoruz. Mesela, biri bu string’e bir telefon numarası atayabilir ve validasyon metodu bunu fark etmeyebilir (eğer daha karmaşık bir kontrol yazmadıysak). Ayrıca, bu yaklaşım bize veri tipinin anlamını değil, sadece bir kontrol mekanizması sunuyor. Oysa Email sınıfı kullandığımızda, hem anlam katıyoruz hem de kontrolü tipin kendisine gömüyoruz. Burada anlam katmak kritik nokta.
Kontrolü Tiplere Gömmek Ne Avantaj Sağlıyor?
Peki, kontrolü Email gibi bir sınıfa gömmek bize yazılım prensipleri açısından ne kazandırıyor?
- Encapsulation (Kapsülleme): Kontrolü Email sınıfına gömdüğümüzde, e-posta adresinin nasıl doğrulanacağı bilgisi tamamen bu sınıfın içinde kalıyor. Dışarıdan birinin bu mantığı bilmesine veya değiştirmesine gerek yok. Mesela, ValidationHelper.ValidateEmail kullandığımızda, bu metodun nasıl çalıştığını bilmek zorundayız ve bu metot başka bir yerde değişirse User sınıfı da etkilenebilir. Ama Email sınıfıyla, bu detaylar gizli kalıyor ve sadece Email nesnesini kullanmamız yetiyor.
- Low Coupling (Düşük Bağımlılık): ValidationHelper gibi bir yardımcı sınıf kullandığımızda, User sınıfı bu yardımcı sınıfa bağımlı hale geliyor. Yani, bir nevi coupling (bağımlılık) artıyor. Eğer ValidationHelper değişirse veya başka bir projede kullanılamaz hale gelirse, User sınıfını da değiştirmemiz gerekebilir. Oysa Email sınıfıyla, bağımlılık azalıyor; User sadece Email tipine bağlı ve bu tip kendi içinde bağımsız bir şekilde çalışıyor.
- Single Responsibility Principle (Tek Sorumluluk İlkesi): Email sınıfı, sadece bir e-posta adresini temsil etmek ve doğrulamakla sorumlu. Bu, her sınıfın tek bir işi olması gerektiği fikrine uyuyor. ValidationHelper yaklaşımında ise bu sorumluluk bir yardımcı sınıfa yayılıyor ve User sınıfı hala biraz “email doğrulama” işine bulaşmış oluyor.
- Type Safety (Tip Güvenliği): Email bir sınıf olduğu için, derleyici bize daha fazla güvenlik sağlıyor. Mesela, string yerine Email kullanırsak, yanlışlıkla bir telefon numarasını Email alanına atamaya çalışamayız — çünkü tipler uyuşmaz. ValidationHelper ile bu kontrol runtime’a kalıyor ve hata ancak çalışma zamanında fark ediliyor.
Özetle, kontrolü tipe gömmek, kodumuzu daha sağlam, bağımsız ve okunabilir hale getiriyor. Bağımlılıkları azaltıp, her bir parçanın kendi işini yapmasını sağlıyor. Bu da uzun vadede bakım yapmayı ve sistemi büyütmeyi kolaylaştırıyor.
Bu Çözüm Bize Ne Kazandırıyor?
- Anlam: Kod daha okunabilir hale geliyor. Email dediğimizde herkes bunun bir e-posta adresi olduğunu anlıyor.
- Güvenlik: Geçersiz veriler sisteme giremiyor, çünkü kontrol tek bir yerde ve tipin içinde.
- Yeniden Kullanım: Email, Age veya Money sınıflarını başka projelerde de kullanabiliriz.
- Bakım Kolaylığı: Bir şey değişirse (mesela yaş sınırı 200’e çıksın), sadece ilgili sınıfı güncelliyoruz.
Validasyon metotlarıda aslında bizi bir adım ileri götürür ama Primitive Obsession’ı tam anlamıyla çözmez çünkü sorun sadece validasyon değil, aynı zamanda tiplerin anlam eksikliği. Değer nesneleri bu anlamı da kazandırıyor ve yukarıda saydığımız prensiplerle kodumuzu daha temiz hale getiriyor.
Performans
Primitive Obsession problemini çözerken akla ilk gelen endişelerden biri performans olabilir. Bunu ilk duyduğumda benimde aklıma fazla fazla referans tipleri oluşturmak bize bir noktada sorun yaratır mı sorusu gelmişti. Referans tiplerini aşırı kullanmanın, özellikle sıkça oluşturulup yok edilen nesnelerde, ek bellek yükü ve garbage collector’ün daha fazla devreye girmesi nedeniyle bir maliyeti olabilir fakat bu etki genellikle, kodun okunabilirliği ve bakımı gibi uzun vadeli kazanımlar yanında görece sınırlı kalır ve modern sistemler için milyonlarca işlem yapmıyorsak bu çok büyük bir sorun olmayabilir. Yine de optimizasyon yapmakda mümküdür. Örneğin performansı iyileştirmek için referans tiplerini ihtiyaca göre struct gibi değer tipleriyle değiştirmek, küçük nesneler kullanmak veya bellek yönetimini bilinçli bir şekilde optimize etmek mümkündür. Bu yaklaşım, hem sürdürülebilir tasarım prensiplerini korur hem de performans sorunlarını çözmede yardımcı olur.
Sonuç olarak, kod yazarken sadece basit türlere güvenmenin başlangıçta kolay geldiğini hepimiz deneyimlemişizdir ancak “primitive obsession” uzun vadede başımıza dert açar. Primitive Obsession’dan kurtulursak ekip içinde ortak bir dil oluşur, iş gereksinimleri daha net belirlenir ve karmaşıklık azalabilir. Eğer kodumuzun gerçekten sağlam ve tutarlı olmasını istiyorsak, projeye uygun modelleme yöntemlerini erkenden benimsemek bizi ve ekibimizi büyük zahmetlerden kurtaracaktır.
İyi çalışmalar.