JavaScript ile SOLID Prensipleri
SOLID yazılım prensipleri; OOP (nesne tabanlı) yazılım geliştirirken kullanılan 5 önemli tasarım ilkesidir. Bu tasarım ilkesi Robert C.Martin tarafından, 1994'te “Design Principles and Design Patterns kitabında tanıtılmıştır.
Peki amacı ne bu SOLID prensiplerinin ?
SOLID ilkesi her yazılımcının uyması gereken, yazılım dünyasında standartlaşmış prensiplerdir. Peki neden bu prensiplere uyulması gerekiyor? Bunu SOLID prensiplerinin birkaç avantajından bahsettikten sonra anlatacağım:
SOLID prensiplerinin avantajları şunlardır:
- Yazılım tasarımlarının daha anlaşılır olur.
- Daha hızlı kod geliştirilir.
- Temiz ve standart yazılır.
- Fazla kodu önler.
- Kolayca test edilebilir.
- Bağımlıkları azdır.
- Yazılan parça yeniden kullanılabilir.
Avantajları saymakla bitmeyen bir prensiptir. Yukarıda sorduğum soruyu tekrar edeyim. Neden her yazılımcı uyması gerekiyor? Bir örnek ile anlatayım bunu:
Örneğin bir şirkette çalışıyorsunuz ve binlerce satır koddan oluşan 3 farklı dosyada değişiklik yapmanız gerekli. Fakat ortada bir sorun var. 3 farklı dosyada da çok fazla aynı işlevi gören metotlar var. Bir dosyada değişiklik yaptığınız zaman, bu değişikiği geriye kalan 2 dosyada da yapmanız gerekiyor. Aynı zamanda bir metot çok fazla iş yapıyor. Bu sebeple bir metodun uzunluğu neredeyse 400–500 satır kodu bulabiliyor. İşte burada 2 SOLID ilkesi çiğnenmiştir:
- Dependecy Inversion Principle
- Single Responsibility Principle
Yukarıdaki en basitinden bir örnek. Birde bunu, yüzlerce geliştiricinin çalışıtığı bir şirkette yaşandığını düşünün. Ortada ciddi anlamda bir kriz oluşurdu.
İşte tüm bu nedenlerden dolayı SOLID, bir yazılımcının uyması gereken prensiplerdir.
SOLID Prensipleri
- Single Responsibility Principle (SRP)
- Open Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
1. Single Responsibility Principle (SRP)
Bir sınıfın sadece tek bir sorumluğu olmalıdır. Yani sınıf; matruşka gibi olmamalıdır. Kod içinde kod, yazılım dünyasında caiz değildir. Bu sebeple yazacağınız her bir sınıfın tek bir görevi olmalıdır.
Çoğu zaman işlerin yetişmesi için yazılımcılar kodları tek bir sınıfın altına tıkıştırabiliyor. Bunu bende yapıyordum eskiden. Fakat bunu yaptıktan sonra dürüst olalım kimse geri dönüp o kodları ayırmıyor. Sorunda burada başlıyor. Bu sebeple; sınıfı yazmaya başlarken bu kurala dikkat edersek uygulanabilirliği çok daha yüksek olacaktır.
“Sınıf sınıf diyorsun ama ben JavaScript yazarken OOP kullanmıyorum”, diyenler olacaktır. O zaman şu eşitliği vermenin zamanı geldi:
- Sınıf = Fonksiyon (metot)
İster metot olsun ister sınıf, her şekilde SOLID ilkelerini uygulayabilirsiniz.
function student (firstname, _class) { this.firstname = firstname;
this.class = _class; let lastname = function() { ... }; // ogrencinin soyadı
let studentCount = function() { ... } // sinifin ogrenci sayisi}
Yukarıdaki örnekte SRP ilkesi çiğnenmiştir. lastname
ve studentCount
metotları, ikisi birlikte, Student
metotunun içerisinde yer almamalıdır. Bu kullanım, metotun içerisinde birden fazla işlem yapılmasına neden olur. SRP ilkesine uygun olarak bu kodu tekrardan yazalım:
function studentLastname (firstname) { this.firstname = firstname
let lastname = function() { ... } // ogrencinin soyadı}
function studentCount (_class) { this.class = _class
let studentCount = function() { ... } // sinifin ogrenci sayisi}
Bu kadar basit bir dönüşümle SOLID ilkesinin ilki olan SRP’yi kodumuza uygulamış olduk.
2. Open Closed Principle (OCP)
En genel tanımıyla bu prensip şunu amaçlamaktadır: Sınıfların gelişime açık, değişime kapalı olması. Bu, bir metodun davranışını genişletmek istiyorsanız, o metodun mevcut kodunu değiştirmeniz gerekmeyeceği anlamına gelir.
const roles = ["Admin", "User"];function checkRule(user) {
if (roles.includes(user)) {
return true;
} else {
return false;
}
}checkRule("Admin");
checkRule("Furkan");
Elimizde yukarıdaki gibi bir codebase olsun. checkRule()
metotu proje içerisinde çok kullanıldığını varsayarsak, burada fonksiyonun içerisine bir şey eklemek doğru olmaz. Zaten bundan önce gördüğümüz SRP prensibide buna karşı çıkıyor. Ama ben burada "Furkan" kullanıcısının da geçmesini istiyorum kontrolden. O zaman kodumuzu genişletmemiz gerekiyor. İşte böyle:
const roles = ["Admin", "User"];function checkRule(user) {
if (roles.includes(user)) {
return true;
} else {
return false;
}
}// yukarıdaki metotu değiştirmeden genişlettim
function addRole(role) {
roles.push(role);
}checkRule("Admin");addRole("Furkan");
checkRule("Furkan");
addRule()
metodunu eklemem istediğim sonucu vermemi sağladı. Bundan sonra sadece, o fonksiyonu çağırmam kaldı. Bu şekilde OCP prensibinide anlamış olduk.
3. Liskov Substitution Principle (LSP)
Liskov derki: değiştirilebilir özelliklerden yazılım sistemleri oluşturun. Başka bir deyişe, her alt sınıf, alt sınıfa özgü tüm yeni davranışlarla birlikte temel sınıftaki tüm davranışları korumalıdır. Alt sınıf, aynı istekleri işleyebilmeli ve üst sınıfıyla aynı görevleri tamamlayabilmelidir.
LSP’nin avantajı, aynı türdeki tüm alt sınıfların tutarlı bir kullanımı paylaştığı için yeni alt sınıfların geliştirilmesini hızlandırmasıdır. Yeni oluşturulan tüm alt sınıfların mevcut kodla çalışabilir. Yeni bir alt sınıfa ihtiyacınız olduğu zaman mevcut kodu yeniden çalışmadan oluşturabilmenize olanak sağlar.
class Rectangle {
constructor(width, height) {
this._width = width;
this._height = height;
} get width() {
return this._width;
}
get height() {
return this._height;
} set width(value) {
this._width = value;
}
set height(value) {
this._height = value;
} getArea() {
return this._width * this._height;
}
}class Square extends Rectangle {
constructor(size) {
super(size, size);
}
}const square = new Square(2);
square.width = 3;
console.log(square.getArea());
Konsolda 6 cevabını geldiğini gördüğünüzü varsayıyorum. Ama bizim istediğimiz değer 9. İşte burada LSP ihlali vardır. Bu ihlali şu şekilde düzeltebiliriz:
class Square extends Rectangle {
constructor(size) {
super(size, size);
} set width(value) {
this._width = this._height = value;
} set height(value) {
this._width = this._height = value;
}
}
Böylece kodumuz daha ölçeklenebilir hale geldi.
4. Interface Segregation Principle (ISP)
ISP’de sınıflar kullanmadıkları davranışları içermesi istenmez. Bu ilke şu avantajları sağlar:
- Daha az kod taşıyan metotlar elde edilir.
- Kodun ihtiyaç durumunda güncellemesi hızlanır.
- Davranıştan bir metot sorumlu olduğu için davranışta karşılaşılan problem hızlı çözülür.
Tek bir sorun var. JavaScript dünyasında interface yok :) Fakat bir şekilde bunu JS dünyasına uyarlıyabiliyoruz (pek tavsiye etmem).
class Phone {
constructor() {
if (this.constructor.name === "Phone")
throw new Error("Phone class is absctract");
} call(number) {} takePhoto() {} connectToWifi() {}
}
Şimdi bu fake interface’i IPhone’da kullanalım.
class IPhone extends Phone {
call(number) {} takePhoto() {} connectToWifi() {}
}
Peki bunu Nokia 3310'da kullanırsak ne ile karşılaşıcaz ?
class Nokia3310 extends Phone {
call(number) {} takePhoto() {} // kamerası yok ki connectToWifi() {} // wifi ne gezer o yıllarda
}
Şimdi en başta kullandığımız Phone
sınıfına bir revize atalım ve ihlali ortadan kaldıralım.
class Phone {
constructor() {
if (this.constructor.name === "Phone")
throw new Error("Phone class is absctract");
} call(number) {}
}
Ortak yapılan metotları yazmamız bu prensibe uymamız için yeterlidir.
5. Dependency Inversion Principle (DIP)
Yüksek seviyeli sınıflar, düşük seviyeli sınıflara bağlı olmamalıdır. Bunun yerine, her ikisi de soyutlamalara (interface) bağlı olmalıdır.
Genel olarak yazılımcılar yüksek seviyeli sınıflar yazmayı severler (bayılırız). DIP prensibinin en temel amacı; düşük ve yüksek seviyeli sınıfları ayırıp, her ikisinin de soyutlamalara (interface) bağlamaktır. Bunun sonucu olarak, yüksek ve düşük seviyeli sınıflar birbirinden yararlanabilir ama birindeki değişiklik doğrudan diğerini etkilememelidir.
class FileSystem {
writeToFile(data) {}
}class ExternalDB {
writeToDatabase(data) {}
}class LocalPersistance {
push(data) {}
}class PersistanceManager {
saveData(db, data) {
if (db instanceof FileSystem) {
db.writeToFile(data);
} if (db instanceof ExternalDB) {
db.writeToDatabase(data);
} if (db instanceof LocalPersistance) {
db.push(data);
}
}
}
Yüksek seviyeli sınıf olan PersistanceManager
, düşük seviyeli FileSystem
, ExternalDB
ve LocalPersstance
sınıflarına bağlıdır. DIP prensibini uyguladıktan sonra kod şu şekli alacaktır:
class FileSystem {
save(data) {}
}class ExternalDB {
save(data) {}
}class LocalPersistance {
save(data) {}
}class PersistanceManager {
saveData(db, data) {
db.save(data);
}
}
Sonuç
Yazılım geliştirirken: “Acaba SOLID ilkelerini ihlal ediyor muyum?” diye sormanız çok daha temiz, okunabilir ve ölçeklenebilir kodlar yazmanızı sağlayacaktır. İlk başlarda SOLID ilkelerine uymak zor olabilir ama kod yazdıkça bu ilkelere çok ihtiyacınızın olduğunu göreceksin.
Yazımı okuduğunuz için teşekkür ederim. Beni takip etmeyi unutmayın :)