C# programlamada en çok karıştırılan konulardan biri olan Abstract Class ve Interface kavramlarını, pratik bir araç kiralama örneği üzerinden detaylı olarak inceleyeceğiz. Bu yazıda, ne zaman hangisini kullanacağınızı ve aralarındaki temel farkları öğreneceksiniz.

Senaryo: Araç Kiralama Sistemi

Bir araç kiralama yazılımı yazdığınızı düşünün. Bu yazılımda farklı türde araçlarınız olacak: Arabalar, Motosikletler, Kamyonlar vb.

Bu örnek üzerinden hem Abstract Class hem de Interface kavramlarını pratik olarak göreceğiz ve hangi durumda hangisini kullanmamız gerektiğini anlayacağız.

Soyut Sınıf (Abstract Class) Örneği: Arac

Diyelim ki tüm araçların ortak bazı temel özellikleri ve bir başlangıç değeri olmalı: her aracın bir markası, bir modeli ve tüm araçlara otomatik olarak atanacak bir seri numarası (Id) olmalı. Ayrıca, her araç hareket edebilir ama her araç farklı şekilde hareket eder.

C# Arac.cs
public abstract class Arac
{
    // Ortak Özellikler (Durum tutabiliriz)
    public Guid Id { get; set; } // Tüm araçların benzersiz bir ID'si olacak
    public string Marka { get; set; }
    public string Model { get; set; }

    // Yapıcı metot (Constructor) - ID'yi otomatik atıyoruz
    public Arac(string marka, string model)
    {
        Id = Guid.NewGuid(); // Her araç oluşturulduğunda benzersiz ID alır
        Marka = marka;
        Model = model;
    }

    // Ortak SOMUT Metot (Tüm araçlar için aynı davranış)
    public void BilgileriGoster()
    {
        Console.WriteLine($"ID: {Id}, Marka: {Marka}, Model: {Model}");
    }

    // Ortak SOYUT Metot (Her araç farklı hareket eder, alt sınıflar bunu uygulamak zorunda)
    public abstract void HareketEt();
}

Neden abstract class Arac?

  • Ortak Alanlar/Özellikler: Id, Marka, Model gibi tüm araçlarda bulunacak alanları tanımlayabiliriz.
  • Ortak Başlatma Mantığı: Yapıcı metot sayesinde her Arac nesnesi oluştuğunda Id'sinin otomatik olarak Guid.NewGuid() ile atanmasını sağlayabiliriz. Bir arayüzde yapıcı metot olamaz.
  • Ortak Somut Davranış: BilgileriGoster() gibi tüm araçların aynı şekilde çalışacak metotlarını yazabiliriz.
  • Zorunlu Soyut Davranış: HareketEt() metodu soyut olduğu için, Arac sınıfından türeyen her sınıf (örneğin Araba, Motosiklet) bu metodu kendi içinde uygulamak zorundadır.

Şimdi bu Arac sınıfından türeyen gerçek araçları oluşturalım:

C# Araba.cs
public class Araba : Arac
{
    public int KapiSayisi { get; set; }

    public Araba(string marka, string model, int kapiSayisi)
        : base(marka, model) // Base class'ın (Arac) yapıcı metodunu çağırıyoruz
    {
        KapiSayisi = kapiSayisi;
    }

    // Soyut metodu uygulamak zorundayız
    public override void HareketEt()
    {
        Console.WriteLine("Araba dört teker üzerinde ilerliyor.");
    }
}
C# Motosiklet.cs
public class Motosiklet : Arac
{
    public bool TekerlekliYanCantasiVarMi { get; set; }

    public Motosiklet(string marka, string model, bool tekerlekliYanCantasiVarMi)
        : base(marka, model) // Base class'ın (Arac) yapıcı metodunu çağırıyoruz
    {
        TekerlekliYanCantasiVarMi = tekerlekliYanCantasiVarMi;
    }

    // Soyut metodu uygulamak zorundayız
    public override void HareketEt()
    {
        Console.WriteLine("Motosiklet iki teker üzerinde hızla ilerliyor.");
    }
}

Arayüz (Interface) Örneği: IKiralayabilir ve IYukTasiyabilir

