Yazılım Tasarım İlkeleri (Software Design Principles) Nedir?

Yazılım Tasarım İlkeleri (Software Design Principles), geliştiricilerin iyi bir sistem tasarımı yapmasına yardımcı olan bir dizi yönergedir. Bu ilkeler, yazılım sektöründeki bazı tanınmış yazılım mühendisleri ve yazarlar tarafından tanıtılan yaklaşımların, stillerin, felsefelerin ve en iyi uygulamaların bir listesidir.

Geliştirme sürecinde, kod yazacağız, kodu okuyacağız ve sistemin bakımını yapacağız. Bu yüzden iyi bir sistem tasarımı yapmak çok önemlidir. Peki iyi yazılım tasarımı nedir? Kalitesini nasıl ölçeriz? İyi bir yazılımı başarmak için hangi uygulamaları izlememiz gerekir? Mimarimizi nasıl esnek, kararlı ve anlaşılması kolay hale getirebiliriz?

Bunlar harika sorular; ancak ne yazık ki, oluşturduğunuz uygulamanın türüne bağlı olarak cevaplar farklıdır. Benim düşünceme göre, iyi bir sistem iyi bir kod tabanına sahip olmalıdır. Okunması kolay, anlaşılması kolay, bakımı kolay (özellik ekleme/değiştirme ve hataları düzeltme) ve sistemin gelecekte genişletmesi kolay olması gerekir. Bu, geliştirme süresini, kaynakları azaltacak ve bizi daha çok mutlu edecektir.

Sizin de kendi projeniz için bu soruları yanıtlamanıza yardımcı olabilecek birkaç evrensel yazılım tasarımı ilkesi vardır. Bu yazı serisinde listelenecek tasarım desenlerinin çoğu bu ilkelere dayanmaktadır.

Yazılım Tasarım İlkeleri Neden Önemlidir?

Yazılım Tasarım İlkeleri olmadan kod yazabilirsiniz. Gerçek bu. Ama iyi ve kaliteli bir yazılımcı olmak istiyorsanız Yazılım Tasarım İlkelerini anlamalı ve işinizde uygulamalısınız. Yazılım Tasarım İlkelerini projenize uygulamak için birçok çözümümüz var. Çözümünüzü düşünebilir ve uygulayabilir veya Tasarım Kalıplarını kullanabilirsiniz. Tasarım deseni veya kalıbı veya örüntüsü, yazılım geliştirmede birçok kez tekrarlanan genel sorunları çözmek için en iyi çözümdür. Bu çözümler, yani tasarım desenleri, yazılım tasarım ilkelerinin en iyi uygulandığı ve kendini kanıtladığı uygulamalardır. Tasarım desenini kullanmak riskleri azaltacak ve kodunuzun bakımını kolaylaştıracaktır.

Hiç Laravel veya Spring Framework gibi frameworklerin kodunu okudunuz mu? Frameworklerin kodunu okumak, becerilerinizi geliştirmenin bir yoludur. Fakat frameworkler birçok tasarım deseni uyguladıkları için anlamak daha zordur: Dekoratör Kalıbı, Strateji Kalıbı vb. Bu frameworkler, Yazılım Tasarımı ilkelerini akıllıca uygular. Örneğin Laravel 5 ve Spring frameworkleri Dependency Inversion Principle (IoC Container, Dependency Injection) kullanır. Özetle kendini kanıtlayan, profesyonel kodlar temelinde yazılım tasarım ilkelerini baz alarak geliştirilmiştir. Bu da ilkelerin önemine büyük bir delildir.

Yazılım Tasarım İlkeleri Nelerdir?

Temel olarak Yazılım Tasarım İlkeleri, OOP’ye dayalı Nesne Yönelimli Tasarım İlkeleridir. OOP’ye çok aşinayız. O yüzden bu konuda yazmayacağım. Ayrıca; Prosedürel, Fonksiyonel, Reaktif, En Boy Yönelimli Programlama vb. gibi birçok paradigmamız var. Lütfen aşağıdaki resme bir göz atın:

Design principles

OOP Design Principles’ın birçok ilkesi var. Fakat her yazılımcının bilmesi ve dikkat etmesi gereken ilkeleri şu şekilde sıralayabiliriz;

  • SOLID
  • SRP – Single Responsibility Principle
  • OCP – Open-Closed Principle
  • LSP – Liskov Substitution Principle
  • ISP – Interface Segregation Principle
  • DIP – Dependency Injection or Inversion principle
  • DRY – Don’t repeat yourself
  • KISS – Keep it simple, stupid!!
  • YAGNI – You ain’t gonna need it
  • Encapsulate What Changes
  • Favor Composition over Inheritance
  • Programming for Interface not implementation

Şimdi tüm bu prensipleri tek tek inceleyelim.

1. SOLID

SOLID, yazılım tasarımlarını daha anlaşılır, esnek ve sürdürülebilir hale getirmeyi amaçlayan beş tasarım ilkesi için bir kısaltmadır (acronym). Robert Martin bunları Agile Software Development, Principles, Patterns, and Practices kitabında tanıtmıştır.

Hayattaki her şeyde olduğu gibi, bu ilkeleri plansızca kullanmak yarardan çok zarar verir. Bu ilkeleri bir programın mimarisine uygulamanın maliyeti, onu olması gerekenden daha karmaşık hale getiriyor olabilir. Tüm bu ilkelerin aynı anda uygulandığı başarılı bir yazılım ürünü olduğundan şüpheliyim. Bu ilkeler için çabalamak iyidir, ancak her zaman pragmatik olmaya çalışın ve burada yazılan her şeyi dogma olarak almayın.

1.1. SRP – Single Responsibility Principle

Bir sınıfın değişmesi için tek bir nedeni olmalıdır veya bir sınıf her zaman tek bir işlevi yerine getirmelidir.SRP

Her sınıfı, yazılımın sağladığı işlevselliğin tek bir bölümünden sorumlu kılmaya çalışın ve bu sorumluluğu tamamen sınıf tarafından encapsulate edilmiş (içerisinde gizli de diyebilirsiniz) yapın.

Bu ilkenin temel amacı karmaşıklığı azaltmaktır. Yalnızca yaklaşık 200 satır kodu olan bir program için sofistike bir tasarım icat etmenize gerek yok.Bunun yerine birden fazla sadece kendi işini yapan methodlar oluşturun ve çok daha iyi olduğunu kendiniz görün.

Asıl problemler, programınız sürekli büyüyüp değiştiğinde ortaya çıkar. Bir noktada sınıflar o kadar büyür ki ayrıntılarını artık hatırlayamazsınız. Kod gezintisi yavaşlayarak gezinmeye başlar ve belirli şeyleri bulmak için tüm sınıfları veya hatta tüm programı taramanız gerekir. Programdaki varlıkların sayısının beyin yığınınızı aştığını ve kod üzerindeki kontrolünüzü kaybettiğinizi hissedersiniz.

Dahası var: Bir sınıf çok fazla şey yapıyorsa, bunlardan biri her değiştiğinde onu değiştirmeniz gerekir. Bunu yaparken, sınıfın değiştirmeyi bile düşünmediğiniz diğer kısımlarını bozma riskini alırsınız. Çünkü artık bağımlılık (coupling) artmıştır.

Programın belirli yönlerine teker teker odaklanmanın zorlaştığını düşünüyorsanız, tek sorumluluk ilkesini hatırlayın ve bazı sınıfları bölümlere ayırmanın zamanının gelip gelmediğini kontrol edin.

Örnek

Employee sınıfının değişmesi için birkaç neden vardır. İlk neden, sınıfın ana işiyle ilgili olabilir: çalışan verilerini yönetmek. Ancak başka bir neden daha var: zaman çizelgesi raporunun formatı zamanla değişebilir ve bu da sınıf içindeki kodu değiştirmenizi gerektirebilir.

ÖNCESİ: sınıf birkaç farklı davranış içerir.

Zaman çizelgesi raporlarını yazdırmayla ilgili davranışı ayrı bir sınıfa taşıyarak sorunu çözün. Bu değişiklik, raporla ilgili diğer öğeleri de yeni sınıfa taşımanıza olanak tanır.

SONRASI: ekstra davranışlar kendi sınıfına taşındı

1.2. OCP – Open-Closed Principle

Sınıflar ve methodlar genişletmek için açık olmalı, ancak düzenleme için kapalı olmalıdır.OCP

Bu ilkenin ana fikri, yeni özellikler uyguladığınızda mevcut test edilmiş ve kullanılan kodun bozulmasını önlemektir.

Bir sınıf eğer onu genişletebilir, bir alt sınıf oluşturabilir, yeni yöntemler veya alanlar ekleyebilir, temel davranışlarını geçersiz kılabilirseniz açıktır. Bazı programlama dilleri, bir sınıfın daha fazla genişletilmesini özel anahtar kelimelerle kısıtlamanıza izin verir, örneğin final. Ve artık bundan sonra sınıf artık açık değildir. Aynı zamanda, diğer sınıflar tarafından kullanılmaya %100 hazır, interface’i açıkça tanımlanmış ve gelecekte değiştirilmeyecekse sınıf kapalıdır (tamamlandı da diyebilirsiniz).

Bu ilkeyi ilk öğrendiğimde kafam karıştı çünkü açık ve kapalı sözcükleri birbirini dışlıyordu. Ancak bu ilkeye göre bir sınıf aynı anda hem açık (genişletme için) hem de kapalı (değişiklik için) olabilir.

Bir sınıf zaten geliştirilmiş, test edilmiş, gözden geçirilmiş ve bir framework’e dahil edilmişse veya bir uygulamada başka bir şekilde kullanılıyorsa, kodunu bozmaya çalışmak risklidir. Sınıfın kodunu doğrudan değiştirmek yerine, bir alt sınıf oluşturabilir ve orjinal sınıfın farklı davranmasını istediğiniz kısımlarını geçersiz kılabilirsiniz. Hedefinize ulaşacaksınız ama aynı zamanda orijinal sınıfın mevcut istemcilerini de bozmayacaksınız.

Bu ilkenin bir sınıftaki tüm değişiklikler için uygulanması amaçlanmamıştır. Sınıfta bir hata olduğunu biliyorsanız, devam edin ve düzeltin; bunun için bir alt sınıf oluşturmayın. Bir child sınıf, parentının sorunlarından sorumlu olmamalıdır.

Örnek

Gönderim maliyetlerini hesaplayan bir Order sınıfına sahip bir e-ticaret uygulamanız var ve tüm gönderim yöntemleri sınıf içinde kodlanmış durumda. Yeni bir gönderi yöntemi eklemeniz gerekiyorsa, Order sınıfının kodunu değiştirmeniz ve bunu bozma riskini almanız gerekir.

ÖNCESİ: Uygulamaya yeni bir gönderim yöntemi eklediğinizde Sipariş sınıfını değiştirmeniz gerekir.

Strateji kalıbını uygulayarak sorunu çözebilirsiniz. Ortak bir arabirimle nakliye yöntemlerini ayrı sınıflara ayırarak başlayın.

SONRASI: Yeni bir nakliye yöntemi eklemek, mevcut sınıfları değiştirmeyi gerektirmez

