sazak@labs:~
oturum

etiketler

Clean Codemaintainabilitycode smellrefactoringsoftware designreadability

Software · Jan 9, 2025 · 10 dk okuma

Clean Code: Okunabilir Kod Gerçekten Neden Önemlidir?

Clean Code, kodu güzel göstermekten çok değişiklikleri güvenli ve anlaşılır hale getirmekle ilgilidir. İyi isimlendirme, küçük fonksiyonlar, side effect kontrolü ve refactoring kararlarının gerçek değerini inceliyoruz.

Clean Code: Okunabilir Kod Gerçekten Neden Önemlidir?

Kod bir kez yazılır, defalarca okunur ve değiştirilir. Clean Code yaklaşımının temelindeki ekonomik gerçek budur. Okunabilir kod, yalnızca geliştiricinin ne yaptığını anlatan kod değildir; başka bir geliştiricinin mevcut davranışı anlayıp yeni bir gereksinimi güvenle uygulayabildiği koddur. Değeri, ilk yazıldığı anda değil, sonraki değişikliklerde ortaya çıkar.

Bir fonksiyonun bugün doğru sonuç üretmesi, yarın güvenle değiştirilebileceği anlamına gelmez. Gereksiz bağımlılıklar, gizli side effect'ler ve belirsiz isimler küçük bir değişikliğin etki alanını büyütebilir. Bu durum geliştirme süresini uzatır, review maliyetini artırır ve hata riskini yükseltir. Clean Code'un amacı her satırı kusursuz hale getirmek değil, yazılımın değişiklik karşısındaki davranışını daha öngörülebilir kılmaktır.

Okunabilirlik, değişikliğin maliyetini belirler

Bir geliştirici mevcut bir fonksiyonu değiştirmeden önce birkaç soruya cevap arar: Bu fonksiyon hangi veriyi kullanıyor? Hangi koşullarda çalışıyor? Neyi değiştiriyor? Hangi hataları üretebilir? Başka hangi parçalar ona bağımlı?

Kod bu soruların cevaplarını açıkça gösteriyorsa değişiklik daha kolay planlanır. Cevaplar farklı dosyalara dağılmışsa veya isimler davranışı gizliyorsa geliştirici önce sistemi araştırmak zorunda kalır.

Bu araştırma süresi bakım maliyetinin önemli bir parçasıdır. Özellikle uzun ömürlü projelerde yeni özellik geliştirmek çoğu zaman mevcut kodun nasıl çalıştığını anlamayı gerektirir. Aynı kod parçası yıllar boyunca farklı kişiler tarafından okunabilir.

Okunabilirliği yalnızca kısa ve düzgün görünen kodla karıştırmamak gerekir. Tek satırlık karmaşık bir ifade, beş satırlık açık bir koşuldan daha zor anlaşılabilir. Benzer şekilde yüzlerce küçük fonksiyona bölünmüş bir işlem, tek bir tutarlı fonksiyondan daha zor takip edilebilir.

Asıl ölçüt, geliştiricinin doğru davranışı anlamak için ne kadar zihinsel bağlam taşıması gerektiğidir.

Örneğin calculateTotal() isimli bir fonksiyonun yalnızca toplam hesapladığını düşünmek doğaldır. Ancak fonksiyon aynı zamanda veritabanına kayıt yazıyor ve e-posta gönderiyorsa isim yanıltıcıdır. Sorun yalnızca isimlendirme değildir. Fonksiyonun dış dünyayla ilişkisi, çağıran kod açısından görünmez hale gelmiştir.

Böyle bir fonksiyonu değiştiren geliştirici, toplam hesaplamasının arkasında hangi işlemlerin tetiklendiğini ayrıca araştırmak zorundadır.

Okunabilirliğin mühendislik değeri burada belirginleşir: Kodun davranışı hakkında doğru tahminler yapılmasını sağlar.

Temiz kod, kısa kod demek değildir

Clean Code hakkında yaygın açıklamalardan biri, iyi kodun kısa fonksiyonlardan ve az satırdan oluştuğudur. Bu yaklaşımın kaçırdığı temel mekanizma, bağımlılıkların ve side effect'lerin nasıl yönetildiğidir. Bir fonksiyonu küçültmek, sorumlulukları doğru ayırmadığınız sürece karmaşıklığı ortadan kaldırmaz. Yalnızca farklı yerlere taşır.

Küçük fonksiyonlar yararlıdır çünkü geliştiricinin aynı anda incelemesi gereken davranış miktarını azaltabilir. Fakat her fonksiyon çağrısı yeni bir bağlam oluşturur. İşlemin ne yaptığını anlamak için sürekli dosyalar arasında gezinmek gerekiyorsa parçalama ters etki yaratır.

Bu nedenle fonksiyon uzunluğu için evrensel bir satır sınırı belirlemek doğru değildir. On satırlık bir fonksiyon karmaşık olabilirken kırk satırlık bir fonksiyon tek ve açık bir algoritmayı anlatabilir.

Single Responsibility Principle da sıklıkla yanlış yorumlanır. Bu ilke, her fonksiyonun yalnızca tek satırlık bir iş yapması gerektiğini söylemez. Sorumluluğun değişme nedenine odaklanır.

Örneğin bir siparişin indirim kurallarını hesaplayan kod ile müşteriye e-posta gönderen kod farklı nedenlerle değişir. İndirim politikası değiştiğinde e-posta altyapısına dokunmak gerekmemelidir.

Bu ayrım, özellikle side effect yönetiminde değer kazanır. Side effect, bir işlemin döndürdüğü sonuç dışında gözlemlenebilir bir değişiklik üretmesidir. Veritabanına yazmak, dosya oluşturmak, mesaj göndermek veya paylaşılan bir nesnenin durumunu değiştirmek buna örnektir.

Side effect'ler başlı başına kötü değildir. Yazılımın gerçek dünyada iş yapabilmesi için gereklidir. Sorun, ne zaman gerçekleşeceklerinin belirsiz olmasıdır.

Bir fiyat hesaplama fonksiyonunun yalnızca verilen değerlere bağlı sonuç üretmesi test etmeyi kolaylaştırır. Aynı fonksiyonun veritabanını güncellemesi ise testin kurulumunu, hata senaryolarını ve çağıran kodun beklentilerini değiştirir.

Anlamlı isimlendirme burada tamamlayıcı bir araçtır. processData() veya handle() gibi isimler bağlam verilmediğinde çok az bilgi taşır. calculateDiscount() ve sendPaymentReceipt() ise farklı davranış türlerini açıkça belirtir.

İsimler mümkün olduğunca niyeti açıklamalıdır. Ancak uzun isimler tek başına iyi tasarımın kanıtı değildir. Bir fonksiyonun doğru isimlendirilmesi, içindeki karışık sorumlulukları ortadan kaldırmaz.

Bir sipariş değişikliği bütün sorunları görünür kılar

Bir e-ticaret uygulamasında ödeme tamamlandıktan sonra sipariş tutarını hesaplayan bir servis düşünelim. Servisin OrderService.ts dosyasında completeOrder() fonksiyonu bulunuyor. Bu fonksiyon sepet satırlarını okuyor, indirim uyguluyor, ödeme durumunu güncelliyor ve müşteriye bildirim gönderiyor.

Yeni gereksinim basit görünüyor: Toplam tutarı belirli bir eşiğin üzerinde olan siparişlerde ek indirim uygulanacak. Fakat dosya incelendiğinde hesaplama ile kayıt işlemlerinin aynı koşullar içinde yürütüldüğü görülüyor.

Örneğin fonksiyonda if (total > 1000) koşulu bulunabilir. Buradaki 1000, anlamı koddan anlaşılmayan bir magic number'dır. Bu değer kampanya eşiği mi, para biriminin en küçük birimi mi, yoksa geçmişten kalan geçici bir kural mı?

Önce bu sorunun cevaplanması gerekir. Değerin DISCOUNT_THRESHOLD gibi anlamlı bir isimle tanımlanması niyeti görünür kılar. Ancak bu da yeterli değildir: Eşiğin hangi para biriminde tutulduğu ve vergi öncesi mi sonrası mı uygulandığı açık olmalıdır.

Para hesaplamalarında ikinci bir risk ortaya çıkar. JavaScript Number türü bazı ondalık değerleri tam temsil edemez. Bu nedenle parasal hesaplamalarda sistemin gereksinimine uygun decimal türü veya tutarların en küçük para biriminde integer olarak saklanması tercih edilebilir.

Integer yaklaşımında 1000, örneğin 1000 kuruş anlamına gelebilir. Bu durumda isimlendirme ve veri sözleşmesi, sayının kendisinden daha önemlidir.

Fonksiyonun içinde başka bir problem daha olabilir: Üç farklı müşteri türü için aynı indirim formülü tekrar yazılmıştır. Bu duplication, yani aynı bilginin birden fazla yerde tutulması, yeni kampanya geldiğinde bütün kopyaların güncellenmesini gerektirir.

Ancak her benzer kodu birleştirmek de doğru değildir. İki müşteri türünün kuralları bugün aynı olabilir ama farklı iş kararlarıyla değişebilir. Böyle bir durumda erken abstraction, birbirinden bağımsız kuralları gereksiz biçimde birbirine bağlar.

Tekrarın zararlı olup olmadığını anlamak için satırların benzerliğine değil, temsil ettikleri bilginin aynı olup olmadığına bakmak gerekir.

Sipariş akışındaki nested conditions da dikkati dağıtabilir. Ödeme başarılıysa, müşteri aktifse, kupon geçerliyse ve teslimat bölgesi uygunsa çalışan iç içe if blokları, başarısızlık durumlarını ana davranışın içine gömer.

Guard clause kullanmak bu yapıyı sadeleştirebilir. Örneğin geçersiz kupon veya tamamlanmamış ödeme durumunda erken dönüş yapmak, başarılı sipariş akışını daha görünür hale getirir.

Fakat burada önemli bir sınır vardır. Erken dönüş yapılmadan önce veritabanı güncellenmişse sistem yarım kalmış bir işlem bırakabilir. Bu nedenle koşulların yeniden düzenlenmesi yalnızca görsel bir temizlik değildir; işlem sırasının korunması gerekir.

Error handling de aynı akışın parçasıdır. catch bloğunda bütün hataları yakalayıp false döndürmek, geçersiz kupon ile veritabanı bağlantı hatasını aynı sonuca indirger. Çağıran kod hangi durumda yeniden deneme yapabileceğini bilemez.

Beklenen iş kuralı hataları ile beklenmeyen altyapı hatalarını ayırmak daha güvenilir bir tasarımdır. Ödeme işlemi sırasında timeout oluştuğunda işlemin gerçekten başarısız olduğunu varsaymak da tehlikelidir. Ödeme gerçekleşmiş fakat cevap ulaşmamış olabilir.

Bu nedenle ödeme servislerinde idempotency önemlidir. Idempotency, aynı isteğin tekrar uygulanmasının ek bir iş etkisi oluşturmamasını sağlayan özelliktir. Uygun bir idempotency key, tekrar gönderilen isteğin ikinci kez tahsilat oluşturmasını engellemeye yardımcı olur.

Bu örnekte Clean Code kararı yalnızca fonksiyonları küçültmek değildir. Hesaplamayı dış sistemlerle iletişimden ayırmak, hata durumlarını açıkça modellemek ve işlemlerin tekrar çalıştırıldığında ne yapacağını belirlemektir.

Code smell, refactoring için kanıt değil işarettir

Bir dosyada uzun fonksiyonlar, belirsiz isimler, tekrar eden koşullar veya aşırı yorum satırları görmek code smell olarak değerlendirilir. Code smell, kodun bakımını zorlaştırabilecek tasarım sorununa işaret eden belirtidir. Tek başına hata bulunduğunu kanıtlamaz.

Örneğin uzun bir fonksiyon bazı durumlarda gerçekten bölünmelidir. Fakat karmaşık bir protokolün adımlarını sırayla gösteren tek bir fonksiyon, gereksiz parçalara ayrıldığında daha zor anlaşılabilir.

Benzer şekilde duplication her zaman doğrudan abstraction gerektirmez. Tekrarın kaldırılması iki bağımsız modülü birbirine bağımlı hale getiriyorsa ilerideki değişiklik maliyeti artabilir.

Yorum satırları da aynı dikkatle ele alınmalıdır. // increase counter gibi kodun zaten söylediğini tekrar eden yorumlar çok az değer üretir. Üstelik kod değiştiğinde yorum güncellenmezse yanlış bilgi verir.

Buna karşılık bir hesaplama yönteminin neden seçildiğini veya alışılmadık bir sınır durumunun neden korunduğunu açıklayan yorumlar değerlidir. Kod genellikle ne yapıldığını gösterebilir; geçmişte alınmış bir kararın gerekçesini her zaman gösteremez.

Örneğin // Retry disabled because the external operation is not idempotent açıklaması önemli bir mühendislik kısıtını belgeler. Bu yorumun kaldırılması, ileride aynı hatanın yeniden yapılmasına yol açabilir.

Refactoring ise mevcut kodun dışarıdan gözlemlenebilir davranışını koruyarak iç yapısını değiştirmektir. Yeni özellik eklemek veya iş kuralını değiştirmek refactoring değildir. Bu ayrım küçük görünse de güvenilir değişiklik yönetiminin temelidir.

Bir sipariş servisinde indirim hesaplamasını ayrı fonksiyona taşımadan önce mevcut davranış testlerle korunabilir. Özellikle indirim sınırındaki değerler, boş sepet, geçersiz kupon ve ödeme hatası gibi durumlar test edilmelidir.

Sınır testlerinde eşitlik önemlidir. total > threshold ile total >= threshold farklı iş kurallarıdır. Eşik tam 1000 ise toplamı 1000 olan siparişin hangi davranışı göstermesi gerektiği açıkça belirlenmelidir.

Testler yalnızca beklenen sonucu değil, kritik side effect'leri de doğrulamalıdır. Başarısız ödeme sonrasında sipariş tamamlandı durumuna geçmemeli ve aynı ödeme isteği tekrarlandığında ikinci tahsilat oluşmamalıdır.

Refactoring için güvenilir yöntem, küçük değişiklikler yapıp her adımda davranışı doğrulamaktır. Önce isimlendirme düzeltilebilir, sonra saf hesaplama kodu ayrılabilir, ardından dış sistem çağrıları daha açık sınırlara taşınabilir.

Bu süreçte her değişikliği tek bir büyük commit içinde toplamak review sürecini zorlaştırır. Küçük ve anlamlı commit'ler, davranış değişiklikleri ile yapısal düzenlemelerin ayrı değerlendirilmesini sağlar.

Ancak testlerin varlığı tek başına güvence değildir. Eksik test kapsamı, yanlış beklentileri doğrulayan testler veya gerçek veritabanı davranışından farklı çalışan test ortamları hataları saklayabilir.

Bu nedenle kritik işlemlerde unit test yanında integration test de gerekir. Özellikle transaction sınırları, benzersizlik kısıtları ve eşzamanlı istekler yalnızca fonksiyon seviyesinde doğrulanamaz.

Temizliği değil, değişiklik güvenliğini optimize edin

Clean Code konusunda en doğru başlangıç noktası, bütün projeyi yeniden düzenlemek değildir. Yakında değiştirilecek, sık hata üreten veya anlaşılması sürekli zaman alan kodu belirlemek daha faydalıdır. Refactoring maliyeti ile gelecekte azaltılması beklenen bakım maliyeti birlikte değerlendirilmelidir.

Bir modüle nadiren dokunuluyorsa, davranışı iyi anlaşılıyorsa ve hata riski düşükse yalnızca estetik nedenlerle kapsamlı refactoring yapmak anlamlı olmayabilir. Buna karşılık ödeme, yetkilendirme veya veri bütünlüğü gibi alanlarda belirsiz side effect'ler ciddi sonuçlar doğurabilir.

Benim tercih ettiğim yaklaşım, refactoring kararını değişiklik sınırları üzerinden vermektir. Yeni bir gereksinim geldiğinde geliştirici kaç farklı sorumluluğa dokunmak zorunda kalıyor? Bir iş kuralı değişirken ilgisiz altyapı kodunun da değişmesi gerekiyorsa tasarımın sınırları yeniden incelenmelidir.

Bu değerlendirme dosya veya fonksiyon sayısından daha güvenilir bir göstergedir. İyi ayrılmış iki modül, tek bir büyük modülden daha kolay değiştirilebilir. Fakat gereksiz abstraction nedeniyle birbirine bağlanmış on modül, aynı işi zorlaştırabilir.

Code review sırasında da yalnızca stil kurallarına bakmak yeterli değildir. Review yapan kişi yeni davranışın hangi varsayımlara dayandığını, hata durumlarında ne olacağını ve değişikliğin hangi dış sistemleri etkilediğini anlayabilmelidir.

Formatter ve linter araçları biçim tutarlılığını sağlayabilir. Static analysis bazı hata sınıflarını yakalayabilir. Type system yanlış veri kullanımlarını azaltabilir. Ancak bu araçların hiçbiri yanlış ayrılmış sorumlulukları veya belirsiz iş kurallarını tek başına düzeltemez.

Clean Code'un sınırı da burada başlar. Performans açısından kritik bir algoritma, bazen daha az sezgisel veri yapıları veya düşük seviyeli optimizasyonlar gerektirebilir. Böyle durumlarda okunabilirlik tek karar ölçütü değildir.

Önce gerçek darboğaz ölçülmeli, optimizasyonun faydası doğrulanmalı ve karmaşık kodun çevresine açık bir arayüz ile davranış testleri yerleştirilmelidir. Gerekiyorsa optimizasyon gerekçesi yorumla belgelenmelidir.

Benzer biçimde kısa ömürlü bir migration script ile yıllarca geliştirilecek bir ödeme servisine aynı tasarım yatırımı yapılması gerekmez. Kodun beklenen ömrü, hata maliyeti ve değişim sıklığı farklıdır.

Uygulanabilir karar şudur: Yeni kodda anlamlı isimler kullanın, sorumlulukları değişme nedenlerine göre ayırın, side effect'leri görünür tutun ve hata davranışını açıkça tanımlayın. Mevcut kodda ise refactoring'i gerçek bir değişiklik ihtiyacıyla ilişkilendirin ve davranışı testlerle koruyun.

Bu öneri, yapısal düzenlemenin maliyeti beklenen bakım kazancını aştığında geçerliliğini kaybeder. O noktada daha fazla parçalamak yerine sınırları belgelemek, kritik davranışı test etmek veya kodu olduğu gibi bırakmak daha doğru olabilir.

Temiz kodun başarılı olup olmadığını satır sayısından anlayamazsınız. Bir sonraki değişiklik geldiğinde doğru yere müdahale edilebiliyor, beklenmeyen etkiler sınırlanabiliyor ve sistemin davranışı güvenle doğrulanabiliyorsa tasarım görevini yerine getiriyordur.

yorumlar

0

Henüz yorum yok. İlk notu sen bırak.

Yorum yazmak için giriş yap.

Gmail ile saniyeler içinde, ya da e-posta ile kaydol.

bağlı 26109:32:58