Software Architecture

Nesne Yönelimli ve Fonksiyonel Programlamayı Uzlaştırma: Fonksiyonel Programlamada SOLİD İlkelerini Uygulama

İlk bakışta Nesne Yönelimli Programlama (NYP) ve Fonksiyonel Programlama (FP) paradigmaları zıt kutuplar gibi görünür. NYP değişken durum, miras ve kapsüllemeye dayanırken FP değişmezlik, saf fonksiyonlar ve veri dönüşümlerini savunur. Ancak her ikisinin de temel hedefi aynıdır: sürdürülebilir, ölçeklenebilir ve test edilebilir yazılım geliştirmek. Başlangıçta NYP için formüle edilen SOLID ilkeleri, bu hedef için sağlam bir çerçeve sunar. Düşünceli bir şekilde uygulandıklarında, bu ilkeler fonksiyonel kavramlarla şaşırtıcı derecede iyi örtüşür ve her iki dünyanın da güçlü yönlerini en üst düzeye çıkaran hibrit bir yaklaşım sunar.

Tek Sorumluluk İlkesi (SRP): Bir Fonksiyon, Bir İş

NYP'de SRP, bir sınıfın değişmesi için yalnızca tek bir nedeni olmalıdır der. Fonksiyonel programlamada bu, fonksiyonların küçük tutulmasına ve tek bir dönüşüme odaklanmasına karşılık gelir. Farklı nedenlerle değişebilecek birden fazla yönteme sahip büyük bir sınıf oluşturmak yerine, mantığı küçük, saf fonksiyonlara ayırırız.

// Kötü: Çok fazla iş yapan bir fonksiyon
function processOrder(order) {
    validateOrder(order);
    calculateTax(order);
    sendEmail(order.customer);
    updateDatabase(order);
}

// İyi: Küçük, odaklanmış, saf fonksiyonlar
const validateOrder = (order) => order.items.length > 0;
const calculateTax = (amount) => amount * 0.08;
const createInvoice = (order, tax) => ({ ...order, tax });

function processOrder(order) {
    if (!validateOrder(order)) throw new Error("Geçersiz Sipariş");
    const tax = calculateTax(order.total);
    const invoice = createInvoice(order, tax);
    // Yan etkiler ayrı olarak veya monadlar aracılığıyla ele alınır
    return invoice;
}

Her fonksiyonun tek bir şeyi iyi yapmasını sağlayarak, vergi mantığındaki değişikliklerin doğrulama mantığını bozmasını engelleriz. Bu modülerlik, SRP'nin fonksiyonel yorumudur.

Açık/Kapalı İlkesi (OCP): Değiştirmeden Davranışı Genişletme

OCP, varlıkların genişletilmeye açık ancak değiştirilmeye kapalı olması gerektiğini belirtir. FP'de bunu, fonksiyon parametreleri olarak uygulanan yüksek sıralı fonksiyonlar ve strateji desenleri aracılığıyla elde ederiz. Yeni bir `Kare` eklemek için bir `Şekil` sınıfından türetmek yerine, bir hesaplama fonksiyonunu argüman olarak geçiririz.

// NYP yaklaşımı temel sınıfı değiştirmeyi veya alt sınıflar oluşturmayı gerektirir
// FP yaklaşımı: Bileşim ve fonksiyon parametrelerini kullanın

const areaCalculator = (shape) => {
    switch (shape.type) {
        case 'circle':
            return Math.PI * shape.radius ** 2;
        case 'square':
            return shape.side ** 2;
        case 'rectangle':
            return shape.width * shape.height;
        default:
            throw new Error("Bilinmeyen şekil");
    }
};

// 'Üçgen' eklemek için hesaplama fonksiyonunu değiştirirsiniz.
// OCP'ye sıkı sıkıya uymak için bir arama tablosu kullanabiliriz:

const areaStrategies = {
    circle: (s) => Math.PI * s.radius ** 2,
    square: (s) => s.side ** 2,
    triangle: (s) => 0.5 * s.base * s.height // Yeni genişletme
};

const getArea = (shape) => areaStrategies[shape.type](shape);

Arama tablosu anahtar eklemek için hâlâ değişiklik gerektirse de, değişikliği çekirdek mantık yerine veri tanımına izole eder. Daha ileri düzey FP teknikleri, strateji fonksiyonunun kendisini bir bağımlılık olarak geçirmeyi içerir; bu da `getArea` fonksiyonunun işlediği belirli şekillerden tamamen habersiz olmasını sağlar.

Liskov Değiştirilebilirlik İlkesi (LSP): Tip Güvenliği ve Sözleşmeler

LSP, `S`, `T`'nin bir alt tipi ise, `T`'nin `S` ile değiştirilmesinin programın doğruluğunu değiştirmemesi gerektiğini garanti eder. Fonksiyonel dillerde LSP genellikle güçlü tip sistemleri ve cebirsel veri tipleri aracılığıyla sağlanır. Bir fonksiyon bir `Hayvan` bekliyorsa ve siz bir `Köpek` geçirirseniz, fonksiyon yine de doğru davranmalıdır çünkü `Köpek`, `Hayvan`'ın yapısal veya isimsel gereksinimlerini karşılar.

// Yapısal tiplendirme yoluyla LSP'yi gösteren TypeScript örneği
interface Animal {
    name: string;
    eat(): void;
}

function feed(animal: Animal) {
    console.log(`${animal.name} besleniyor`);
    animal.eat();
}

class Dog implements Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    eat(): void {
        console.log(`${this.name} mama yiyor`);
    }
    // Dog, feed() fonksiyonuna güvenle geçirilebilir
}

Saf FP'de, miras tuzağından kaçınmak için bileşime ve açık tip kısıtlamalarına güveniriz. Bir fonksiyon `f: A -> B` ise, aynı yan etki kısıtlamalarına uyan, `A` kabul eden ve `B` döndüren herhangi bir fonksiyon geçerli bir ikame olabilir.

Arayüz Ayrıştırma İlkesi (ISP): İnce Tanımlı Arayüzler

ISP, hiçbir istemcinin kullanmadığı yöntemlere bağımlı kalmaya zorlanmaması gerektiğini önerir. NYP'de bu, daha küçük arayüzler oluşturularak sağlanır. FP'de, arayüzleri aynı şekilde kullanmadığımız için, ISP'yi büyük nesneler yerine belirli fonksiyonların fonksiyon ayrıştırması ve bağımlılık enjeksiyonu yoluyla elde ederiz.

Yalnızca 2'sine ihtiyaç duyulurken 20 yöntemi olan büyük bir `UserManager` nesnesi geçirmek yerine, gereken iki belirli fonksiyonu geçiririz:

// Kötü: Büyük bir nesneye sıkı bağlanma
function renderProfile(userManager: UserManager) {
    const user = userManager.getUser();
    const photos = userManager.getPhotos();
    // ...
}

// İyi: Belirli fonksiyonlar aracılığıyla gevşek bağlanma
function renderProfile(
    getUser: () => User,
    getPhotos: (userId: string) => Photo[]
) {
    const user = getUser();
    const photos = getPhotos(user.id);
    // ...
}

Bu yaklaşım, `renderProfile`'ın yalnızca gereksinim duyduğu kesin davranışa bağımlı olmasını sağlar, bu da test etmeyi ve değiştirmeyi kolaylaştırır.

Bağımlılık Tersine Çevirme İlkesi (DIP): Uygulama Üzerinde Soyutlama

DIP, üst düzey modüllerin alt düzey modüllere bağımlı olmaması gerektiğini; her ikisinin de soyutlamalara bağımlı olması gerektiğini belirtir. FP'de bunu, tiplere (soyutlamalara) bağımlı olarak ve çalışma zamanında somut uygulamaları (fonksiyonları) enjekte ederek elde ederiz. Bu, fonksiyonel dillerde Test Odaklı Geliştirme'nin (TDD) çekirdeğidir.

// Soyutlamaya (tip) bağımlı üst düzey modül
type DataStore = {
    save: (item: Item) => Promise;
    fetch: (id: string) => Promise;
};

function processItem(store: DataStore, item: Item) {
    // Yalnızca DataStore arayüzüne bağımlı mantık
    return store.save(item);
}

// Alt düzey modüller (uygulamalar)
const memoryStore: DataStore = {
    save: async (item) => { /* ... */ },
    fetch: async (id) => { /* ... */ }
};

const dbStore: DataStore = {
    save: async (item) => { /* ... */ },
    fetch: async (id) => { /* ... */ }
};

// Farklı soyutlamalarla üst düzey fonksiyonu kullanma
processItem(memoryStore, item1);
processItem(dbStore, item2);

`DataStore`'u bir tip takma adı veya arayüz olarak tanımlayarak bir soyutlama oluştururuz. `processItem` fonksiyonu, belirli uygulamaya değil bu soyutlamaya bağımlıdır. Bu, iş mantığını değiştirmeden veri deposunu test veya üretim için değiştirmemizi sağlar.

Sonuç: Sağlam Mimari İçin Hibrit Bir Yaklaşım

Fonksiyonel programlamada SOLID ilkelerini uygulamak, NYP desenlerini FP koduna zorlamak değil, altta yatan tasarım hedeflerini tanımaktır: modülerlik, test edilebilirlik ve sürdürülebilirlik. Saf fonksiyonları, değişmezliği ve güçlü tip sistemlerini kullanarak, fonksiyonel kodun avantajlarından yararlanırken SOLID'in ruhunu karşılayabiliriz. Sonuç, hangi paradigmayı vurgulamayı seçersek seçelim, akıl yürütmesi, test etmesi ve genişletmesi daha kolay yazılımdır.

Sonraki sisteminizi mimarilerken, bu ilkeleri katı kurallar olarak değil, birlikte uyum içinde çalışan bileşenler oluşturmanıza yardımcı olan rehber ışıklar olarak düşünün. JavaScript, TypeScript, Scala veya Rust kullanıyor olmanız fark etmez, temel fikir aynı kalır: değişim için tasarlayın ve kodunuz, evrilen gereksinimler karşısında dayanıklı kalacaktır.

Share: