C++ ile Design Patterns
Bir şehir düşün. Her şehirde benzer problemler var: trafik yönetimi, su dağıtımı, çöp toplama. Ve bu problemlerin zaman içinde kanıtlanmış çözümleri oluşmuş — kavşaklar, boru hatları, toplama güzergahları. Her şehir bu çözümleri kendi coğrafyasına uyarlıyor ama temel prensip aynı. İşte design pattern'ler yazılım dünyasındaki bu kanıtlanmış çözümlerdir. "Bu problemi daha önce binlerce geliştirici yaşadı, en iyi çözüm bu şekilde" diyen kolektif bilgelik.
C++'ta design pattern'ler özellikle ilginç çünkü dil, diğer dillerde olmayan güçler sunuyor: deterministic destruction, template metaprogramming, value semantics, zero-cost abstractions. Bu yüzden bazı pattern'ler C++'a özgüdür (RAII, Pimpl, CRTP), bazıları ise klasik GoF pattern'lerinin C++ lezzetidir (Singleton, Factory, Observer, Strategy, Builder).
Bu derste her pattern için aynı formülü izleyeceğiz: Problem → Çözüm → Kod → Ne zaman kullanılır.
RAII — Resource Acquisition Is Initialization
Problem
C'de ve birçok dilde kaynakları (dosya, bellek, mutex, socket) manuel olarak alır ve serbest bırakırsın. Ve bu iş berbat sonuçlanır:
void tehlikeliFonksiyon() {
FILE* f = fopen("veri.txt", "r");
int* buffer = new int[1000];
// ... bir sürü iş ...
if (hataVar) {
// Oops! buffer ve f serbest bırakılmadı
return; // KAYNAK SIZINTISI (resource leak)
}
// ... daha fazla iş ...
delete[] buffer;
fclose(f);
}Her erken dönüş noktasında, her exception'da kaynakları serbest bırakmayı hatırlamak zorundasın. Kod büyüdükçe bu imkansız hale geliyor. Bir return eklemeyi unuttun mu? Leak. Bir exception fırladı mı? Leak. İç içe kaynaklar mı var? Kabus.
Çözüm: Kaynak Edinme = Başlatma
RAII'nin fikri dahice basit: Kaynağı bir nesnenin constructor'ında al, destructor'ında serbest bırak. C++'ta destructor'lar deterministik olarak çağrılır — nesne scope'tan çıktığında, exception fırladığında, her durumda. Bu garanti sayesinde kaynak sızıntısı yapısal olarak imkansız hale gelir.
#include <fstream>
#include <iostream>
#include <mutex>
#include <vector>
// RAII sarmalayıcı: dosya kaynağı
class DosyaYonetici {
private:
FILE* dosya_;
public:
explicit DosyaYonetici(const char* yol, const char* mod)
: dosya_(fopen(yol, mod))
{
if (!dosya_) {
throw std::runtime_error("Dosya acilamadi");
}
}
// Kopyalama yasak — kaynak tek bir yöneticiye ait
DosyaYonetici(const DosyaYonetici&) = delete;
DosyaYonetici& operator=(const DosyaYonetici&) = delete;
// Taşıma OK — sahiplik aktarılabilir
DosyaYonetici(DosyaYonetici&& other) noexcept : dosya_(other.dosya_) {
other.dosya_ = nullptr;
}
~DosyaYonetici() {
if (dosya_) {
fclose(dosya_); // Her koşulda temizlik
}
}
FILE* get() const { return dosya_; }
};
// Artık leak imkansız
void guvenliOkuma() {
DosyaYonetici dosya("veri.txt", "r");
// dosya scope'tan çıktığında otomatik kapanır
// exception fırlasa bile destructor çağrılır
char buffer[256];
while (fgets(buffer, sizeof(buffer), dosya.get())) {
std::cout << buffer;
}
} // <-- dosya burada otomatik kapanıyorStandart kütüphane zaten RAII ile dolu: std::unique_ptr, std::shared_ptr (bellek), std::fstream (dosya), std::lock_guard / std::unique_lock (mutex), std::jthread (thread).
#include <mutex>
std::mutex mtx;
void threadGuvenliFonksiyon() {
std::lock_guard<std::mutex> kilit(mtx); // lock alındı
// ... kritik bölge ...
} // <-- kilit scope'tan çıkınca unlock otomatikNe Zaman Kullanılır?
Her zaman. RAII bir pattern'den çok bir yaşam biçimidir C++'ta. Eğer new/delete, fopen/fclose, lock/unlock çiftleri yazıyorsan, RAII sarmalayıcı yaz. Modern C++ kodunda çıplak new ve delete neredeyse hiç bulunmaz.
💡 Altın kural: Eğer bir kaynağı manuel serbest bırakıyorsan, muhtemelen bir RAII sarmalayıcı yazmalısın — ya da standart kütüphanede zaten olan birini kullanmalısın.
Pimpl — Pointer to Implementation
Problem
C++'ta bir header dosyasını (*.h) değiştirdiğinde, o header'ı include eden tüm dosyalar yeniden derlenir. Büyük projelerde bu saatler sürebilir. Ayrıca header'daki private üyeler API'nin bir parçası haline gelir — binary uyumluluk (ABI compatibility) kırılır.
// widget.h — Kötü: tüm implementation detayları header'da
#include <string>
#include <vector>
#include <map>
#include "karmasik_kutuphane.h" // Bu header'ı include eden herkes bunu da çeker
class Widget {
public:
void isYap();
private:
std::string isim_;
std::vector<int> veriler_;
std::map<std::string, int> onbellek_;
KarmasikNesne dahiliNesne_; // Implementation detayı ama herkes biliyor
};Widget sınıfının private bölümünü değiştirdin mi? widget.h'ı include eden 200 dosya yeniden derlenir.
Çözüm: Compilation Firewall
Pimpl idiom'u private üyeleri bir forward-declared yapıya taşır. Header'da sadece bir pointer kalır:
// widget.h — Temiz: sadece public API görünür
#include <memory>
class Widget {
public:
Widget();
~Widget();
// Move operasyonları
Widget(Widget&& other) noexcept;
Widget& operator=(Widget&& other) noexcept;
void isYap();
void veriEkle(int deger);
int veriSayisi() const;
private:
struct Impl; // Forward declaration — sadece isim
std::unique_ptr<Impl> pImpl_; // Tek bir pointer
};// widget.cpp — Tüm detaylar burada
#include "widget.h"
#include <string>
#include <vector>
#include <map>
#include <iostream>
struct Widget::Impl {
std::string isim = "Varsayilan";
std::vector<int> veriler;
std::map<std::string, int> onbellek;
void dahiliHesaplama() {
for (auto& v : veriler) onbellek[std::to_string(v)] = v * 2;
}
};
Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // Destructor .cpp'de — Impl burada complete type
Widget::Widget(Widget&& other) noexcept = default;
Widget& Widget::operator=(Widget&& other) noexcept = default;
void Widget::isYap() {
pImpl_->dahiliHesaplama();
std::cout << "Islem tamam. " << pImpl_->onbellek.size()
<< " kayit islendi." << std::endl;
}
void Widget::veriEkle(int deger) { pImpl_->veriler.push_back(deger); }
int Widget::veriSayisi() const { return static_cast<int>(pImpl_->veriler.size()); }Artık Impl yapısını istediğin kadar değiştir — widget.h'ı include eden dosyalar yeniden derlenmez. Sadece widget.cpp derlenir.
Ne Zaman Kullanılır?
Kütüphane yazıyorsan: ABI uyumluluğu korumanız gerekiyorsa (yeni versiyon çıktığında kullananlar yeniden derlemek zorunda kalmasın)
Derleme süresi kritikse: Çok include edilen header'lar için
Implementation detaylarını gizlemek istiyorsan: Closed-source kütüphanelerde
Dezavantaj: Her fonksiyon çağrısında bir pointer indirection var (genelde ihmal edilebilir) ve heap allocation gerekiyor.
CRTP — Curiously Recurring Template Pattern
Problem
Virtual fonksiyonlar runtime'da bir vtable lookup yapar. Çoğu durumda bu maliyet ihmal edilebilir. Ama sıkı döngülerde (tight loops), oyun motorlarında, HFT (yüksek frekanslı ticaret) sistemlerinde her nanosaniye önemli. Hem polimorfik davranış istiyorsun, hem de virtual'ın maliyetini istemiyorsun.
Çözüm: Static Polymorphism
CRTP'de bir sınıf, kendisini template parametresi olarak base class'a verir. Compile-time'da hangi fonksiyonun çağrılacağı belirlenir — runtime vtable yok.
#include <iostream>
#include <cmath>
// Base class: türetilen sınıfı template parametresi olarak alıyor
template <typename Derived>
class Sekil {
public:
double alan() const {
// static_cast ile derived sınıfa erişim — compile-time'da çözülür
return static_cast<const Derived*>(this)->alanHesapla();
}
void bilgiYazdir() const {
std::cout << "Alan: " << alan() << std::endl;
}
};
class Daire : public Sekil<Daire> { // Kendini veriyor!
double yaricap_;
public:
explicit Daire(double r) : yaricap_(r) {}
double alanHesapla() const {
return M_PI * yaricap_ * yaricap_;
}
};
class Dikdortgen : public Sekil<Dikdortgen> {
double en_, boy_;
public:
Dikdortgen(double e, double b) : en_(e), boy_(b) {}
double alanHesapla() const {
return en_ * boy_;
}
};
// Compile-time polimorfizm — virtual yok, vtable yok
template <typename T>
void sekilBilgisi(const Sekil<T>& s) {
s.bilgiYazdir(); // Hangi fonksiyon çağrılacağı derleme zamanında belli
}
int main() {
Daire d(5.0);
Dikdortgen r(3.0, 4.0);
sekilBilgisi(d); // Alan: 78.5398
sekilBilgisi(r); // Alan: 12
return 0;
}Burada Sekil<Daire> ve Sekil<Dikdortgen> birbirinden tamamen farklı tiplerdir. Derleyici her biri için ayrı kod üretir ve fonksiyon çağrılarını inline edebilir. Virtual dispatch'in overhead'i sıfır.
CRTP'nin bir diğer güçlü kullanımı mixin pattern'dir — base class'tan derived class'a işlevsellik "enjekte etmek". Örneğin bir Karsilastirma<Derived> base class'ı, sadece == ve < tanımlaman karşılığında !=, >, <=, >= operatörlerini otomatik üretebilir. (C++20'de <=> spaceship operator bu spesifik kullanımı gereksiz kıldı, ama CRTP mixin'leri logging, serialization, cloning gibi alanlarda hâlâ çok güçlü.)
Ne Zaman Kullanılır?
Performans kritikse ve virtual dispatch maliyetinden kaçınmak istiyorsan
Mixin işlevsellik eklemek istiyorsan (derived class'a otomatik özellik kazandırma)
Static interface tanımlamak istiyorsan (C++20 concepts ile birlikte)
Heterogeneous collection'a (farklı tipleri aynı container'da) ihtiyacın yoksa. CRTP'de
Sekil<Daire>veSekil<Dikdortgen>farklı tipler — aynı vector'e koyamazsın.
Singleton — Tek Örnek Garantisi
Problem
Bazı nesnelerden sistemde sadece bir tane olmalı: loglama sistemi, konfigürasyon yöneticisi, connection pool, thread pool. Birden fazla instance oluşturmak tutarsızlığa veya kaynak israfına yol açar.
Çözüm: Meyers Singleton
C++11 sonrası en temiz ve thread-safe Singleton, Meyers Singleton olarak bilinen static local variable yaklaşımıdır. C++11 standardı, static local variable'ların thread-safe başlatılmasını garanti eder.
#include <iostream>
#include <string>
#include <fstream>
#include <mutex>
class Logger {
public:
// Tek erişim noktası
static Logger& instance() {
static Logger logger; // C++11: thread-safe initialization garantisi
return logger;
}
// Kopyalama ve taşıma yasak
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
void log(const std::string& mesaj) {
std::lock_guard<std::mutex> kilit(mutex_);
dosya_ << mesaj << std::endl;
std::cout << "[LOG] " << mesaj << std::endl;
}
void setSeviye(int seviye) { seviye_ = seviye; }
int getSeviye() const { return seviye_; }
private:
// Constructor private — dışarıdan kimse oluşturamaz
Logger() : dosya_("uygulama.log", std::ios::app), seviye_(0) {
std::cout << "Logger olusturuldu (tek sefer)" << std::endl;
}
~Logger() {
dosya_ << "=== Logger kapaniyor ===" << std::endl;
dosya_.close();
}
std::ofstream dosya_;
std::mutex mutex_;
int seviye_;
};
int main() {
// Her ikisi de aynı nesneye referans
Logger& log1 = Logger::instance();
Logger& log2 = Logger::instance();
log1.log("Uygulama basladi");
log2.log("Ayni logger, ayni dosya");
std::cout << "Ayni mi? " << (&log1 == &log2 ? "Evet" : "Hayir")
<< std::endl; // Evet
return 0;
}Alternatif olarak std::call_once ile de thread-safe başlatma yapılabilir. Bu özellikle heap allocation gereken veya başlatma sırasının kontrolü kritik olduğu durumlarda tercih edilir. Ama çoğu durumda Meyers Singleton yeterli ve daha basittir.
Ne Zaman Kullanılır?
Global state zorunluysa: Logger, konfigürasyon, kaynak havuzları
Dikkatli kullan: Singleton aslında global bir değişkendir. Testleri zorlaştırır, bağımlılıkları gizler. Mümkünse dependency injection tercih et.
Factory Method ve Abstract Factory
Problem
Nesne yaratma mantığı karmaşıklaştığında, new ile doğrudan oluşturma kodu bakımsız hale gelir. Farklı koşullara göre farklı nesneler yaratılması gerektiğinde client kodu kırılganlaşır.
Çözüm: Yaratmayı Soyutla
Factory Method, nesne oluşturmayı bir fonksiyona devreder. Client hangi concrete sınıfın oluşturulduğunu bilmez, sadece arayüzü (interface) kullanır.
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <functional>
#include <stdexcept>
// Ürün arayüzü
class Belge {
public:
virtual ~Belge() = default;
virtual void ac() = 0;
virtual void kaydet() = 0;
virtual std::string tur() const = 0;
};
// Concrete ürünler
class PdfBelge : public Belge {
public:
void ac() override {
std::cout << "PDF belgesi aciliyor (Adobe reader)..." << std::endl;
}
void kaydet() override {
std::cout << "PDF olarak kaydedildi." << std::endl;
}
std::string tur() const override { return "PDF"; }
};
class WordBelge : public Belge {
public:
void ac() override {
std::cout << "Word belgesi aciliyor (DOCX parser)..." << std::endl;
}
void kaydet() override {
std::cout << "DOCX olarak kaydedildi." << std::endl;
}
std::string tur() const override { return "Word"; }
};
class CsvBelge : public Belge {
public:
void ac() override {
std::cout << "CSV dosyasi aciliyor (tablo gorunumu)..." << std::endl;
}
void kaydet() override {
std::cout << "CSV olarak kaydedildi." << std::endl;
}
std::string tur() const override { return "CSV"; }
};
// Factory — kayıt tabanlı (registration-based)
class BelgeFactory {
public:
using Yaratici = std::function<std::unique_ptr<Belge>()>;
static BelgeFactory& instance() {
static BelgeFactory factory;
return factory;
}
void kaydet(const std::string& uzanti, Yaratici yaratici) {
yaraticilar_[uzanti] = std::move(yaratici);
}
std::unique_ptr<Belge> olustur(const std::string& uzanti) const {
auto it = yaraticilar_.find(uzanti);
if (it == yaraticilar_.end()) {
throw std::runtime_error("Bilinmeyen dosya turu: " + uzanti);
}
return it->second();
}
private:
std::unordered_map<std::string, Yaratici> yaraticilar_;
};
int main() {
auto& factory = BelgeFactory::instance();
// Factory'ye türleri kaydet
factory.kaydet("pdf", []() { return std::make_unique<PdfBelge>(); });
factory.kaydet("docx", []() { return std::make_unique<WordBelge>(); });
factory.kaydet("csv", []() { return std::make_unique<CsvBelge>(); });
// Client kodu — hangi sınıfın oluşturulduğunu bilmiyor
std::string dosyaAdi = "rapor.pdf";
std::string uzanti = dosyaAdi.substr(dosyaAdi.find_last_of('.') + 1);
auto belge = factory.olustur(uzanti);
belge->ac(); // PDF belgesi aciliyor (Adobe reader)...
belge->kaydet(); // PDF olarak kaydedildi.
return 0;
}Bu registration-based factory yaklaşımı, yeni belge türleri eklendiğinde factory kodunun değişmesini gerektirmez — sadece yeni türü kaydet. Open/Closed Principle (genişlemeye açık, değişikliğe kapalı) buna denir.
Abstract Factory ise birbiriyle ilişkili nesne ailelerini oluşturur. Örneğin bir UI toolkit'te Button ve TextBox birlikte değişmeli — Windows teması seçildiyse hepsi Windows stilinde olmalı. Soyut UIFactory arayüzünü WindowsFactory ve LinuxFactory implement eder, client kodu sadece factory arayüzüyle çalışır.
Ne Zaman Kullanılır?
Nesne oluşturma mantığı karmaşıksa veya koşullara bağlıysa
Client kodu concrete sınıflardan bağımsız olmalıysa
Plugin sistemleri: Yeni türler dinamik olarak eklenebilmeli
Observer — Olay Tabanlı İletişim
Problem
Bir nesne değiştiğinde, bağımlı nesnelerin otomatik bilgilendirilmesi gerekiyor. Ama bu nesneler birbirini doğrudan tanımamalı — aksi takdirde sıkı bağımlılık (tight coupling) oluşur.
Çözüm: Yayınla-Abone Ol (Publish-Subscribe)
#include <iostream>
#include <vector>
#include <string>
#include <functional>
#include <algorithm>
#include <unordered_map>
// Modern C++ Observer — std::function ile callback tabanlı
template <typename... Args>
class EventEmitter {
public:
using Callback = std::function<void(Args...)>;
using ListenerId = size_t;
ListenerId on(Callback callback) {
ListenerId id = sonrakiId_++;
dinleyiciler_.push_back({id, std::move(callback)});
return id;
}
void off(ListenerId id) {
dinleyiciler_.erase(
std::remove_if(dinleyiciler_.begin(), dinleyiciler_.end(),
[id](const auto& entry) { return entry.id == id; }),
dinleyiciler_.end()
);
}
void emit(Args... args) {
for (auto& entry : dinleyiciler_) {
entry.callback(args...);
}
}
size_t dinleyiciSayisi() const { return dinleyiciler_.size(); }
private:
struct Entry {
ListenerId id;
Callback callback;
};
std::vector<Entry> dinleyiciler_;
ListenerId sonrakiId_ = 0;
};
// Kullanım örneği: Fiyat değişim sistemi
class HisseSenedi {
public:
explicit HisseSenedi(std::string sembol, double fiyat)
: sembol_(std::move(sembol)), fiyat_(fiyat) {}
// Olaylara abone ol
EventEmitter<double, double>& fiyatDegisti() { return fiyatOlayi_; }
void fiyatGuncelle(double yeniFiyat) {
double eskiFiyat = fiyat_;
fiyat_ = yeniFiyat;
fiyatOlayi_.emit(eskiFiyat, yeniFiyat); // Tüm dinleyicilere bildir
}
const std::string& sembol() const { return sembol_; }
double fiyat() const { return fiyat_; }
private:
std::string sembol_;
double fiyat_;
EventEmitter<double, double> fiyatOlayi_;
};
int main() {
HisseSenedi apple("AAPL", 150.0);
// Dinleyici 1: Konsola yazdır
auto id1 = apple.fiyatDegisti().on([&](double eski, double yeni) {
std::cout << apple.sembol() << ": $" << eski
<< " -> $" << yeni << std::endl;
});
// Dinleyici 2: Büyük değişimlerde alarm
auto id2 = apple.fiyatDegisti().on([&](double eski, double yeni) {
double degisim = ((yeni - eski) / eski) * 100.0;
if (std::abs(degisim) > 5.0) {
std::cout << "⚠ ALARM: %" << degisim << " degisim!" << std::endl;
}
});
apple.fiyatGuncelle(155.0); // Normal bildirim
apple.fiyatGuncelle(180.0); // ALARM tetikler
// Bir dinleyiciyi kaldır
apple.fiyatDegisti().off(id1);
apple.fiyatGuncelle(175.0); // Sadece alarm dinleyicisi aktif
return 0;
}Ne Zaman Kullanılır?
Olay tabanlı sistemler: UI framework'leri, oyun motorları, mesajlaşma
Loose coupling istiyorsan: Yayıncı ve abone birbirini doğrudan tanımasın
Bir değişiklik birden fazla tepkiye yol açıyorsa
Strategy — Algoritma Değiştirme
Problem
Bir işlemi farklı yollarla yapmak istiyorsun — sıralama algoritması, fiyat hesaplama stratejisi, veri sıkıştırma yöntemi — ve bunu çalışma zamanında değiştirebilmek istiyorsun.
Çözüm: Algoritmayı Nesne Olarak Ayır
Klasik Strategy pattern soyut sınıf ve kalıtım kullanır. Modern C++'ta std::function ile çok daha temiz yazılır:
#include <iostream>
#include <vector>
#include <string>
#include <functional>
#include <numeric>
#include <algorithm>
// Modern Strategy: std::function ile
class FiyatHesaplayici {
public:
using Strateji = std::function<double(double)>;
void stratejiAyarla(Strateji strateji) {
strateji_ = std::move(strateji);
}
double hesapla(double bazFiyat) const {
if (!strateji_) {
return bazFiyat; // Varsayılan: indirim yok
}
return strateji_(bazFiyat);
}
private:
Strateji strateji_;
};
int main() {
FiyatHesaplayici hesaplayici;
double fiyat = 100.0;
// Strateji 1: Yüzde indirim
hesaplayici.stratejiAyarla([](double f) { return f * 0.90; }); // %10
std::cout << "Yuzde indirim: " << hesaplayici.hesapla(fiyat) << std::endl;
// Strateji 2: Sabit indirim
hesaplayici.stratejiAyarla([](double f) { return f - 15.0; });
std::cout << "Sabit indirim: " << hesaplayici.hesapla(fiyat) << std::endl;
// Strateji 3: Kademeli indirim (100+ ise %20, değilse %5)
hesaplayici.stratejiAyarla([](double f) {
return f >= 100.0 ? f * 0.80 : f * 0.95;
});
std::cout << "Kademeli indirim: " << hesaplayici.hesapla(fiyat) << std::endl;
return 0;
}Lambda kullanımı sayesinde yeni strateji eklemek için sınıf yazmaya gerek yok. std::function sınıf hiyerarşisi gerektirmeden stratejiyi taşır.
Daha karmaşık stratejilerde (state tutan, birden fazla fonksiyon gerektiren) sınıf hiyerarşisi hâlâ geçerlidir: soyut bir SiralamaStratejisi base class, HizliSiralama ve KararliSiralama concrete sınıfları, unique_ptr<SiralamaStratejisi> ile runtime'da değiştirilebilir strateji. Ama çoğu durumda std::function + lambda yeterli ve çok daha az boilerplate gerektirir.
Ne Zaman Kullanılır?
Aynı işi farklı yollarla yapman gerektiğinde
Algoritma çalışma zamanında değişecekse
if/else zinciri büyüyorsa: Her koşul dalını bir stratejiye dönüştür
Builder — Adım Adım Nesne İnşası
Problem
Bir sınıfın çok fazla parametresi var. Constructor'da 10 parametre mi geçireceksin? Hangisi hangisi belli olmuyor, bazıları opsiyonel...
// Kabus: 8 parametreli constructor
auto sorgu = Sorgu("users", true, false, 100, 0, "name", "ASC", true);
// Hangisi ne? Okunamaz. Hata yapmak çok kolay.Çözüm: Fluent Builder
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <optional>
class HttpIstek {
public:
// Builder sınıfı
class Builder {
public:
Builder(std::string url) : url_(std::move(url)) {}
Builder& metod(std::string m) { metod_ = std::move(m); return *this; }
Builder& header(std::string anahtar, std::string deger) {
headerlar_.push_back(std::move(anahtar) + ": " + std::move(deger));
return *this;
}
Builder& body(std::string b) { body_ = std::move(b); return *this; }
Builder& timeout(int saniye) { timeout_ = saniye; return *this; }
Builder& takipEt(bool t) { yonlendirmeTakip_ = t; return *this; }
HttpIstek build() {
return HttpIstek(std::move(*this));
}
private:
friend class HttpIstek;
std::string url_;
std::string metod_ = "GET";
std::vector<std::string> headerlar_;
std::optional<std::string> body_;
int timeout_ = 30;
bool yonlendirmeTakip_ = true;
};
void gonder() const {
std::cout << metod_ << " " << url_ << std::endl;
for (const auto& h : headerlar_) {
std::cout << " " << h << std::endl;
}
if (body_) {
std::cout << " Body: " << *body_ << std::endl;
}
std::cout << " Timeout: " << timeout_ << "s" << std::endl;
}
private:
explicit HttpIstek(Builder&& b)
: url_(std::move(b.url_))
, metod_(std::move(b.metod_))
, headerlar_(std::move(b.headerlar_))
, body_(std::move(b.body_))
, timeout_(b.timeout_)
, yonlendirmeTakip_(b.yonlendirmeTakip_) {}
std::string url_;
std::string metod_;
std::vector<std::string> headerlar_;
std::optional<std::string> body_;
int timeout_;
bool yonlendirmeTakip_;
};
int main() {
// Okunabilir, anlaşılır, güvenli
auto istek = HttpIstek::Builder("https://api.example.com/users")
.metod("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer xyz123")
.body(R"({"isim": "Tolga", "rol": "admin"})")
.timeout(10)
.build();
istek.gonder();
// Minimal istek — sadece zorunlu parametre
auto basitIstek = HttpIstek::Builder("https://example.com")
.build();
basitIstek.gonder();
return 0;
}Method chaining (return *this) sayesinde her adım bir sonrakine zincirlenir. Hangi parametrenin ne olduğu isimden belli — sıra önemli değil, opsiyonel olanlar atlanabilir.
Ne Zaman Kullanılır?
Constructor parametreleri 4-5'i geçtiyse
Bazı parametreler opsiyonelse
Nesne oluşturma adımları mantıksal bir sıra izliyorsa
İmmutable nesne istiyorsan (build sonrası değiştirilemez)
Gerçek Dünya Örneği: Pattern'leri Birleştirmek
Gerçek projelerde pattern'ler izole kullanılmaz — birbirini tamamlar. Bir log sistemi düşün:
Singleton:
LogSistemi::instance()— tek bir global loggerObserver:
handlerEkle(callback)— birden fazla çıkış noktası (konsol, dosya, remote server). Yeni çıkış eklemek için log sınıfını değiştirmeye gerek yok.Strategy:
formatAyarla(formatterFn)— log format mantığı (std::functionile) runtime'da değiştirilebilir. Basit format mı, zaman damgalı mı, JSON mı? Stratejiyi değiştir.RAII:
shared_ptr<ofstream>ile dosya handler'ı — dosya kapanışı otomatik, leak imkansız.
Dört pattern, tek bir sınıfta harmonik şekilde çalışır. Her birinin kendi sorumluluğu var ve hiçbiri diğerinin alanına karışmaz.
Pattern Seçim Rehberi
| Durum | Pattern | C++ Özelliği |
|---|---|---|
| Kaynak yönetimi (dosya, bellek, lock) | RAII | Destructor, smart pointer |
| Header'da implementation gizleme | Pimpl | unique_ptr, forward declaration |
| Virtual olmadan polimorfizm | CRTP | Template |
| Tek instance garantisi | Singleton | Static local variable |
| Koşula göre nesne oluşturma | Factory | unique_ptr, std::function |
| Olay bildirimi, loose coupling | Observer | std::function, std::vector |
| Algoritma değiştirme | Strategy | std::function, lambda |
| Çok parametreli nesne inşası | Builder | Method chaining |
Özet
RAII C++'ın en temel pattern'idir — kaynağı constructor'da al, destructor'da bırak. Leak yapısal olarak imkansız hale gelir.
Pimpl header bağımlılığını keser, derleme süresini düşürür, ABI uyumluluğu korur.
CRTP compile-time polimorfizm sağlar — virtual dispatch overhead'i sıfır. Mixin pattern için de güçlüdür.
Singleton global tek instance garantiler. Meyers Singleton (static local) C++11'de thread-safe'dir. Ama aşırı kullanma — test edilebilirliği düşürür.
Factory nesne oluşturmayı soyutlar. Registration-based factory ile Open/Closed Principle sağlanır.
Observer olay tabanlı sistemlerin temelini oluşturur.
std::functionile modern C++'ta çok temiz yazılır.Strategy algoritmayı nesne olarak ayırır. Lambda +
std::functionile sınıf hiyerarşisine gerek kalmaz.Builder çok parametreli nesneleri okunabilir ve güvenli şekilde oluşturur.
Pattern'ler izole kullanılmaz — gerçek projelerde birbirini tamamlar. Önemli olan problemi tanımak ve doğru pattern'i seçmektir.
AI Asistan
Sorularını yanıtlamaya hazır