C++ Soyut Sınıflar ve Arayüzler (Abstract Classes & Interfaces)
C++’ta soyut sınıflar ve arayüzleri öğrenin. Pure virtual fonksiyonlar, interface tasarımı ve polimorfizm örneklerle anlatılıyor.
C++’ta soyut sınıflar (abstract classes), en az bir saf sanal (pure virtual) fonksiyon içeren ve doğrudan örneklenemeyen sınıflardır. “Arayüz” (interface) kavramı C++’ta ayrı bir anahtar sözcükle gelmez; bunun yerine tümü saf sanal fonksiyonlardan oluşan soyut bir sınıf, arayüz rolü görür. Bu yazıda soyut sınıf/arayüz tasarımını, çoklu arayüz uygulamayı, bağımlılık tersine çevirme (DIP) yaklaşımını ve pratik bir Book senaryosunu ele alacağız.
1) Saf Sanal Fonksiyon ve Soyut Sınıf
En az bir üyenin = 0 ile işaretlendiği sanal fonksiyon, saf sanaldır. Böyle bir sınıf soyut olur ve
doğrudan nesnesi oluşturulamaz.
struct IPrintable {
virtual ~IPrintable() = default; // Arayüzlerde de sanal yıkıcı iyi pratiktir
virtual void Print() const = 0; // saf sanal → arayüz sözleşmesi
};
struct IEntity {
virtual ~IEntity() = default;
virtual std::string Key() const = 0;
};
Bu iki sınıf artık birer “arayüz”dür. Uygulayan sınıflar Print() ve Key() davranışını tanımlamak zorundadır.
2) Book Tabanı: Soyut Sınıf
Ortak sözleşmeyi soyut bir BookBase sınıfıyla ifade edelim. Fiyat hesabını alt sınıflar belirleyecek.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct BookBase : IPrintable, IEntity {
protected:
string title, author;
public:
BookBase(string t, string a) : title(move(t)), author(move(a)) {}
virtual ~BookBase() = default;
// Saf sanal: alt sınıflar fiyatı hesaplar
virtual double FinalPrice() const = 0;
// IPrintable sözleşmesi
void Print() const override {
cout << title << " - " << author
<< " | " << FinalPrice() << " TL" << endl;
}
// IEntity sözleşmesi
string Key() const override { return title + ":" + author; }
};
BookBase doğrudan new edilemez; çünkü FinalPrice() saf sanaldır. Ancak ortak davranış olarak
Print() ve Key() için varsayılan bir uygulama sağlayabildik.
3) Arayüz Uygulayan Sınıflar (EBook / PrintedBook)
Şimdi iki farklı kitap türü oluşturalım ve FinalPrice() sözleşmesini yerine getirelim.
struct EBook : BookBase {
double base, vat;
EBook(string t, string a, double b, double v)
: BookBase(move(t), move(a)), base(b), vat(v) {}
double FinalPrice() const override { return base * (1.0 + vat); }
};
enum class Cover { Soft, Hard };
struct PrintedBook : BookBase {
double base, shipping; int pages; Cover cover;
PrintedBook(string t, string a, double b, double s, int p, Cover c)
: BookBase(move(t), move(a)), base(b), shipping(s), pages(p), cover(c) {}
double FinalPrice() const override {
double cost = base + shipping + 0.04 * pages;
if (cover == Cover::Hard) cost *= 1.1;
return cost;
}
};
Her iki sınıf da BookBase arayüzünü sağlıyor; bu sayede polimorfik kullanılabilirler.
4) Çoklu Arayüz Uygulaması (Interface Segregation)
Tek büyük bir arayüz yerine, küçük ve odaklı arayüzler tasarlamak bakım maliyetini düşürür.
Örneğin bir IDiscountable arayüzü ile indirim politikasını ayırabiliriz.
struct IDiscountable {
virtual ~IDiscountable() = default;
virtual double ApplyDiscount(double rate) const = 0; // 0.10 → %10
};
struct DiscountedEBook : EBook, IDiscountable {
using EBook::EBook; // kurucuları devral
double ApplyDiscount(double rate) const override {
return FinalPrice() * (1.0 - rate);
}
};
Bu tasarımla DiscountedEBook hem BookBase sözleşmesini hem de indirim arayüzünü destekler.
5) Bağımlılık Tersine Çevirme (DIP) ve Test Edilebilirlik
Sınıflar somut türlere değil, soyutlamalara (arayüzlere) bağımlı olmalıdır. Böylece modüller gevşek bağlanır ve
birim testleri kolaylaşır. Aşağıda bir “fiyatlandırma motoru” yalnızca BookBase arayüzünü görür.
struct PricingEngine {
double Total(const vector<shared_ptr<BookBase>>& items) const {
double sum = 0.0;
for (const auto& b : items) sum += b->FinalPrice();
return sum;
}
};
int main() {
vector<shared_ptr<BookBase>> items;
items.push_back(make_shared<EBook>("Modern C++", "A. Dev", 100, 0.1));
items.push_back(make_shared<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, Cover::Hard));
PricingEngine eng;
cout << "Toplam: " << eng.Total(items) << " TL" << endl;
for (const auto& b : items) b->Print(); // IPrintable
}
PricingEngine somut türe bağımlı değildir; yalnızca BookBase arayüzüyle çalışır. Bu sayede
EBook yerine bir MockBook vererek birim testleri yazabilirsiniz.
6) Çoklu Kalıtımda Arayüzler
C++ çoklu kalıtımı destekler; arayüzler için bu genelde güvenlidir çünkü arayüzler durum (state) taşımaz. Durum taşıyan taban sınıflarda çoklu kalıtım diamond problem riski yaratır; uygun olan yerde virtual inheritance veya tercihen composition kullanın.
struct IStorable { virtual ~IStorable()=default; virtual void Save() const =0; };
struct ISearchable{ virtual ~ISearchable()=default; virtual bool Matches(string) const =0; };
struct SearchableBook : BookBase, IStorable, ISearchable {
using BookBase::BookBase;
double FinalPrice() const override { return 0.0; } // örnek basit
void Save() const override { /* dosyaya yaz... */ }
bool Matches(string q) const override {
return title.find(q)!=string::npos || author.find(q)!=string::npos;
}
};
7) Arayüzle Fabrika (Factory) Örneği
Oluşturma mantığını somut sınıflardan ayırmak için fabrika arayüzü kullanabiliriz.
struct IBookFactory {
virtual ~IBookFactory() = default;
virtual shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v)=0;
virtual shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c)=0;
};
struct DefaultBookFactory : IBookFactory {
shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v) override {
return make_shared<EBook>(move(t), move(a), b, v);
}
shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c) override {
return make_shared<PrintedBook>(move(t), move(a), b, s, p, c);
}
};
Üst düzey modüller yalnızca IBookFactory’ye bağımlıdır; somut üretim kolayca değiştirilebilir veya testlerde taklit edilebilir.
8) Tasarım İpuçları
- Arayüzlerde daima virtual destructor tanımlayın.
- Arayüzleri küçük, odaklı tutun (Interface Segregation Principle).
- Somut sınıflara değil, soyutlamalara bağımlı olun (DIP).
- Polimorfik kullanımlarda pointer/referans tercih edin; değerle kopyalama slicing yaratır.
- Çoklu kalıtım yalnızca davranış (arayüz) paylaşımında tercih edilmeli; durum için composition daha sağlıklıdır.
9) Sık Karşılaşılan Hatalar
- Soyut sınıfta sanal yıkıcıyı unutmak.
- Arayüzü gereğinden büyük tasarlamak (uygulayıcıların boş metot yazmasına yol açar).
- Somut sınıflara doğrudan bağımlılık (test edilebilirliği ve değişebilirliği azaltır).
- Çoklu kalıtımda durum (state) barındıran tabanları birleştirip diamond problem oluşturmak.
10) TL;DR
- C++’ta “arayüz” = tümü saf sanal fonksiyonlardan oluşan soyut sınıf.
- Soyut sınıflar doğrudan örneklenemez; alt sınıf sözleşmeyi override ederek somutlaşır.
- Küçük ve odaklı arayüzler (ISP) ve soyutlamalara bağımlılık (DIP) tercih edin.
- Polimorfizm için pointer/referans kullanın; sanal yıkıcıyı unutmayın.
- Çoklu kalıtımı arayüzlerde kullanın; durum paylaşımı için composition daha güvenlidir.
- Tüm örnekler Visual Studio 2022 ve GCC 11+ üzerinde çalıştırılabilir.