Şimdi, bazı araçların kiralanabilir, bazı araçların yük taşıyabilir özelliklere sahip olması gerektiğini düşünelim. Bir araba hem kiralanabilir hem de yük taşıyabilir, bir motorsiklet sadece kiralanabilir olabilir.

C# IKiralayabilir.cs
// Kiralama yeteneği olan her varlığın bu metodu uygulaması gerektiğini belirten bir sözleşme
public interface IKiralayabilir
{
    // İmza tanımlıyoruz, uygulama detayı yok
    void Kirala(int gunSayisi);
    decimal HesaplaKiraBedeli(int gunSayisi, decimal gunlukFiyat);
}
C# IYukTasiyabilir.cs
// Yük taşıma yeteneği olan her varlığın bu metodu uygulaması gerektiğini belirten bir sözleşme
public interface IYukTasiyabilir
{
    // İmza tanımlıyoruz, uygulama detayı yok
    void YukDoldur(int miktar);
    void YukBosalt(int miktar);
}

Şimdi bu arayüzleri araçlarımıza uygulayalım:

C# Araba.cs - Updated
public class Araba : Arac, IKiralayabilir // Araba, Arac'tan miras alır ve IKiralayabilir'i uygular
{
    public int KapiSayisi { get; set; }

    public Araba(string marka, string model, int kapiSayisi)
        : base(marka, model)
    {
        KapiSayisi = kapiSayisi;
    }

    public override void HareketEt()
    {
        Console.WriteLine("Araba dört teker üzerinde ilerliyor.");
    }

    // IKiralayabilir arayüzünün metotlarını uyguluyoruz
    public void Kirala(int gunSayisi)
    {
        Console.WriteLine($"{Marka} {Model} {gunSayisi} günlüğüne kiralandı.");
    }

    public decimal HesaplaKiraBedeli(int gunSayisi, decimal gunlukFiyat)
    {
        return gunSayisi * gunlukFiyat;
    }
}
C# Kamyon.cs
public class Kamyon : Arac, IYukTasiyabilir, IKiralayabilir // Kamyon hem yük taşıyabilir hem kiralanabilir
{
    public int KapasiteTon { get; set; }

    public Kamyon(string marka, string model, int kapasiteTon)
        : base(marka, model)
    {
        KapasiteTon = kapasiteTon;
    }

    public override void HareketEt()
    {
        Console.WriteLine("Kamyon ağır yüklerle yolda ilerliyor.");
    }

    // IYukTasiyabilir arayüzünün metotlarını uyguluyoruz
    public void YukDoldur(int miktar)
    {
        Console.WriteLine($"{Marka} {Model}'e {miktar} ton yük yüklendi.");
    }

    public void YukBosalt(int miktar)
    {
        Console.WriteLine($"{Marka} {Model}'den {miktar} ton yük boşaltıldı.");
    }

    // IKiralayabilir arayüzünün metotlarını uyguluyoruz (örneğin kamyonları da kiralayabiliriz)
    public void Kirala(int gunSayisi)
    {
        Console.WriteLine($"{Marka} {Model} {gunSayisi} günlüğüne kiralandı (kamyon).");
    }

    public decimal HesaplaKiraBedeli(int gunSayisi, decimal gunlukFiyat)
    {
        return gunSayisi * gunlukFiyat * 1.5m; // Kamyon olduğu için daha pahalı olabilir
    }
}

Pratik Kullanım Örneği

