Series: JavaScript Basics Lesson 81

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.