C# Interface ve Abstract Class Farkı: Hangisi Ne Zaman?
6 dk okuma

C# derslerinde en çok karıştırılan iki kavram interface ve abstract class. İkisi de "doğrudan nesnesi oluşturulamayan, başkaları için şablon olan tip" gibi görünür ve sınavda "farkı nedir?" diye sorulduğunda çoğu cevap ezber bir tabloya dönüşür. Oysa fark tek cümlede özetlenebilir: interface bir yetenek sözleşmesidir ("bunu yapabilirim"), abstract class ise yarım bırakılmış bir sınıftır ("ben buyum, ama bir kısmımı türeyen sınıf tamamlayacak"). Bu yazıda ikisinin neler içerebildiğini, ne zaman hangisini seçeceğini ve sınavlarda nasıl sorulduğunu bir ödeme sistemi örneği üzerinden anlatıyorum.
Interface: Bir Yetenek Sözleşmesi
Interface, bir tipin dışarıya hangi üyeleri sunacağını söyler; o üyelerin nasıl çalışacağıyla ilgilenmez. Constructor yazısındaki e-ticaret siparişini hatırlayın. Siparişin ödenmesi gerekiyor ama sipariş sınıfı ödemenin kartla mı havaleyle mi yapıldığını bilmek zorunda değil:
public interface IOdemeYontemi
{
string Ad { get; }
bool Ode(decimal tutar);
}
public interface IIadeEdilebilir
{
bool IadeEt(decimal tutar);
}Dikkat edilecek noktalar: üyelerin gövdesi yok, erişim belirleyici yazmadık (interface üyeleri varsayılan olarak public'tir) ve alan (field) tanımlayamıyoruz. Interface durum (state) tutmaz; sadece "bu tipte şu metot ve property'ler vardır" der.
Abstract Class: Yarım Bırakılmış Sınıf
Abstract class ise gerçek bir sınıftır: alanı, constructor'ı, gövdeli metotları olabilir. Tek farkı, new ile doğrudan nesnesinin oluşturulamaması ve isterse bazı üyelerini abstract bırakarak türeyen sınıfa "bunu sen yaz" diyebilmesidir.
Ödeme yöntemlerinin ortak bir akışı var: tutarı kontrol et, komisyonu ekle, ödemeyi gerçekleştir, sonucu kaydet. Bu akışı her sınıfta tekrar yazmak yerine bir kez yazalım:
public abstract class OdemeBase : IOdemeYontemi
{
private readonly List<string> _kayitlar = new();
protected OdemeBase(string ad)
{
Ad = ad;
}
public string Ad { get; }
public IReadOnlyList<string> Kayitlar => _kayitlar;
// Ortak akış: her ödeme yöntemi aynı adımlardan geçer
public bool Ode(decimal tutar)
{
if (tutar <= 0)
{
Kaydet("Geçersiz tutar reddedildi");
return false;
}
decimal toplam = tutar + KomisyonHesapla(tutar);
bool sonuc = OdemeyiGerceklestir(toplam);
Kaydet($"{toplam:N2} TL -> {(sonuc ? "başarılı" : "başarısız")}");
return sonuc;
}
// Türeyen sınıf yazmak ZORUNDA
protected abstract bool OdemeyiGerceklestir(decimal toplam);
// Türeyen sınıf isterse değiştirir
protected virtual decimal KomisyonHesapla(decimal tutar) => 0m;
protected void Kaydet(string mesaj) => _kayitlar.Add($"[{Ad}] {mesaj}");
}Burada interface'in yapamayacağı üç şey var: _kayitlar adında özel bir alan, protected bir constructor ve türeyen sınıfların paylaştığı hazır kod. protected kelimesi size yabancıysa access modifiers yazısına göz atın; abstract, virtual ve override üçlüsünün ayrıntısı ise kalıtım ve polymorphism rehberinde.
Hangisi Neleri İçerebilir?
| Özellik | interface | abstract class |
|---|---|---|
| Instance alanı (field) | Hayır | Evet |
| Constructor | Hayır | Evet (türeyen sınıf base(...) ile çağırır) |
| Gövdeli metot | Sadece varsayılan implementasyon olarak | Evet |
| Gövdesiz üye | Evet (varsayılan hali) | Evet (abstract ile) |
| Erişim belirleyiciler | Varsayılan public |
Hepsi kullanılabilir |
| Bir sınıf kaç tane alabilir? | İstediği kadar | Sadece bir tane |
struct kullanabilir mi? |
Evet | Hayır |
new ile nesne |
Oluşturulamaz | Oluşturulamaz |
Varsayılan Interface Üyeleri
"Interface'te gövde olmaz" cümlesi C# 8'den beri tam doğru değil. Bir interface üyesine varsayılan implementasyon yazabilirsiniz:
public interface IOdemeYontemi
{
string Ad { get; }
bool Ode(decimal tutar);
// Varsayılan (default) implementasyon - C# 8 ve sonrası
string Ozet() => $"{Ad} ile ödeme";
}Bu özelliğin asıl amacı, yayınlanmış bir interface'e yeni üye eklerken onu uygulayan mevcut sınıfları bozmamaktır. Çalışma zamanı desteği gerektirdiği için eski .NET Framework'te kullanılamaz; modern .NET'te sorun yok. İki sınır aynen duruyor: interface hâlâ instance alanı tutamaz ve constructor'ı olamaz. Yani varsayılan üyeler interface'i abstract class'a çevirmez. Interface'ler ayrıca statik üyeler de içerebilir ama başlangıç seviyesinde bunlara ihtiyacınız olmayacak.
Tek Kalıtım, Çoklu Interface
C#'ta bir sınıfın yalnızca bir taban sınıfı olabilir, ama istediği kadar interface uygulayabilir. Kredi kartı hem ortak ödeme akışını miras alıyor hem de iade yeteneğini ekliyor:
public class KrediKarti : OdemeBase, IIadeEdilebilir
{
private readonly string _kartNo;
public KrediKarti(string kartNo) : base("Kredi Kartı")
{
_kartNo = kartNo;
}
protected override bool OdemeyiGerceklestir(decimal toplam)
{
// Gerçek projede burada banka servisi çağrılır
return _kartNo.Length == 16;
}
protected override decimal KomisyonHesapla(decimal tutar) => tutar * 0.02m;
public bool IadeEt(decimal tutar)
{
Kaydet($"{tutar:N2} TL iade edildi");
return true;
}
}
public class Havale : OdemeBase
{
private readonly string _iban;
public Havale(string iban) : base("Havale")
{
_iban = iban;
}
protected override bool OdemeyiGerceklestir(decimal toplam) => _iban.StartsWith("TR");
}Önemli bir ayrıntı: taban sınıfı kullanmak zorunlu değil. Ortak akışa uymayan bir ödeme yöntemi interface'i doğrudan uygulayabilir:
public class HediyeCeki : IOdemeYontemi
{
private decimal _kalan;
public HediyeCeki(decimal tutar)
{
_kalan = tutar;
}
public string Ad => "Hediye Çeki";
public bool Ode(decimal tutar)
{
if (tutar <= 0 || tutar > _kalan) return false;
_kalan -= tutar;
return true;
}
}Mini Senaryo: Sepet Ödeme Ekranı
Bir sepet ekranı düşünün: kullanıcı ödeme yöntemini seçiyor, sistem ödemeyi deniyor ve sonuca göre mesaj gösteriyor. Ekran kodunun somut sınıfları bilmesine gerek yok:
var sepetTutari = 1250m;
List<IOdemeYontemi> yontemler =
[
new KrediKarti("4111111111111111"),
new Havale("TR00 0000 0000 0000 0000 0000 00"),
new HediyeCeki(500m)
];
foreach (var yontem in yontemler)
{
bool basarili = yontem.Ode(sepetTutari);
Console.WriteLine($"{yontem.Ozet()} -> {(basarili ? "onaylandı" : "reddedildi")}");
if (basarili && yontem is IIadeEdilebilir iade)
Console.WriteLine($" İade mümkün: {iade.IadeEt(100m)}");
}Çıktı:
Kredi Kartı ile ödeme -> onaylandı
İade mümkün: True
Havale ile ödeme -> onaylandı
Hediye Çeki ile ödeme -> reddedildiDöngü üç farklı sınıfla çalışıyor ama sadece IOdemeYontemi'ni tanıyor. Yarın "Kripto" diye bir yöntem eklenirse döngüye dokunmazsınız. İade yeteneği ise ayrı bir interface olduğu için is ile sorulabiliyor: hediye çeki iade edilemiyor ve bunu bize derleyici seviyesinde belli ediyor. Aynı fikir ASP.NET Core'daki dependency injection'ın da temelidir; Minimal API başlangıç yazısında servisi bir interface üzerinden kaydedip kullanıyoruz.
.NET'in kendisi de bu yaklaşımla dolu: Array.Sort veya List<T>.Sort elemanları karşılaştırabilmek için tipinizin IComparable<T> uygulamasını bekler. Sıralamanın arka planda nasıl yapıldığını merak ediyorsanız sıralama algoritmaları sınav soruları yazısı iyi bir devam olur.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
Interface seçin:
- Birbiriyle akraba olmayan tipler aynı yeteneği paylaşacaksa (
IComparable<T>,IDisposablegibi). - Bir sınıfın birden fazla rol üstlenmesi gerekiyorsa.
- Test yazarken ya da dependency injection'da gerçek sınıfın yerine sahtesini koymak istiyorsanız.
structtipleri de sözleşmeye dahil olacaksa.
Abstract class seçin:
- Türeyen sınıflar gerçekten "bir çeşit X" ise ve ortak alan ya da ortak kod paylaşıyorlarsa.
- Yukarıdaki
Odemetodu gibi sabit bir akışın sadece bazı adımları değişecekse. - Constructor'da zorunlu kılmak istediğiniz ortak veriler varsa.
İkisini birlikte kullanın: Pratikte en sık gördüğünüz düzen budur. Interface sözleşmeyi tanımlar, abstract class tekrarlanan kısmı üstlenir, dışarıdaki kod sadece interface'i bilir.
Hiçbirini kullanmayın: Tek bir implementasyon varsa ve ikincisinin geleceğine dair somut bir neden yoksa, normal bir sınıf yeter. Her sınıfa refleks olarak bir I... interface'i açmak kodu okunmaz yapar. Yalnızca birkaç sabit değer taşıyan durumlar için de enum veya record daha doğru araçtır.
Sık Yapılan Hatalar
1. Interface üyesini eksik bırakmak
Belirti: CS0535: 'KrediKarti' does not implement interface member 'IOdemeYontemi.Ode(decimal)'. Çözüm: üyeyi aynı imzayla ve public olarak yazın. Abstract class tarafındaki karşılığı CS0534'tür; orada çözüm override ile yazmak ya da sınıfı da abstract yapmaktır.
2. Abstract class'tan nesne oluşturmaya çalışmak
var odeme = new OdemeBase("Test");
// CS0144: Cannot create an instance of the abstract type or interface 'OdemeBase'
IOdemeYontemi odeme2 = new Havale("TR00 ..."); // Doğrusu: somut sınıftan oluştur3. Varsayılan interface üyesini sınıf değişkeninden çağırmak
Varsayılan üyeler sınıfa miras geçmez, sadece interface tipi üzerinden görünür:
var kart = new KrediKarti("4111111111111111");
// kart.Ozet(); // CS1061: 'KrediKarti' does not contain a definition for 'Ozet'
IOdemeYontemi yontem = kart;
Console.WriteLine(yontem.Ozet()); // Kredi Kartı ile ödeme4. Taban sınıfı interface'lerden sonra yazmak
class KrediKarti : IIadeEdilebilir, OdemeBase yazarsanız CS1722: Base class 'OdemeBase' must come before any interfaces hatasını alırsınız. Sıra her zaman: önce taban sınıf, sonra interface'ler.
Sınavda Çıkan Tipik Sorular
Soru 1: Abstract class'ın nesnesi oluşturulamıyorsa constructor'ı ne işe yarar?
Türeyen sınıfın nesnesi oluşturulurken önce taban sınıfın constructor'ı çalışır. Abstract class bu sayede ortak alanlarını kendisi başlatır. Örnekteki base("Kredi Kartı") çağrısı tam olarak budur.
Soru 2: Hiç abstract üyesi olmayan bir abstract class yazılabilir mi?
Evet. abstract kelimesi sadece "bu sınıftan doğrudan nesne oluşturma" anlamına gelir. Tersi ise geçerli değildir: abstract üye içeren bir sınıf mutlaka abstract olmalıdır.
Soru 3: Bir sınıf iki abstract class'tan türeyebilir mi?
Hayır. C#'ta sınıflar için çoklu kalıtım yoktur; derleyici CS1721 hatası verir. Birden fazla interface uygulamak ise serbesttir.
Soru 4: İki interface'te aynı imzalı metot varsa ne olur?
Tek bir public metot ikisini birden karşılar. Farklı davranmaları gerekiyorsa açık (explicit) implementasyon kullanılır:
public interface IYazici { void Baslat(); }
public interface ITarayici { void Baslat(); }
public class CokIslevliYazici : IYazici, ITarayici
{
void IYazici.Baslat() => Console.WriteLine("Yazdırma başladı");
void ITarayici.Baslat() => Console.WriteLine("Tarama başladı");
}
var cihaz = new CokIslevliYazici();
((IYazici)cihaz).Baslat(); // Yazdırma başladı
((ITarayici)cihaz).Baslat(); // Tarama başladı
// cihaz.Baslat(); // Derlenmez: açık üyeler sadece interface üzerinden görünürSoru 5: Interface içinde alan tanımlanabilir mi?
Instance alanı tanımlanamaz, derleyici CS0525 verir. Property tanımlanabilir, çünkü property aslında bir metot çiftidir.
Bu tarz soruları kâğıt üzerinde çözme pratiği yapmak isterseniz sınav destek sayfasına da bakabilirsiniz.
Sık Sorulan Sorular
Interface mi daha hızlı, abstract class mı?
Aradaki fark günlük uygulama kodunda ölçülebilir bir sorun yaratmaz. Seçimi performansa göre değil tasarıma göre yapın: yetenek mi tanımlıyorsunuz, ortak kod mu paylaşıyorsunuz?
Bir abstract class interface uygulayabilir mi?
Evet, örnekteki OdemeBase tam olarak bunu yapıyor. Hatta interface üyelerini kendisi yazmak yerine abstract olarak işaretleyip türeyen sınıflara da bırakabilir.
Varsayılan interface üyeleri abstract class'ı gereksiz kılar mı?
Hayır. Interface hâlâ instance alanı ve constructor içeremez, yani durum tutamaz. Ortak veri ve ortak başlatma mantığı gerekiyorsa abstract class yerini korur.
Interface isimleri neden I harfiyle başlıyor?
Bu bir dil kuralı değil, .NET dünyasının yerleşik isimlendirme alışkanlığıdır. Kodu okuyan kişi IOdemeYontemi gördüğünde bunun bir sözleşme olduğunu hemen anlar; bu yüzden uymanızı öneririm.
İlgili Yazılar
C# Kalıtım ve Polymorphism: virtual, override, new Rehberi
C# kalıtım ve polymorphism rehberi: base, constructor zinciri, virtual/override ile new farkı, sealed, is/as ve klasik "çıktı nedir?" sınav sorusu.
OOP'ye Giriş: Neden Prosedürel Programlama Artık Yetmiyor?
Prosedürel programlamadan nesne yönelimliye geçiş. Spaghetti koddan kurtulup gerçek dünya problemlerini koda dönüştürmeyi keşfedin.
C# Constructor: Nesne Fabrikasının Gizli Kahramanı
Constructor nedir, neden gerekli? Default constructor, parametreli constructor ve constructor overloading kavramlarını gerçek dünya örnekleriyle keşfediyoruz.