Artık yeni bir gönderi yöntemi uygulamanız gerektiğinde, Order sınıfının herhangi bir koduna dokunmadan Shipping arayüzünden yeni bir sınıf türetebilirsiniz. Order sınıfının müşteri kodu, kullanıcı bu gönderim yöntemlerini kullanıcı arabiriminde seçtiğinde, siparişleri yeni sınıfın bir gönderim nesnesine bağlayacaktır.

Bir bonus olarak, bu çözüm, teslimat süresi hesaplamasını tek sorumluluk ilkesine göre daha ilgili sınıflara taşımanıza olanak tanır.

1.3. LSP – Liskov Substitution Principle

Bir sınıfı genişletirken, istemci kodunu bozmadan üst sınıfın nesneleri yerine alt sınıfın nesnelerini geçirebilmeniz gerektiğini unutmayın.LSP

Bu, alt sınıfın üst sınıfın davranışıyla uyumlu kalması gerektiği anlamına gelir. Bir yöntemi geçersiz kılarken, tamamen başka bir şeyle değiştirmek yerine temel davranışı genişletin.

Yerine koyma ilkesi, bir alt sınıfın üst sınıfın nesneleriyle çalışabilen kodla uyumlu olup olmadığını tahmin etmeye yardımcı olan bir dizi denetimdir. Bu kavram, kütüphaneler ve çerçeveler geliştirirken kritik öneme sahiptir, çünkü sınıflarınız, koduna doğrudan erişemeyeceğiniz ve değiştiremeyeceğiniz diğer kişiler tarafından kullanılacaktır.

Yoruma çok açık olan diğer tasarım ilkelerinin aksine, ikame ilkesinin alt sınıflar ve özellikle onların yöntemleri için bir dizi resmi gereksinimi vardır. Bu kontrol listesini ayrıntılı olarak gözden geçirelim.

Yoruma çok açık olan diğer tasarım ilkelerinin aksine, yerine geçebilme (substitution) ilkesinin alt sınıflar ve özellikle onların yöntemleri için bir dizi resmi gereksinimi vardır. Bu kontrol listesini ayrıntılı olarak gözden geçirelim.

Liste: Sınıflar ve Methodlar
  • Bir alt sınıfın methodundaki parametre türleri, üst sınıfın methodundaki parametre türleriyle eşleşmeli veya daha soyut olmalıdır. Kafa karıştırıcı geliyor mu? Bir örnek verelim.
  • Kedileri beslemesi gereken bir methoda sahip bir sınıf olduğunu varsayalım: feed(Cat c). İstemci kodu her zaman Cat nesnelerini bu yönteme geçirir.
  • İyi: Herhangi bir hayvanı besleyebilmesi için yöntemi geçersiz kılan bir alt sınıf oluşturduğunuzu varsayalım (kedilerin bir üst sınıfı): feed(Animal c) . Şimdi, istemci koduna üst sınıfın bir nesnesi yerine bu alt sınıfın bir nesnesini iletirseniz, her şey yine de iyi çalışır. Method tüm hayvanları besleyebilir, böylece müşteri tarafından geçen herhangi bir kediyi besleyebilir.
  • Kötü: Başka bir alt sınıf oluşturdunuz ve besleme yöntemini yalnızca Bengal kedilerini (kedilerin bir alt sınıfı) kabul edecek şekilde kısıtladınız: feed(BengalCat c). İstemci koduna orijinal sınıf yerine bunun gibi bir nesne ile bağlanırsanız ne olur? Yöntem yalnızca belirli bir kedi cinsini besleyebildiğinden, müşteri tarafından geçen genel kedilere hizmet etmeyecek ve ilgili tüm işlevleri bozacaktır.
  • Bir alt sınıfın methodundaki dönüş türü, üst sınıfın methodundaki dönüş türünün alt türüyle eşleşmeli veya bu tür olmalıdır. Gördüğünüz gibi, bir dönüş türü gereksinimleri, parametre türleri gereksinimlerinin tersidir.
  • buyCat(): Cat methoduna sahip bir sınıfınız olduğunu varsayalım. İstemci kodu, bu methodun yürütülmesi sonucunda herhangi bir kedi almayı bekler.
  • İyi: Bir alt sınıf, methodu şu şekilde geçersiz kılar: buyCat(): BengalCat. Müşteri, hala bir kedi olan bir Bengal kedisi alır, yani her şey yolundadır.
  • Kötü: Bir alt sınıf, methodu şu şekilde geçersiz kılar: buyCat(): Animal. Şimdi müşteri kodu, bir kedi için tasarlanmış bir yapıya uymayan bilinmeyen bir jenerik hayvan (timsah? ayı?) aldığı için bozulur.
Liste: Hatalar ve Zayıflıklar

Dinamik yazma ile programlama dilleri dünyasından başka bir anti-örnek gelir: temel yöntem bir dize döndürür, ancak geçersiz kılınan yöntem bir sayı döndürür.

  • Bir alt sınıftaki bir method, temel methodun atması beklenmeyen istisna türlerini atmamalıdır. Başka bir deyişle, istisna türleri, temel methodun zaten atabildiği istisna türleri ile eşleşmeli veya bunların alt türleri olmalıdır. Bu kural, istemci kodundaki try-catch bloklarının, temel methodun atması muhtemel olan belirli istisna türlerini hedef alması gerçeğinden gelir. Bu nedenle, beklenmeyen bir özel durum, istemci kodunun defansif satırlarından geçerek tüm uygulamayı çökertir.
  • Çoğu modern programlama dilinde, özellikle statik olarak yazılanlarda (Java, C# ve diğerleri), bu kurallar dilde yerleşiktir. Bu kuralları ihlal eden bir program derleyemezsiniz.
  • Bir alt sınıf, ön koşulları güçlendirmemelidir. Örneğin, temel yöntemin int türünde bir parametresi vardır. Bir alt sınıf bu yöntemi geçersiz kılar ve methoda iletilen bir argümanın değerinin pozitif olmasını gerektiriyorsa (değer negatifse bir istisna atarak), bu ön koşulları güçlendirir. Negatif sayıları methoda geçirirken iyi çalışan istemci kodu, bu alt sınıfın bir nesnesiyle çalışmaya başlarsa şimdi bozulur.
  • Bir alt sınıf, son koşulları zayıflatmamalıdır. Bir veritabanıyla çalışan bir methodu olan bir sınıfınız olduğunu varsayalım. Sınıfın bir methodunun, bir değer döndürdükten sonra her zaman tüm açılan veritabanı bağlantılarını kapatması gerekir.
  • Bir alt sınıf oluşturdunuz ve onu, veritabanı bağlantılarının açık kalması ve yeniden kullanabilmeniz için değiştirdiniz. Ancak müşteri, niyetiniz hakkında hiçbir şey bilmiyor olabilir. Methodların tüm bağlantıları kapatmasını beklediğinden, yöntemi çağırdıktan hemen sonra programı sonlandırabilir ve sistemi hayalet veritabanı bağlantılarıyla kirletebilir.
Liste: Koruma
  • Bir üst sınıfın değişmezleri korunmalıdır. Bu muhtemelen en az resmi kuraldır. Değişmezler, bir nesnenin anlamlı olduğu koşullardır. Örneğin, bir kedinin değişmezleri dört bacağa, bir kuyruğa, miyavlama yeteneğine vb. sahiptir. Değişmezlerle ilgili kafa karıştıran kısım, bunlar interface sözleşmeleri veya methodlar içindeki bir dizi iddia şeklinde açıkça tanımlanabilseler de, ayrıca müşteri kodunun belirli birim testleri ve beklentileri tarafından da ima edilebilir.
  • Değişmezler kuralı ihlal edilmesi en kolay kuraldır çünkü karmaşık bir sınıfın tüm değişmezlerini yanlış anlayabilir veya anlayamayabilirsiniz. Bu nedenle, bir sınıfı genişletmenin en güvenli yolu, yeni alanlar ve yöntemler tanıtmak ve üst sınıfın mevcut üyeleriyle uğraşmamaktır. Tabii ki, bu gerçek hayatta her zaman mümkün değildir.
  • Bir alt sınıf, üst sınıfın private değerlerini değiştirmemelidir. Ne? Bu nasıl mümkün olur? Görünüşe göre bazı programlama dilleri, reflection mekanizmaları aracılığıyla bir sınıfın private üyelerine erişmenize izin veriyor. Diğer dillerin (Python, JavaScript) private üyeler için hiçbir koruması yoktur.

Örnek

Substitution ilkesini ihlal eden bir belge sınıfları hiyerarşisi örneğine bakalım.

ÖNCESİ: salt okunur bir belgede kaydetmenin bir anlamı yoktur, bu nedenle alt sınıf, geçersiz kılınan yöntemde temel davranışı sıfırlayarak sorunu çözmeye çalışır.

ReadOnlyDocuments alt sınıfındaki save yöntemi, birisi onu çağırmaya çalışırsa bir istisna atar. Temel yöntemde bu kısıtlama yoktur. Bu, kaydetmeden önce belge türünü kontrol etmezsek müşteri kodunun bozulacağı anlamına gelir.

İstemci kodu somut belge sınıflarına bağımlı hale geldiğinden, sonuçta ortaya çıkan kod açık/kapalı ilkesini de ihlal eder. Yeni bir belge alt sınıfı eklerseniz, bunu desteklemek için istemci kodunu değiştirmeniz gerekir.

SONRASI: salt okunur belge sınıfını hiyerarşinin temel sınıfı yaptıktan sonra sorun çözüldü.

Sınıf hiyerarşisini yeniden tasarlayarak sorunu çözebilirsiniz. Bir alt sınıf, bir üst sınıfın davranışını genişletmelidir, bu nedenle salt okunur belge hiyerarşinin temel sınıfı olur. Yazılabilir belge artık temel sınıfı genişleten ve kaydetme davranışını ekleyen bir alt sınıftır.

1.4. ISP – Interface Segregation Principle

İstemciler, kullanmadıkları yöntemlere bağlı kalmaya zorlanmamalıdır.ISP

Interface’lerinizi, istemci sınıflarının ihtiyaç duymadıkları davranışları uygulamak zorunda kalmayacak kadar daraltmaya çalışın

Arayüz ayrımı ilkesine göre, “şişman” arayüzlerinizi daha ayrıntılı ve spesifik olanlara ayırmalısınız. Müşteriler yalnızca gerçekten ihtiyaç duydukları interfaceleri uygulamalıdır. Aksi takdirde, “şişman” bir arayüzde yapılacak bir değişiklik, değiştirilen yöntemleri kullanmayan istemcileri bile bozabilecekti.

Sınıf mirası (inheritance), bir sınıfın yalnızca bir üst sınıfa sahip olmasına izin verir, ancak sınıfın aynı anda uygulayabileceği inteface sayısını sınırlamaz. Bu nedenle, tek bir interface’e tonlarca alakasız yöntemi sıkıştırmaya gerek yoktur. Birkaç tane daha iyileştirilmiş interface’e bölün; gerekirse hepsini tek bir sınıfta uygulayabilirsiniz. Ancak, bazı sınıflar bunlardan sadece birini uygulamakla daha iyi olabilir.

Örnek

Uygulamaları çeşitli bulut bilişim sağlayıcılarıyla entegre etmeyi kolaylaştıran bir library oluşturduğunuzu hayal edin. İlk sürümde yalnızca Amazon Cloud’u desteklerken, artık tüm bulut hizmetleri ve özelliklerini kapsıyordu.

O zamanlar tüm bulut sağlayıcılarının Amazon ile aynı geniş özellik yelpazesine sahip olduğunu varsayıyordunuz. Ancak başka bir sağlayıcı için destek uygulamaya geldiğinde, kütüphanenin arayüzlerinin çoğunun çok geniş olduğu ortaya çıktı. Bazı methodlar, diğer bulut sağlayıcılarının sahip olmadığı özellikleri ortaya çıkardı varsayalım.

ÖNCESİ: Tüm istemciler, şişkin arayüzün gereksinimlerini karşılayamaz.

Hala bu yöntemleri implement edip ve oralara bazı taslakları koyabilmek güzel bir çözüm olmazdı. Daha iyi yaklaşım, interface’i parçalara ayırmaktır. Orjinal interface’i uygulayabilen sınıflar artık sadece birkaç interface uygulayabilir. Diğer sınıflar, yalnızca bunları anlamayan methodları olan bu interface’leri uygulayabilir.

SONRASI: şişirilmiş bir interface, bir dizi daha ayrıntılı interface’e bölünür.

Diğer ilkelerde olduğu gibi, bunda da çok ileri gidebilirsiniz. Zaten oldukça spesifik olan bir interface’i daha fazla bölmeyin. Ne kadar çok arayüz oluşturursanız, kodunuz o kadar karmaşık hale gelir. Dengeyi koruyun.

1.5. DIP – Dependency Injection or Inversion principle

Yüksek seviyeli sınıflar, düşük seviyeli sınıflara bağlı olmamalıdır. Her ikisi de soyutlamalara dayanmalıdır. Soyutlamalar ayrıntılara bağlı olmamalıdır. Detaylar soyutlamalara bağlı olmalıdır.DIP

Genellikle yazılım tasarlarken, iki sınıf düzeyi arasında bir ayrım yapabilirsiniz.

  • Düşük seviyeli sınıflar, bir diskle çalışma, bir ağ üzerinden veri aktarma, bir veritabanına bağlanma vb. gibi temel işlemleri gerçekleştirir.
  • Yüksek seviyeli sınıflar, düşük seviyeli sınıfları bir şeyler yapmaya yönlendiren karmaşık business logicleri içerir.

Bazen insanlar önce düşük seviyeli sınıflar tasarlarlar ve daha sonra yüksek seviyeli sınıflar üzerinde çalışmaya başlarlar. Yeni bir sistemde bir prototip geliştirmeye başladığınızda bu çok yaygındır. Düşük seviyeli şeyler henüz uygulanmadığı veya net olmadığı için daha yüksek seviyede nelerin mümkün olduğundan bile emin olmazsanız. Böyle bir yaklaşımla business logic sınıfları, ilkel düşük seviyeli sınıflara bağımlı hale gelme eğilimindedir.

Bağımlılığın tersine çevrilmesi ilkesi, bu bağımlılığın yönünün değiştirilmesini önerir.

  1. Yeni başlayanlar için, yüksek seviyeli sınıfların dayandığı düşük seviyeli işlemler için interfaceleri tercihen business terimleriyle tanımlamanız gerekir. Örneğin, business logic bir dizi openFile(x) , readBytes(n) , closeFile(x) yöntemi yerine openReport(file) yöntemini çağırmalıdır. Bu interfaceler üst düzey olarak sayılır.
  2. Artık somut düşük seviyeli sınıflar yerine yüksek seviyeli sınıfları bu arayüzlere bağımlı hale getirebilirsiniz. Bu bağımlılık, orijinalinden çok daha yumuşak olacaktır.
  3. Düşük seviyeli sınıflar bu arayüzleri uyguladığında, orijinal bağımlılığın yönünü tersine çevirerek business logic seviyesine bağımlı hale gelirler.

Bağımlılığı tersine çevirme ilkesi genellikle açık/kapalı ilkesiyle birlikte gider. Düşük düzeyli sınıfları, mevcut sınıfları bozmadan farklı business logic sınıflarıyla kullanmak üzere genişletebilirsiniz.

Örnek

Bu örnekte, yüksek seviyeli bütçe raporlama sınıfı, verilerini okumak ve kalıcı hale getirmek için düşük seviyeli bir veritabanı sınıfı kullanır. Bu, veritabanı sunucusunun yeni bir sürümünün piyasaya sürülmesi gibi düşük seviyeli sınıftaki herhangi bir değişikliğin, veri depolama ayrıntılarını umursamaması gereken yüksek seviyeli sınıfı etkileyebileceği anlamına gelir.

ÖNCESİ: yüksek seviyeli bir sınıf, düşük seviyeli bir sınıfa bağlıdır.

Bu sorunu, okuma/yazma işlemlerini açıklayan bir üst düzey interface oluşturarak ve raporlama sınıfının düşük düzeyli sınıf yerine bu interface’i kullanmasını sağlayarak çözebilirsiniz. Ardından, business logic tarafından bildirilen yeni okuma/yazma interface’ini uygulamak için orjinal düşük seviyeli sınıfı değiştirebilir veya genişletebilirsiniz.

SONRASI: düşük seviyeli sınıflar, yüksek seviyeli bir soyutlamaya bağlıdır.

Sonuç olarak, orijinal bağımlılığın yönü tersine çevrilmiştir: düşük seviyeli sınıflar artık yüksek seviyeli soyutlamalara bağımlıdır.

2. DRY (Don’t Repeat Yourself)

Her küçük bilgi parçası (kod) tüm sistemde yalnızca bir kez oluşturulabilmelidir.DRY

DRY ilkesi, yazılım sistemlerinde kod tekrarını ve çabayı azaltmayı amaçlar. Temel olarak, aynı kodu/yapılandırmayı birden çok yere yazmamanız gerektiği anlamına gelir. Bunu yapmazsanız, onları senkronize tutmanız gerekir; ve bir yerde kodda yapılacak herhangi bir değişiklik, başka yerlerde de değişiklik yapılmasını gerektirecektir.

DRY ilkesi yeniden kullanılabilirliği destekler. Kodu daha sürdürülebilir, daha genişletilebilir ve daha az sorunlu hale getirir.

Aşağıda, yazılım mühendisliğinde DRY ilkesine dayanan bazı kavramlar verilmiştir

  • Inheritance ve Composition, bir yerde kod yazmanıza ve ardından onu başka yerlerde yeniden kullanmanıza izin verir.
  • Veritabanı Normalization, veri fazlalığını (tekrarını) ortadan kaldırmak için veritabanlarında kullanılan bir tasarım tekniğidir.

Örnek

Servise gelen birçok taşıt türü olduğunu hayal edin. Bakımdan sonra araçlar yıkama işleminden geçiyor ve bu yıkama kodu sürekli tekrarlanıyor.

public class Mechanic { public void serviceBus() { System.out.println("Servicing bus now"); //Process washing } public void serviceCar() { System.out.println("Servicing car now"); //Process washing } }

Aynı kod blokunu sürekli kullanmak yerine bunu ayrı bir methoda taşıyın. Böylece yıkama işleminde yapacağınız herhangi bir değişiklik için her yere müdahale etmek zorunda kalmazsınız. Üstelik kodlarınız daha düzenli olacaktır.

public class Mechanic { public void serviceBus() { System.out.println("Servicing bus now"); washVehicle(); } public void serviceCar() { System.out.println("Servicing car now"); washVehicle(); } public void washVehicle() { //Process washing } }

3. KISS (Keep It Simple, Stupid)

Her küçük yazılım parçasını basit tutmaya çalışın ve gereksiz karmaşıklıktan kaçınınKISS

Yazılım sistemleri, basit tutulduklarında en iyi şekilde çalışır. Gereksiz karmaşıklıktan kaçınmak, sisteminizi daha sağlam, anlaşılması daha kolay, akıl yürütmeyi ve genişletmeyi daha kolay hale getirecektir.

Çok açık. Ancak biz mühendisler, genellikle işleri karmaşıklaştırma eğilimindeyiz. Kimsenin bilmediği ve gurur duyduğu o süslü dil özelliklerini kullanıyoruz. Her basit şey için projemize sayısız bağımlılık getiriyoruz ve sonunda bir bağımlılık cehennemi yaşıyoruz. Her basit şey için sonsuz mikro hizmetler yaratıyoruz.

Projenize yeni bir bağımlılık eklediğinizde veya süslü yeni bir frameworkü kullanmaya başladığınızda veya yeni bir mikro hizmet oluşturduğunuzda, sisteminize ek karmaşıklık getirdiğinizi unutmayın. Bu karmaşıklığın buna değip değmeyeceğini düşünmeniz gerekir.

Basitçe anlatamıyorsan, yeterince anlamamışsındır.Albert EINSTEIN

Örnek

Bir sayının faktoriyelini hesaplamak için bir çok karmaşık ve uzun kod yazmaya gerek yoktur. Bu kodlar sizin iyi bir yazılımcı olduğunuzu kanıtlamaz.

public static int factorial(int n){ int fact = 1; for(i=1; i<=n; i++){ fact = fact * i; } return fact; }

Bunun yerine en basit yöntemi kullanmayı deneyin.

BigInteger factorial = BigIntegerMath.factorial(n);

4. YAGNI (You Ain’t Gonna Need It)

Bir şeyleri her zaman gerçekten ihtiyacınız olduğunda uygulayın, hiçbir şeyi ihtiyaç duymadan uygulayın.YAGNI

KISS ilkesi gibi, YAGNI de karmaşıklıktan, özellikle gelecekte ihtiyaç duyabileceğinizi düşündüğünüz işlevsellik eklemekten kaynaklanan karmaşıklıktan kaçınmayı amaçlar.

Şu anda sahip olmadığınız gelecekteki bir sorunu çözmek için bir şeyler tanıtmamanız gerektiğini belirtir. Bir şeyleri her zaman gerçekten ihtiyacınız olduğunda uygulayın. Yazılımınızı yalın ve basit tutmanıza yardımcı olacaktır. Ayrıca size ekstra para ve çaba tasarrufu sağlayacaktır.

Ayrıca, gelecekte bu işlevselliğe ihtiyacınız olduğunu düşünebilirsiniz. Ancak çoğu zaman, yazılım dünyamızın sürekli değişen gereksinimleri nedeniyle buna ihtiyacınız bile olmayacaktır.

Erken optimizasyon tüm kötülüklerin köküdürDonald KNUTH

5. Encapsulate What Changes

Uygulamanızın değişen yönlerini tanımlayın ve bunları aynı kalanlardan ayırın. Bu ilkenin temel amacı, değişikliklerin neden olduğu etkiyi en aza indirmektir.

Programınızın bir gemi olduğunu ve değişikliklerin su altında kalan korkunç madenler olduğunu hayal edin. Mayına çarpan gemi batar. Bunu bilerek, geminin gövdesini, hasarı tek bir bölmeyle sınırlamak için güvenli bir şekilde kapatılabilen bağımsız bölmelere ayırabilirsiniz. Şimdi, gemi bir mayına çarparsa, gemi bir bütün olarak yüzer durumda kalır.

Aynı şekilde, programın bağımsız modüllerde değişen kısımlarını izole ederek kodun geri kalanını olumsuz etkilerden koruyabilirsiniz. Sonuç olarak, programı tekrar çalışır duruma getirmek, değişiklikleri uygulamak ve test etmek için daha az zaman harcarsınız. Değişiklik yapmak için ne kadar az zaman harcarsanız, özellikleri uygulamak için o kadar fazla zamanınız olur.

5.1. Yöntem düzeyinde kapsülleme

Diyelim ki bir e-ticaret sitesi yapıyorsunuz. Kodunuzun bir yerinde, vergiler dahil sipariş için genel bir toplamı hesaplayan bir getOrderTotal yöntemi vardır.

Vergiyle ilgili kodun gelecekte değişmesi gerekebileceğini tahmin edebiliriz. Vergi oranı, müşterinin ikamet ettiği ülkeye, eyalete ve hatta şehre bağlıdır ve gerçek formül, yeni yasalar veya düzenlemeler nedeniyle zaman içinde değişecektir. Sonuç olarak, getOrderTotal yöntemini oldukça sık değiştirmeniz gerekecek. Ancak yöntemin adı bile verginin nasıl hesaplandığıyla ilgilenmediğini gösteriyor.

method getOrderTotal(order) is total = 0 foreach item in order.lineItems total += item.price * item.quantity if (order.country == "US") total += total * 0.07 // US sales tax else if (order.country == "EU"): total += total * 0.20 // European VAT return total

Vergi hesaplama mantığını orijinal yöntemden gizleyerek ayrı bir yönteme çıkarabilirsiniz.

method getOrderTotal(order) is total = 0 foreach item in order.lineItems total += item.price * item.quantity total += total * getTaxRate(order.country) return total method getTaxRate(country) is if (country == "US") return 0.07 // US sales tax else if (country == "EU") return 0.20 // European VAT else return 0

Vergiyle ilgili değişiklikler, tek bir yöntem içinde yalıtıldı. Üstelik hesaplama mantığı çok karmaşık hale gelirse ayrı bir sınıfa taşımak artık daha kolay oldu.

5.2. Sınıf düzeyinde kapsülleme

Zamanla, basit bir şey yapmak için kullanılan bir yönteme giderek daha fazla sorumluluk ekleyebilirsiniz. Bu eklenen davranışlar genellikle kendi yardımcı alanları ve yöntemleriyle birlikte gelir. Bu da sonunda kapsayıcı sınıfın birincil sorumluluğunu bulanıklaştırır. Her şeyi yeni bir sınıfa çıkarmak, işleri çok daha açık ve basit hale getirecektir.

Order sınıfının nesneleri, vergiyle ilgili tüm işleri tam da bunu yapan özel bir nesneye devreder.

6. Programming for Interface not Implementation

Bir Implementation’a değil, bir Interface’e programlayın. Somut sınıflara değil, soyutlamalara bağlı kalın.

Mevcut herhangi bir kodu bozmadan kolayca genişletebilirseniz, tasarımın yeterince esnek olduğunu söyleyebilirsiniz. Başka bir kedi örneğine bakarak bu ifadenin doğru olduğundan emin olalım. Herhangi bir yiyeceği yiyebilen bir Kedi, sadece sosis yiyebilen bir kediden daha esnektir. Yine de ilk kediyi sosislerle besleyebilirsiniz çünkü onlar “herhangi bir yiyeceğin” bir alt kümesidir; ancak, o kedinin menüsünü başka herhangi bir yiyecekle genişletebilirsiniz.

İki sınıfı birbiriyle işbirliği yaptırmak istediğinizde, birini diğerine bağımlı hale getirerek başlayabilirsiniz. Bunun için nesneler arasında işbirliği kurmanın esnek bir yolu vardır:

  1. Bir nesnenin diğerinden tam olarak neye ihtiyacı olduğunu belirleyin: Hangi yöntemleri uygular?
  2. Bu yöntemleri yeni bir interface veya abstract sınıfta açıklayın.
  3. Bağımlılık olan sınıfın bu interface’i uygulamasını sağlayın.
  4. Şimdi ikinci sınıfı somut sınıftan ziyade bu interface’e bağımlı hale getirin. Yine de orijinal sınıfın nesneleri ile çalışmasını sağlayabilirsiniz, ancak bağlantı artık çok daha esnektir.
Arayüzü çıkarmadan önce ve sonra. Sağdaki kod soldaki koddan daha esnektir, ancak aynı zamanda daha karmaşıktır.

Bu değişikliği yaptıktan sonra, muhtemelen hemen bir fayda hissetmeyeceksiniz. Aksine, kod eskisinden daha karmaşık hale geldi. Bununla birlikte, bunun bazı ekstra işlevler için iyi bir uzantı noktası olabileceğini veya kodunuzu kullanan diğer bazı kişilerin burada genişletmek isteyebileceğini düşünüyorsanız, bunun için gidin.

Örnek

Interface’ler aracılığıyla nesnelerle çalışmanın somut sınıflarına bağlı olmaktan daha faydalı olabileceğini gösteren başka bir örneğe bakalım. Bir yazılım geliştirme şirketi simülatörü oluşturduğunuzu hayal edin. Çeşitli çalışan türlerini temsil eden farklı sınıflarınız var.

ÖNCESİ: tüm sınıflar sıkı bir şekilde bağlanmıştır.

Başlangıçta, Company sınıfı, somut çalışan sınıflarına sıkı sıkıya bağlıdır. Ancak, uygulamalarındaki farklılıklara rağmen, işle ilgili çeşitli yöntemleri genelleştirebilir ve ardından tüm çalışan sınıfları için ortak bir interface elde edebiliriz.

Bunu yaptıktan sonra, Employee interface’i aracılığıyla çeşitli çalışan nesnelerini işleyerek Company sınıfı içinde polimorfizm uygulayabiliriz.

DAHA İYİ: polimorfizm kodu basitleştirmemize yardımcı oldu, ancak Company sınıfının geri kalanı hala somut çalışan sınıflarına bağlı

Company sınıfı, Employee sınıflarına bağlı kalır. Bu kötü bir şey çünkü başka türde çalışanlarla çalışan yeni şirket türlerini tanıtırsak, bu kodu yeniden kullanmak yerine Company sınıfının çoğunu geçersiz kılmamız gerekecek.

Bu sorunu çözmek için çalışanları alma yöntemini soyut olarak ilan edebiliriz. Her somut şirket, yalnızca ihtiyaç duyduğu çalışanları yaratarak bu yöntemi farklı şekilde uygulayacaktır.

SONRA: Company sınıfının birincil yöntemi, somut çalışan sınıflarından bağımsızdır. Çalışan nesneleri, somut şirket alt sınıflarında oluşturulur

Bu değişiklikten sonra Company sınıfı, çeşitli çalışan sınıflarından bağımsız hale gelmiştir. Artık bu sınıfı genişletebilir ve temel şirket sınıfının bir kısmını yeniden kullanırken yeni şirket ve çalışan türlerini tanıtabilirsiniz. Temel şirket sınıfını genişletmek, zaten ona dayanan mevcut herhangi bir kodu bozmaz.

Bu arada, az önce bir tasarım desenini çalışırken gördünüz! Bu, Factory Method kalıbının bir örneğiydi. Endişelenmeyin: Bunu daha sonra ayrıntılı olarak tartışacağız.

7. Favor Composition over Inheritance

Kalıtım, kodu sınıflar arasında yeniden kullanmanın muhtemelen en açık ve kolay yoludur. Aynı koda sahip iki sınıfınız var. Bu ikisi için ortak bir temel sınıf oluşturun ve benzer kodu buna taşıyın.

Ne yazık ki, kalıtım, yalnızca programınızın zaten tonlarca sınıfı olduğunda ve herhangi bir şeyi değiştirmek oldukça zor olduğunda ortaya çıkan uyarılarla birlikte gelir. İşte bu sorunların bir listesi.

  • Bir alt sınıf, üst sınıfın interface’ini azaltamaz. Ana sınıfın tüm soyut yöntemlerini kullanmasanız bile uygulamanız gerekir.
  • Yöntemleri geçersiz kılarken (override), yeni davranışın temel davranışla uyumlu olduğundan emin olmanız gerekir. Bu önemlidir, çünkü alt sınıfın nesneleri, üst sınıfın nesnelerini bekleyen herhangi bir koda geçirilebilir ve bu kodun bozulmasını istemezsiniz.
  • Kalıtım, üst sınıfın kapsüllenmesini bozar çünkü üst sınıfın ayrıntıları alt sınıf için kullanılabilir hale gelir. Bir programcının daha fazla genişletmeyi kolaylaştırmak adına bir üst sınıfı alt sınıfların bazı ayrıntılarından haberdar ettiği ters bir durum olabilir.
  • Alt sınıflar, üst sınıflara sıkı sıkıya bağlıdır (tightly coupled). Bir üst sınıftaki herhangi bir değişiklik, alt sınıfların işlevselliğini bozabilir.
  • Kodu kalıtım yoluyla yeniden kullanmaya çalışmak, paralel kalıtım hiyerarşileri oluşturmaya yol açabilir. Kalıtım genellikle tek bir boyutta gerçekleşir. Ancak iki veya daha fazla boyut olduğunda, sınıf hiyerarşisini saçma bir boyuta şişirerek çok sayıda sınıf kombinasyonu oluşturmanız gerekir.

Kompozisyon (composition) adı verilen kalıtımın bir alternatifi var. Kalıtım (inheritance), sınıflar arasındaki “is a” ilişkiyi (araba bir ulaştırma aracıdır) temsil ederken, kompozisyon “has a” ilişkisini (arabanın motoru vardır) temsil eder.

Bu ilkenin aynı zamanda toplama (aggregation) için de geçerli olduğunu belirtmeliyim – bir nesnenin diğerine atıfta bulunabileceği ancak yaşam döngüsünü yönetmediği daha rahat bir kompozisyon çeşidi. İşte bir örnek: Bir arabanın sürücüsü var, ancak başka bir araba kullanabiliyor veya arabasız yürüyebiliyor.

Örnek

Bir araba üreticisi için bir katalog uygulaması oluşturmanız gerektiğini düşünün. Şirket hem araba hem de kamyon üretiyor; elektrik veya gaz olabilirler; tüm modellerde manuel kontrol veya otomatik pilot bulunuyor.

INHERITANCE: bir sınıfı çeşitli boyutlarda genişletmek (kargo tipi × motor tipi × sefer tipi) alt sınıfların birleşimsel patlamasına yol açabilir.

Gördüğünüz gibi, her ek parametre, alt sınıfların sayısının çarpılmasıyla sonuçlanır. Bir alt sınıf aynı anda iki sınıfı genişletemediğinden, alt sınıflar arasında çok sayıda yinelenen kod vardır.

Bu sorunu kompozisyon ile çözebilirsiniz. Kendi başlarına bir davranış uygulayan araba nesneleri yerine, onu diğer nesnelere devredebilirler.

Ek avantaj, çalışma zamanında bir davranışı değiştirebilmenizdir. Örneğin, sadece araca farklı bir motor nesnesi atayarak bir araba nesnesine bağlı bir motor nesnesini değiştirebilirsiniz.

COMPOSITION: Kendi sınıf hiyerarşilerine çıkarılan farklı işlevsellik “boyutları”.