C# Program.cs
class Program
{
    static void Main(string[] args)
    {
        // Farklı araç türleri oluşturuyoruz
        var araba = new Araba("Toyota", "Corolla", 4);
        var motosiklet = new Motosiklet("Yamaha", "R1", false);
        var kamyon = new Kamyon("Mercedes", "Actros", 25);

        // Tüm araçlar ortak Arac sınıfından türediği için aynı şekilde kullanabiliyoruz
        Console.WriteLine("=== Araç Bilgileri ===");
        araba.BilgileriGoster();
        motosiklet.BilgileriGoster();
        kamyon.BilgileriGoster();

        Console.WriteLine("\n=== Hareket Şekilleri ===");
        araba.HareketEt();
        motosiklet.HareketEt();
        kamyon.HareketEt();

        // Interface'ler sayesinde farklı yetenekleri kullanabiliyoruz
        Console.WriteLine("\n=== Kiralama İşlemleri ===");
        araba.Kirala(7);
        kamyon.Kirala(3);

        Console.WriteLine("\n=== Yük Taşıma İşlemleri ===");
        kamyon.YukDoldur(15);
        kamyon.YukBosalt(10);

        // Polimorfizm örneği
        Console.WriteLine("\n=== Polimorfizm Örneği ===");
        List<IKiralayabilir> kiralanabilirAraclar = new List<IKiralayabilir>
        {
            araba,
            kamyon
        };

        foreach (var arac in kiralanabilirAraclar)
        {
            arac.Kirala(5);
        }
    }
}

Ne Zaman Hangisini Kullanmalıyız?

Abstract Class

VS

Interface

Ortak Durum Paylaşımı

Tüm türetilmiş sınıfların ortak bir temel Id'ye, yaratılma/güncellenme zamanına vb. sahip olmasını garanti etmek istediğinizde.

Yetenek Sözleşmesi

Farklı sınıfların ortak bir "yapabilirlik" (capability) veya "sözleşme" paylaşmasını istediğinizde.

Varsayılan Implementasyon

Bazı metotların varsayılan (somut) uygulamalarını sağlamak ve kod tekrarını önlemek istediğinizde.

Çoklu Yetenek

Birden fazla "yapabilirliği" bir sınıfa eklemek istediğinizde. Bir sınıf birden fazla arayüz uygulayabilir.

Tek Kalıtım

Türetilmiş sınıfların paylaşması gereken durum (fields/properties) varsa ve sadece tek bir temel tipte olmasını istediğinizde.

Esnek Implementasyon

Uygulama detaylarının her sınıfa bırakılmasını veya varsayılan arayüz metotları ile çok genel bir uygulama sağlamak istediğinizde.

Karar Ağacı

Ortak duruma (fields/properties) ihtiyacım var mı?
Evet
Abstract Class kullan Örnek: Entity base class
Hayır
Birden fazla yetenek mi ekleyeceğim?
Evet
Interface kullan Örnek: IKiralayabilir + IYukTasiyabilir
Hayır
Her ikisi de uygun Tercihe göre karar ver

Özet ve Önemli Noktalar

Abstract Class

  • Ortak durum (state) paylaşımı
  • Yapıcı metot (constructor) olabilir
  • Hem somut hem soyut metotlar
  • Tek kalıtım (single inheritance)
  • Varsayılan implementasyon sağlar

Interface

  • Sadece sözleşme (contract) tanımlar
  • Yapıcı metot olamaz
  • Sadece metot imzaları (C# 8.0+ default methods)
  • Çoklu implementasyon (multiple inheritance)
  • Esneklik ve polimorfizm sağlar

Dikkat Edilmesi Gerekenler

  • Abstract class'tan direkt nesne oluşturamazsınız
  • Interface'lerdeki tüm metotları implement etmek zorundasınız
  • Bir sınıf sadece bir abstract class'tan türeyebilir ama birden fazla interface implement edebilir
  • Abstract metotlar mutlaka override edilmelidir

Sonuç

Abstract Class ve Interface kavramları, nesne yönelimli programlamanın temel taşlarıdır. Doğru kullanıldığında kodunuzun daha modüler, esnek ve sürdürülebilir olmasını sağlarlar.

Araç kiralama örneğinde gördüğümüz gibi, Abstract Class ortak özellikleri ve davranışları paylaşmak için, Interface ise farklı yetenekleri (capabilities) tanımlamak için kullanılır. Her ikisinin de kendine özgü kullanım alanları vardır ve doğru seçim yapmak kodunuzun kalitesini önemli ölçüde artırır.

Sonraki Adımlar

  • Bu örnekleri kendi IDE'nizde çalıştırarak deneyimleyin
  • Farklı senaryolar için Abstract Class ve Interface tasarımları yapın
  • SOLID prensiplerine göre bu yapıları nasıl kullanabileceğinizi araştırın

Video Kaynaklar

Konuyu pekiştirmek için önerilen videolar: