Takip et

Yazılımda Karmaşıklık Neden Hak Edilmelidir?

Yazılım geliştirirken neden en karmaşık mimarileri seçiyoruz? Yazılımda karmaşıklık kontrol edilmediğinde projeleri nasıl çıkmaza sokar?

Yazılımda Karmaşıklık Neden Hak Edilmelidir?

Yazılım geliştirirken neden en karmaşık mimarileri seçiyoruz? Yazılımda karmaşıklık kontrol edilmediğinde projeleri nasıl çıkmaza sokar? İşte yanıtlar.

Yazılımda Karmaşıklık Nedir ve Neden Kaçınılmalıdır?

Yazılım dünyasında yeni bir projeye başlarken hepimiz en güncel teknolojileri, en havalı mimari desenleri (design patterns) ve en gelişmiş kütüphaneleri kullanmak isteriz. Ancak, bu istek genellikle projenin erken aşamalarında büyük bir felaketle sonuçlanır. Yazılımda karmaşıklık, bir sistemin anlaşılmasını, bakımının yapılmasını ve değiştirilmesini zorlaştıran her türlü yapısal durumu ifade eder. Fred Brooks’un ünlü “No Silver Bullet” (Gümüş Kurşun Yoktur) makalesinde belirttiği gibi, karmaşıklığı iki temel kategoriye ayırabiliriz: Zorunlu karmaşıklık (essential complexity) ve yapay karmaşıklık (accidental complexity).

Zorunlu karmaşıklık, çözmeye çalıştığınız iş probleminin kendisinden kaynaklanır. Örneğin, bir bankacılık sistemindeki yasal mevzuatlar veya karmaşık faiz hesaplama formülleri zorunlu bir karmaşıklık yaratır. Bu durumdan kaçınmanız neredeyse imkansızdır. Diğer yandan, yapay karmaşıklık ise tamamen bizim verdiğimiz teknik kararların bir sonucudur. Henüz on aktif kullanıcısı bile olmayan bir e-ticaret sitesi için mikroservis (microservices) mimarisi kurmaya çalışırsanız, kendi elinizle yapay bir karmaşıklık yaratmış olursunuz. Bu durum, yazılım geliştirme sürecini ciddi şekilde yavaşlatır, hata yapma riskini artırır ve proje bütçesini hızla tüketir.

Bu nedenle, yazılımda karmaşıklık her zaman hak edilmelidir. Basit bir problemi çözmek için gereksiz derecede karmaşık araçlar kullanmak, sineği öldürmek için top atmaya benzer. Geliştiriciler olarak asıl görevimiz, en karmaşık kodu yazmak değil, iş hedeflerine en sade ve sürdürülebilir yoldan ulaşmaktır. Eğer kod tabanınızı (codebase) gereksiz soyutlamalarla doldurursanız, gelecekte en ufak bir özelliği eklemek bile işkenceye dönüşecektir. Sonuç olarak, sadelik her zaman ilk hedefiniz olmalıdır.

Erken Optimizasyon Tuzağına Nasıl Düşüyoruz?

Birçok yazılım geliştirici, gelecekte karşılaşabilecekleri olası sorunları bugünden çözmeye çalışır. Bu durum literatürde “erken optimizasyon” (premature optimization) olarak adlandırılır. Bilgisayar biliminin öncülerinden Donald Knuth’un da belirttiği gibi, erken optimizasyon yazılımdaki tüm kötülüklerin anasıdır. Peki, biz yazılımcılar bu tuzağa tam olarak nasıl düşüyoruz?

Genellikle popüler teknoloji bloglarını okuyarak veya büyük teknoloji devlerinin (Netflix, Google, Amazon gibi) mimari çözümlerini taklit ederek bu süreci başlatıyoruz. “Eğer Netflix mikroservis kullanıyorsa, bizim de kesinlikle kullanmamız gerekir” düşüncesi son derece yaygın ama bir o kadar da hatalı bir yaklaşımdır. Çünkü Netflix’in çözmeye çalıştığı küresel ölçeklenme sorunları ile sizin yerel pazar için geliştirdiğiniz SaaS (yazılım servisi) uygulamasının sorunları tamamen farklıdır.

Erken aşamada eklenen her soyutlama katmanı (abstraction layer), her yeni veritabanı ve her harici servis, sistemin bakım maliyetini katlayarak artırır. Örneğin, henüz veri tabanınızda sadece birkaç bin satır veri varken NoSQL veya dağıtık veritabanı sistemlerine geçiş yapmak, sorgu yazmayı zorlaştırmaktan başka bir işe yaramaz. Sonuç olarak, henüz pazar uyumunu (product-market fit) yakalayamamış bir girişim, teknik borçların (technical debt) altında ezilerek başarısız olabilir. Bu yüzden, gelecekteki hayali ihtiyaçlar için bugünden karmaşık sistemler tasarlamaktan kesinlikle kaçınmalıyız. Kod tabanınızı olabildiğince esnek ve basit tutarak, gerçek ihtiyaçlar ortaya çıktığında kolayca evrilebilecek bir yapı kurmalısınız.

Basit Başlayıp Adım Adım Ölçekleme Nasıl Yapılır?

Peki, bir projeye başlarken sadeliği nasıl koruyabiliriz? İlk kural olarak, her zaman en basit çalışan modeli (MVP – Minimum Viable Product) hedeflemelisiniz. Kodunuzu yazarken gelecekteki genişleme ihtimallerini düşünün ancak bu ihtimaller için şimdiden karmaşık sınıflar ve arayüzler (interfaces) tanımlamayın. Şimdi, konuyu somutlaştırmak için Türkiye pazarında sıkça karşılaştığımız bir senaryoyu ele alalım. Bir kullanıcıya SMS göndermek istiyoruz. İlk aşamada sadece tek bir SMS sağlayıcısı ile çalışacağız.

Bunun için doğrudan karmaşık bir tasarım deseni (design pattern) uygulamak yerine, basit bir sınıf ile işe başlayabiliriz:

// Aşama 1: Basit ve Doğrudan Çözüm
class SmsService {
    async sendSms(phoneNumber, message) {
        // Entegrasyon kodları doğrudan buraya yazılır
        console.log(${phoneNumber} numarasına SMS gönderildi: ${message});
        return { success: true };
    }
}

Bu kod başlangıç için tamamen yeterlidir ve işimizi görür. Ancak, işlerimiz büyüdü ve artık sadece SMS değil, e-posta ve anlık bildirimler (push notifications) de göndermek istiyoruz. Ayrıca, SMS sağlayıcımızın kesinti yaşaması durumunda yedek bir sağlayıcıya geçiş yapmamız gerekiyor. İşte bu noktada, yazılımda karmaşıklık seviyesini artırmayı “hak etmiş” oluruz. Artık kodumuzu soyutlama katmanları ekleyerek yeniden yapılandırabiliriz (refactoring):

// Aşama 2: Hak Edilmiş Karmaşıklık - Soyutlama ve Polimorfizm
class NotificationProvider {
    async send(recipient, content) {
        throw new Error("Bu metod alt sınıflar tarafından override edilmelidir.");
    }
}

class SmsProvider extends NotificationProvider {
    async send(phoneNumber, content) {
        console.log(SMS gönderildi: ${content});
        return true;
    }
}

class EmailProvider extends NotificationProvider {
    async send(email, content) {
        console.log(E-posta gönderildi: ${content});
        return true;
    }
}

class NotificationService {
    constructor(provider) {
        this.provider = provider;
    }

    async notify(recipient, content) {
        return await this.provider.send(recipient, content);
    }
}

Gördüğünüz gibi, ikinci aşamadaki karmaşıklık sisteme rastgele eklenmedi. Tam tersine, iş gereksinimlerinin değişmesi ve sistemin büyümesi sonucunda bu mimariyi hak ettik. Eğer ilk gün bu yapıyla başlasaydık, gereksiz yere zaman kaybedecek ve kodun okunabilirliğini boş yere azaltacaktık. Bu nedenle, adımları her zaman kademeli olarak atmalıyız.

Yazılım Mimarilerinde “Kazanılan Karmaşıklık” Ne Demektir?

“Complexity should be earned” (Karmaşıklık kazanılmalıdır) prensibi, bir sistemin karmaşıklık seviyesinin, o sistemin çözdüğü problemin ölçeği ile doğru orantılı olması gerektiğini savunur. Eğer sisteminiz küçük bir kullanıcı kitlesine hizmet ediyorsa, mimariniz de buna uygun olarak minimalist olmalıdır. Ne zaman ki kullanıcı sayınız artar, veritabanınız darboğaz (bottleneck) yaşamaya başlar veya geliştirici ekibiniz büyür; işte o zaman daha karmaşık mimari çözümlere geçiş yapabilirsiniz.

Aşağıdaki tabloda, hak edilmemiş karmaşıklık ile hak edilmiş karmaşıklık arasındaki temel farkları detaylıca inceleyebilirsiniz:

Kriter Hak Edilmemiş Karmaşıklık (Overengineering) Hak Edilmiş Karmaşıklık (Earned Complexity)
Tetikleyici Unsur Geliştiricinin kişisel merakı veya popüler trendler. Gerçek performans darboğazları ve iş gereksinimleri.
Geliştirme Hızı Gereksiz soyutlamalar yüzünden oldukça yavaştır. Sadece ihtiyaç duyulan alanlarda esneklik sağlar.
Bakım Kolaylığı Kod tabanını anlamak için günler harcamak gerekir. Sadece gerekli yerlerde soyutlama yapıldığı için anlaşılırdır.
Altyapı Maliyeti Gereksiz sunucu, kuyruk ve veritabanı masrafları yaratır. Yatırımın karşılığını (ROI) doğrudan performans olarak verir.

Bu tablo bize net bir şekilde gösteriyor ki, teknik kararlar alırken her zaman “Neden bu çözüme ihtiyacımız var?” sorusunu sormalıyız. Eğer cevabımız “Çünkü bu teknoloji çok popüler” ise, orada hak edilmemiş bir karmaşıklık söz konusudur. Ancak cevabımız “Çünkü mevcut SQL veritabanımız saniyede 10.000 eşzamanlı isteği karşılayamıyor” ise, karmaşıklığı artırmanın ve yeni teknolojiler entegre etmenin zamanı gelmiştir.

Karmaşıklık Seviyesini Belirleyen Gerçek Dünya Kriterleri Nelerdir?

Peki, yazılımda karmaşıklık seviyesini artırmaya karar verirken hangi somut kriterleri göz önünde bulundurmalıyız? Bu kararı sezgisel hislerle değil, tamamen verilere dayanarak almalısınız. İlk olarak, Conway Yasası (Conway’s Law) bize organizasyonların kendi iletişim yapılarını kopyalayan sistemler tasarladığını söyler. Eğer 3 veya 4 kişilik küçük bir yazılım ekibiniz varsa, sistemi 15 farklı mikroservise bölmek ekipler arası iletişimi ve deployment (canlıya alma) süreçlerini imkansız hale getirecektir. Bu durumda monolitik (tek parça) bir mimari ile devam etmek en doğrusudur.

İkinci kriter ise veri trafiği ve işlem hacmidir. Saniyede sadece 5-10 işlem yapan bir sistem için CQRS (Command Query Responsibility Segregation – Komut Sorgu Sorumluluklarının Ayrılması) deseni uygulamak gereksizdir. Çünkü bu desen, veri yazma ve okuma işlemlerini ayırarak sistemi karmaşıklaştırır. Üçüncü olarak, iş alanının (domain) karmaşıklığı belirleyicidir. Eğer karmaşık bir finansal hesaplama, lojistik rotalama veya büyük bir ERP sistemi geliştiriyorsanız, DDD (Domain Driven Design – Etki Alanı Odaklı Tasarım) prensiplerini uygulamak mantıklı hale gelir. Ancak basit bir içerik yönetim sistemi (CMS) veya blog sitesi için DDD kullanmak projeyi baştan çıkmaza sokmak demektir. Sonuç olarak, karmaşıklığı artırmadan önce ekibinizin yetkinliğini, bütçenizi ve gerçek kullanıcı metriklerini dikkatlice analiz etmelisiniz.

Büyük Şirketler Karmaşıklığı Nasıl Yönetiyor?

Türkiye’deki büyük e-ticaret ve teknoloji şirketlerinin gelişim süreçlerine baktığımızda, bu prensibin ne kadar hayati olduğunu net bir şekilde görürüz. Bu şirketlerin hiçbiri faaliyetlerine ilk gün yüzlerce mikroservis ve devasa Kubernetes cluster’ları (kümeleri) ile başlamadı. İlk yıllarında, hızlıca pazara çıkabilmek ve fikirlerini doğrulamak için tek bir monolitik uygulama kullandılar. Zamanla kullanıcı sayısı milyonlara ulaştığında ve monolitik yapı artık ölçeklenemez hale geldiğinde, mimariyi parça parça mikroservislere dönüştürdüler.

Bu dönüşüm esnasında her bir mikroservisin sınırları, gerçek trafik verilerine ve ekiplerin sorumluluk alanlarına göre belirlendi. Örneğin, ödeme sistemi (payment gateway) çok kritik ve yüksek trafik alan bir bölüm olduğu için ilk olarak o parça monolitten ayrıştırıldı. Bu durum, karmaşıklığın adım adım ve hak edilerek kazanıldığının en güzel kanıtıdır. Eğer bu şirketler ilk gün mikroservis mimarisi ile yola çıksalardı, muhtemelen dağıtık sistemlerin getirdiği ağ gecikmesi (network latency) ve veri tutarlılığı (data consistency) gibi karmaşık problemlerle baş edemeyip pazarda başarısız olacaklardı. Dolayısıyla, büyük şirketlerin bugünkü durumunu değil, o duruma gelene kadar geçtikleri aşamaları kendimize örnek almalıyız.

Yazılımda Karmaşıklık Yönetimi Hakkında Sıkça Sorulan Sorular

Mikroservis mimarisine ne zaman geçiş yapmalıyız?

Mikroservis mimarisine geçiş yapmak için iki temel şartın sağlanması gerekir. Birincisi, monolitik uygulamanızın artık dikey veya yatay olarak ölçeklenememesi ve performans darboğazı yaratmasıdır. İkincisi ise, yazılım ekibinizin büyüyerek tek bir kod tabanı üzerinde çakışmadan çalışmakta zorlanmasıdır. Eğer ekibiniz 10 kişiden azsa ve trafik sorununuz yoksa, mikroservis mimarisi size faydadan çok zarar getirecektir.

Yazılımda teknik borç (technical debt) her zaman kötü bir şey midir?

Hayır, teknik borç her zaman kötü değildir. Tıpkı finansal borçlar gibi, teknik borçlar da doğru yönetildiğinde işinizi hızlandırabilir. Pazara hızlı girmek ve fikirlerinizi test etmek için bilinçli olarak bazı teknik borçlar alabilirsiniz (örneğin test yazımını ertelemek veya basit bir mimari seçmek). Ancak, bu borçları işler büyüdüğünde geri ödemeli (refactoring yapmalı) ve sistemin sürdürülebilirliğini sağlamalısınız.

“KISS” ve “YAGNI” prensipleri karmaşıklığı önlemede nasıl yardımcı olur?

KISS (Keep It Simple, Stupid – Basit Tut, Aptalca Olsun) prensibi, bir problemi çözerken her zaman en sade çözümü tercih etmeniz gerektiğini savunur. YAGNI (You Aren’t Gonna Need It – Buna İhtiyacınız Olmayacak) prensibi ise, gelecekte ihtiyaç duyabileceğinizi düşündüğünüz özellikleri şimdiden kodunuza eklememenizi söyler. Bu iki prensip, hak edilmemiş karmaşıklığı önlemedeki en büyük yardımcılarınızdır.

Kodun gereğinden fazla karmaşık olduğunu nasıl anlarız?

Eğer bir koda yeni bir özellik eklemek günler sürüyorsa, küçük bir değişiklik yaptığınızda sistemin alakasız yerlerinde hatalar (bugs) oluşuyorsa veya yeni işe başlayan bir geliştiricinin projeye adapte olması haftalar alıyorsa, o kod tabanı aşırı karmaşık demektir. Bu durum, acilen sadeleştirme (refactoring) yapılması gerektiğinin en net göstergesidir.

#Teknoloji #WebGeliştirme #YazılımMimarisi #CleanCode

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.