SOLID JS'da
SOLID'ning besh qoidasi (S, O, L, I, D) JS misollarida: pure function orqali Single Responsibility, prototype polymorphism orqali Open/Closed, extends shartnomasi orqali Liskov, Object.assign composition orqali Interface Segregation, va Dependency Injection orqali Dependency Inversion.
81-dars
SOLID JS'da
Design Patterns'da aytganimizdek, SOLID — bu V8 ichida ishlaydigan mexanizm emas, balki kod arxitekturasi qoidalari to'plami (1990-2000 yillarda Robert C. Martin tomonidan OOP uchun tizimlashtirilgan). Bu yerda "chuqurlashtirish" — har bir qoidani nega buzilishi kelajakda qanday aniq, o'lchanadigan muammo keltirib chiqarishini ko'rsatish bo'ladi, va bu qoidalarning oldingi bosqichlarda o'rgangan JS mexanizmlari (closure, prototype, first-class function) bilan qanday bog'lanishini ko'rsatish.
SOLID — besh qoidaning bosh harflaridan tashkil topgan:
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
S — Single Responsibility Principle (Yagona javobgarlik)
Qoida: Har bir funksiya/class — faqat bitta sababga ko'ra o'zgarishi kerak, ya'ni faqat bitta vazifani bajarishi kerak.
// ❌ YOMON - bitta funksiya UCHTA turli vazifani bajaryapti
function processOrder(order) {
// 1. Validatsiya
if (!order.items.length) throw new Error("Bo'sh buyurtma");
// 2. Narxni hisoblash
const total = order.items.reduce((sum, item) => sum + item.price, 0);
// 3. Serverga saqlash
fetch('/api/orders', { method: 'POST', body: JSON.stringify({ ...order, total }) });
return total;
}
Bu funksiya uchta sababga ko'ra o'zgarishi mumkin: validatsiya qoidasi o'zgarsa, narx hisoblash formulasi o'zgarsa, yoki API endpoint o'zgarsa — hammasi shu bitta funksiyaga tegishi kerak. Bu — test yozishni ham qiyinlashtiradi: narx hisoblashni test qilish uchun sizga tarmoq so'rovini mock qilish kerak bo'ladi, garchi siz faqat matematikani tekshirmoqchi bo'lsangiz ham.
// ✅ YAXSHI - har biri bitta vazifa
function validateOrder(order) {
if (!order.items.length) throw new Error("Bo'sh buyurtma");
}
function calculateTotal(order) {
return order.items.reduce((sum, item) => sum + item.price, 0);
}
function saveOrder(order, total) {
return fetch('/api/orders', { method: 'POST', body: JSON.stringify({ ...order, total }) });
}
function processOrder(order) {
validateOrder(order);
const total = calculateTotal(order);
saveOrder(order, total);
return total;
}
Endi calculateTotalni tarmoqqa umuman tegmasdan, sof funksiya sifatida test qilish mumkin (bu — oldingi bandda o'rgangan pure function g'oyasi bilan bevosita bog'liq).
O — Open/Closed Principle (Ochiq/Yopiq printsipi)
Qoida: Kod kengaytirish uchun ochiq, lekin o'zgartirish uchun yopiq bo'lishi kerak — ya'ni, yangi funksionallik qo'shish uchun mavjud, ishlab turgan kodni o'zgartirish shart bo'lmasligi kerak.
// ❌ YOMON - yangi shakl qo'shilganda, MAVJUD funksiyani o'zgartirish kerak
function calculateArea(shape) {
if (shape.type === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
}
// Uchburchak qo'shmoqchimisiz? Bu funksiyani yana o'zgartirishga majbursiz
}
Muammo — har safar yangi shakl qo'shilganda, allaqachon ishlab turgan, test qilingan calculateArea funksiyasiga tegish kerak bo'ladi. Bu — regressiya xatosi (yangi o'zgarish eski, ishlab turgan qismni buzib qo'yishi) xavfini oshiradi.
// ✅ YAXSHI - har bir shakl o'zining hisoblash mantig'ini "olib yuradi"
class Circle {
constructor(radius) { this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }
}
class Rectangle {
constructor(width, height) { this.width = width; this.height = height; }
area() { return this.width * this.height; }
}
// Yangi shakl qo'shish - FAQAT YANGI class, eskisiga tegilmaydi
class Triangle {
constructor(base, height) { this.base = base; this.height = height; }
area() { return 0.5 * this.base * this.height; }
}
function calculateArea(shape) {
return shape.area(); // hech qanday if/else kerak emas
}
Bu yerda polymorphism (prototype-based inheritance g'oyasi) ishlyapti: calculateArea shaklning aniq turini bilishi shart emas, u faqat shape.area() metodi mavjudligiga ishonadi. Yangi Triangle qo'shish — mavjud Circle/Rectangle kodiga hech qanday tegishni talab qilmaydi.
L — Liskov Substitution Principle (Liskov almashtirish printsipi)
Qoida: Agar B — A'dan meros olgan bo'lsa, A ishlatilgan har qanday joyda B'ni hech narsani buzmasdan almashtirish mumkin bo'lishi kerak.
// ❌ YOMON - Square, Rectangle'dan meros oladi, lekin uning "shartnomasini" buzadi
class Rectangle {
setWidth(w) { this.width = w; }
setHeight(h) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w) {
this.width = w;
this.height = w; // Square uchun width=height bo'lishi SHART
}
setHeight(h) {
this.width = h;
this.height = h;
}
}
function testRectangle(rect) {
rect.setWidth(4);
rect.setHeight(5);
console.log(rect.area()); // Rectangle uchun kutilgan: 20
}
testRectangle(new Rectangle()); // 20 ✅
testRectangle(new Square()); // 25 ❌ - kutilmagan natija!
Muammo aniq: Square — Rectanglening barcha metodlarini meros oladi, lekin uning xatti-harakatini o'zgartiradi shu tarzda-ki, Rectangle uchun to'g'ri bo'lgan taxmin (setWidth va setHeight bir-biridan mustaqil) Square uchun noto'g'ri bo'lib qoladi. Bu — extends ishlatilganda (Class inheritance) faqat sintaksis to'g'ri bo'lishi yetarli emasligini, balki mantiqiy shartnoma (contract) ham saqlanishi kerakligini ko'rsatadi.
Yechim: bu holatda Squareni Rectangledan meros qilib olmaslik kerak — ular matematik jihatdan bog'liq bo'lsa-da, dasturiy xatti-harakat jihatidan turli "shartnoma"larga ega.
I — Interface Segregation Principle (Interfeys ajratish printsipi)
Qoida: Client ishlatmaydigan metodlarga bog'liq bo'lishga majburlanmasligi kerak — kichik, aniq interfeyslar, bitta katta "hamma narsani qamrab oluvchi" interfeysdan yaxshiroq.
JS'da klassik "interface" kalit so'zi yo'q (TypeScript'dan tashqari), lekin g'oya object shape darajasida qo'llaniladi:
// ❌ YOMON - bitta "katta" interfeys, hamma narsani talab qiladi
class Worker {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
}
class RobotWorker extends Worker {
work() { console.log('Ishlayapti'); }
eat() { throw new Error('Robot ovqat yemaydi!'); } // majburiy, lekin mantiqsiz
sleep() { throw new Error('Robot uxlamaydi!'); }
}
RobotWorker — Workerdan meros olgani uchun, ishlatmaydigan (eat, sleep) metodlarni ham "meros qilib olishga" majbur. Bu — chalkash, xato-qiluvchan API yaratadi.
// ✅ YAXSHI - kichik, alohida "qobiliyatlar" (JS'da mixin/composition orqali)
const canWork = { work() { console.log('Ishlayapti'); } };
const canEat = { eat() { console.log('Ovqatlanyapti'); } };
const canSleep = { sleep() { console.log('Uxlayapti'); } };
const human = Object.assign({}, canWork, canEat, canSleep);
const robot = Object.assign({}, canWork); // faqat kerakli qobiliyat
Bu yerda Object.assign orqali composition (obyektlarni birlashtirish) ishlatilyapti — bu Object.create va prototip zanjiriga alternativ yondashuv: "inheritance" (meros) o'rniga "composition" (tarkib) — zamonaviy JS'da ko'proq tavsiya etiladigan uslub ("composition over inheritance" degan mashhur qoida shu yerdan kelib chiqadi).
D — Dependency Inversion Principle (Bog'liqlikni teskari aylantirish)
Qoida: Yuqori darajadagi modul quyi darajadagi modulning konkret implementatsiyasiga emas, balki abstraktsiyaga (interfeysga) bog'liq bo'lishi kerak.
// ❌ YOMON - UserService to'g'ridan-to'g'ri MySQLDatabase'ga "qattiq bog'langan"
class MySQLDatabase {
save(data) { console.log('MySQL ga saqlandi:', data); }
}
class UserService {
constructor() {
this.db = new MySQLDatabase(); // qattiq bog'liqlik
}
createUser(user) {
this.db.save(user);
}
}
Muammo: agar ertaga MongoDB'ga o'tish kerak bo'lsa, UserServicening ichiga kirib o'zgartirish kerak bo'ladi. Bundan tashqari, test yozish qiyin — UserServiceni test qilish uchun haqiqiy MySQL ulanishi kerak bo'ladi.
// ✅ YAXSHI - Dependency Injection orqali, "abstraktsiya"ga bog'liq
class UserService {
constructor(database) { // konkret emas, "har qanday database" qabul qilinadi
this.db = database;
}
createUser(user) {
this.db.save(user);
}
}
class MySQLDatabase {
save(data) { console.log('MySQL ga saqlandi:', data); }
}
class MongoDatabase {
save(data) { console.log('MongoDB ga saqlandi:', data); }
}
// Kerakli implementatsiya TASHQARIDAN "inject" qilinadi
const service1 = new UserService(new MySQLDatabase());
const service2 = new UserService(new MongoDatabase());
// Test uchun - soxta (mock) database ham osongina beriladi
const mockDb = { save: (data) => console.log('Mock saqlandi:', data) };
const testService = new UserService(mockDb);
Bu — Dependency Injection deb ataladigan texnika, va bu aynan D qoidasining amaliy natijasi: UserService endi qaysi konkret database ishlatilishini bilmaydi, u faqat .save(data) metodi mavjudligiga ishonadi (bu — JS'ning duck typing xususiyati: "agar u o'rdakdek yursa va o'rdakdek g'ag'illasa — u o'rdak" — class ierarxiyasi emas, metod mavjudligi muhim).
Xulosa — SOLID va oldingi mavzular orasidagi bog'liqlik
| Qoida | Qaysi JS mexanizmiga tayanadi |
|---|---|
| Single Responsibility | Pure function (State management'dagi reducer g'oyasi) |
| Open/Closed | Prototype-based polymorphism |
| Liskov Substitution | extends/inheritance shartnomasi |
| Interface Segregation | Object composition, Object.assign (class'siz JS'ning tabiiy kuchi) |
| Dependency Inversion | First-class function/obyekt, Factory pattern |
Muhim amaliy eslatma — JS'da SOLID qanchalik "qattiq" qo'llaniladi: SOLID asli klassik OOP tillari (Java, C#) uchun ishlab chiqilgan, ular qat'iy interfeys/class tizimiga ega. JS dinamik, prototype-based, duck-typing til bo'lgani uchun, ba'zi qoidalar (ayniqsa I — Interface Segregation) JS'da rasmiy interfeys shaklida emas, balki kelishuv (convention) darajasida qo'llaniladi. Shuning uchun tajribali JS dasturchilar SOLID'ni so'zma-so'z emas, balki ruhini (kodni bog'liqsiz, testlanadigan, kengaytiriladigan qilish) saqlagan holda moslashtirib ishlatishadi.