Tarkibga o'tish

05 β€” SOLID printsiplari

⬅️ Oldingi: 04 β€” Coupling va cohesion Β· 🏠 README Β· Keyingi: 06 β€” Boshqa printsiplar: DRY, KISS, YAGNI ➑️


Bu bobda: SOLID β€” obyektga yo'naltirilgan dizaynning beshta printsipi (SRP, OCP, LSP, ISP, DIP). Har birini "nima, nega, qachon" tarzida, yomon misol ❌ va yaxshi misol βœ… bilan ko'ramiz. Bu printsiplar oldingi bobdagi coupling/cohesion g'oyalarining amaliy ko'rinishi: ularning maqsadi β€” kodni o'zgartirishga oson qilish. Ayniqsa DIP (Dependency Inversion) muhim β€” u keyinroq o'rganadigan hexagonal va Clean arxitekturaning poydevori (12-13 bob).

Trade-off eslatmasi / Halollik: SOLID β€” dogma emas, vosita. Maqsad β€” kelajakdagi o'zgarishni arzonlashtirish, abstraksiya soni rekordini o'rnatish emas. Har bir printsipni haddan oshirib qo'llash (over-engineering) o'z xatosi. Bu bobdagi barcha TypeScript misollari $env:TEMP/arx-probe da tsc --strict va tsx orqali haqiqatan ishga tushirib tekshirilgan β€” bob oxirida natijalar keltirilgan.


SOLID nima va nega kerak

SOLID β€” beshta dizayn printsipining bosh harflaridan tuzilgan qisqartma. Ularni 2000-yillarda Robert C. Martin (Uncle Bob) mashhur qildi (printsiplarning ayrimlari undan oldin ham mavjud edi β€” masalan LSP'ni 1988-yilda Barbara Liskov ta'riflagan, OCP'ni Bertrand Meyer kiritgan). "SOLID" so'zini esa Michael Feathers taklif qilgan.

Harf Nomi Bir jumlada
S Single Responsibility (SRP) Sinf bitta o'zgarish sababiga ega bo'lsin
O Open/Closed (OCP) Kengaytirishga ochiq, o'zgartirishga yopiq
L Liskov Substitution (LSP) Pastki tip ota tipni buzmasdan almashtirsin
I Interface Segregation (ISP) Kichik, maxsus interfeyslar; ishlatilmaydigan metodga bog'lanma
D Dependency Inversion (DIP) Abstraksiyaga bog'lan, konkret klassga emas

Lekin eng muhim savol: nega? SOLID printsiplarining yagona umumiy maqsadi bor β€” o'zgarishga moslashuvchan kod. Dasturiy ta'minot doim o'zgaradi: yangi talab keladi, biznes qoidasi o'zgaradi, integratsiya almashadi. Yomon dizaynda bitta kichik o'zgarish o'nlab faylga tarqaladi (04-bobdagi tight coupling). SOLID β€” aynan shu tarqalishni kamaytirish texnikasi.

Eslatma: 04-bobni eslang β€” loose coupling (zaif bog'liqlik) va high cohesion (yuqori jipslik) yaxshi dizaynning o'lchovi edi. SOLID β€” o'sha o'lchovlarga erishishning amaliy retsepti. SRP cohesion'ni oshiradi; DIP, OCP, ISP coupling'ni kamaytiradi.

Diqqat β€” SOLID dogma emas. SOLID'ni "har bir sinf uchun interfeys yarat" deb tushunish β€” keng tarqalgan xato. Bu over-engineering'ga olib keladi: bitta implementatsiyali interfeys, foydasiz abstraksiya qatlamlari, o'qib bo'lmaydigan kod. To'g'ri yondashuv: o'zgarish ehtimoli yuqori joyni moslashuvchan qil, qolganini sodda qoldir. Bu bob har printsipning "qachon kerak emas"ligini ham aytadi.


SRP β€” Single Responsibility (yagona mas'uliyat)

Ta'rif va eng katta tushunmovchilik

"Bir sinf o'zgarishi uchun faqat bitta sabab bo'lishi kerak." β€” Robert C. Martin

Bu eng ko'p noto'g'ri tushuniladigan printsip. Eng keng tarqalgan xato β€” "SRP degani sinfda bitta metod bo'lishi kerak" deb o'ylash. Bu noto'g'ri. SRP metodlar soni haqida emas.

Robert Martin keyinchalik ta'rifni aniqlashtirdi: "bir sinf bitta aktyor (manfaatdor tomon, stakeholder) oldida javobgar bo'lsin". Ya'ni: agar sinfingizni turli sabablarga ko'ra, turli odamlar (boshqaruv, dizayner, DBA) o'zgartirishni talab qilsa β€” bu sinf juda ko'p mas'uliyatga ega.

Intuitsiya: o'zingizdan so'rang: "bu sinfni o'zgartirishni kim so'raydi?" Agar javob bitta bo'lsa (masalan, faqat marketing bo'limi) β€” SRP yaxshi. Agar uchta turli bo'lim bo'lsa β€” sinf uchta sababga bog'langan.

Yomon misol ❌ β€” aralashgan mas'uliyatlar

E-commerce loyihasida bitta HisobotXizmati sinfi: hisobotni tuzadi, uni HTML formatlaydi, va diskka saqlaydi.

// ❌ YOMON: bitta sinf 3 ta o'zgarish sababiga ega
class HisobotXizmati {
  // 1) Biznes mantig'i β€” buxgalteriya o'zgarsa o'zgaradi
  yarat(qatorlar: string[]) { /* hisob-kitob */ }

  // 2) Taqdimot β€” dizayner HTML'ni o'zgartirsa o'zgaradi
  htmlGa() { /* <h1>...</h1> */ }

  // 3) Saqlash β€” fayl tizimidan bazaga o'tsak o'zgaradi
  saqla(nom: string) { /* diskka yoz */ }
}

Muammo: HTML dizaynini o'zgartirsak β€” biznes mantiqli sinfga tegamiz. Saqlashni bazaga ko'chirsak β€” yana shu sinfga tegamiz. Uch xil sabab, bitta fayl. Bu nima bilan xavfli? Har tegishda regressiya (eski xato qaytishi) xavfi bor, va testlash qiyinlashadi.

Yaxshi misol βœ… β€” mas'uliyatlarni ajratish

// βœ… YAXSHI: har sinf bitta sababga o'zgaradi
interface Hisobot {
  sarlavha: string;
  qatorlar: string[];
}

class HisobotGeneratori {            // sabab: biznes mantig'i
  yarat(qatorlar: string[]): Hisobot {
    return { sarlavha: "Oylik savdo", qatorlar };
  }
}

class HisobotFormatlovchi {          // sabab: taqdimot
  htmlGa(h: Hisobot): string {
    return `<h1>${h.sarlavha}</h1><ul>${h.qatorlar
      .map((q) => `<li>${q}</li>`)
      .join("")}</ul>`;
  }
}

class HisobotSaqlovchi {             // sabab: saqlash mexanizmi
  private xotira = new Map<string, string>();
  saqla(nom: string, mazmun: string): void { this.xotira.set(nom, mazmun); }
  oqi(nom: string): string | undefined { return this.xotira.get(nom); }
}

Endi har sinf bitta aktyorga javob beradi. HTML o'zgarsa β€” faqat HisobotFormatlovchi. Saqlash bazaga o'tsa β€” faqat HisobotSaqlovchi (yoki uning yangi varianti). Biznes mantiq daxlsiz qoladi.

SRP: aralashgan mas'uliyatni uch sinfga ajratish va OCP: yangi to'lov turini mavjud kodga tegmasdan qo'shish

Amaliyotda: SRP'ni "ajratish kerakmi?" deb hal qilishda o'zgarish chastotasiga qarang. Agar formatlash va saqlash hech qachon alohida o'zgarmasa (masalan, kichik skriptda) β€” ularni bitta sinfda qoldirish to'g'ri. SRP'ni mexanik qo'llab, har funksiyani alohida sinfga bo'lib chiqish β€” anemik, sochilib ketgan dizayn (over-engineering). Mas'uliyat = o'zgarish o'qi, satr soni emas.


OCP β€” Open/Closed (ochiq/yopiq)

Ta'rif

"Dasturiy modul kengaytirishga ochiq, lekin o'zgartirishga yopiq bo'lishi kerak." β€” Bertrand Meyer

Ma'nosi: yangi xulq qo'shganda mavjud, sinalgan kodga tegmaslik kerak. Yangi xususiyatni β€” yangi kod yozib qo'shamiz, eskisini o'chirib-qayta yozib emas. Bu nega muhim? Chunki ishlab turgan kodga har tegish β€” yangi xato kiritish xavfi. "Yopiq" β€” barqarorlik; "ochiq" β€” o'sish.

Klassik anti-pattern β€” uzun if/else yoki switch turlar bo'yicha. Har yangi tur qo'shilganda shu switchga yana bitta case qo'shasiz. Bu OCP'ni buzadi.

Yomon misol ❌ β€” har yangi to'lov turi switch'ni o'zgartiradi

// ❌ YOMON: yangi tur = mavjud metodni TAHRIRLASH
class Tolovchi {
  tola(usul: string, summa: number): string {
    if (usul === "karta") return `Kartadan ${summa} yechildi`;
    else if (usul === "naqd") return `${summa} naqd qabul qilindi`;
    // "click" qo'shish uchun -> shu metodni OCHIB tahrirlaymiz ❌
    else throw new Error("Noma'lum usul");
  }
}

Telegram-bot to'lov tizimida yangi provayder (Click, Payme, Uzum) qo'shilsa β€” har safar shu tola metodini ochib, yana bitta else if qo'shamiz. Metod o'sib, "ochiq jarrohlik"ga aylanadi va har tahrirda boshqa to'lov turlarini buzish xavfi paydo bo'ladi.

Yaxshi misol βœ… β€” strategiya / polimorfizm

Har to'lov turini umumiy interfeysni bajaruvchi alohida sinf qilamiz:

// βœ… YAXSHI: yangi tur = yangi sinf, mavjud kod O'ZGARMAYDI
interface TolovUsuli {
  nom: string;
  tola(summa: number): string;
}

class Karta implements TolovUsuli {
  nom = "karta";
  tola(summa: number) { return `Kartadan ${summa} so'm yechildi`; }
}
class Naqd implements TolovUsuli {
  nom = "naqd";
  tola(summa: number) { return `${summa} so'm naqd qabul qilindi`; }
}
// Yangi usul β€” Tolovchi'ga TEGMASDAN qo'shildi:
class Click implements TolovUsuli {
  nom = "click";
  tola(summa: number) { return `Click orqali ${summa} so'm to'landi`; }
}

class Tolovchi {
  // Bu sinf "yopiq" β€” endi hech qachon o'zgarmaydi
  bajar(usul: TolovUsuli, summa: number): string {
    return usul.tola(summa);
  }
}

Payme qo'shmoqchimisiz? Yangi sinf yozasiz, Tolovchiga umuman tegmaysiz. Mana shu β€” "kengaytirishga ochiq, o'zgartirishga yopiq".

Trade-off: OCP bepul emas. Har bir abstraksiya (interfeys) β€” qo'shimcha murakkablik. Agar to'lov turlari hech qachon ko'paymasa (va shunday qolishi aniq bo'lsa), oddiy switch ham yetarli. OCP'ni o'zgarish kelishi ehtimoli yuqori o'qda qo'llang. Hamma narsani oldindan abstraktlash β€” YAGNI buzilishi (06-bobda). Qoida: "uchinchi marta case qo'shayotgan bo'lsangiz β€” strategiyaga o'ting".

Eslatma: OCP'ni amalga oshirishning eng keng tarqalgan yo'li β€” Strategy patterni (09-bob). Yuqoridagi TolovUsuli aslida Strategy'ning aynan o'zi. DIP bilan birga, OCP design patternlarning ko'pchiligi asosida yotadi.


LSP β€” Liskov Substitution (Liskov almashtiruvi)

Ta'rif

"Agar S tipi T tipining pastki tipi bo'lsa, T tipidagi obyektlarni S tipidagi obyektlar bilan dasturni buzmasdan almashtirish mumkin bo'lishi kerak." β€” Barbara Liskov (1988)

Oddiy tilda: pastki sinf (subclass) ota sinfning shartnomasini bajarishi shart. Agar funksiya Qush qabul qilsa, unga istalgan Qush (jumladan pingvin) berilganda ham to'g'ri ishlashi kerak β€” kutilmagan xato bermasdan.

LSP buzilishi odatda meros (inheritance) noto'g'ri ishlatilganda yuzaga keladi: "X β€” bu Y" deb meros qildik, lekin X aslida Y'ning hamma va'dasini bajara olmaydi.

Yomon misol ❌ β€” uchmaydigan "uchuvchi" (Pingvin-Qush)

// ❌ YOMON: Qush bazasiga uch() qo'yib, hamma qush uchadi deb taxmin qildik
abstract class Qush {
  abstract uch(): string;   // <- bu yerda buzilish urug'i
  abstract ovqatlan(): string;
}

class Chumchuq extends Qush {
  uch() { return "Chumchuq uchdi"; }
  ovqatlan() { return "don yedi"; }
}

class Pingvin extends Qush {
  uch() { throw new Error("Pingvin ucholmaydi!"); } // ❌ shartnomani buzdi
  ovqatlan() { return "baliq yedi"; }
}

function hammasiniUchiramiz(qushlar: Qush[]) {
  for (const q of qushlar) q.uch(); // Pingvin kelsa -> qulaydi!
}

hammasiniUchiramiz funksiyasi har Qush ucha oladi deb ishonadi. Lekin Pingvin kelganda exception otadi β€” dastur buziladi. Pingvin Qush'ni xavfsiz almashtira olmadi β€” LSP buzildi. Muqobil "yechim" β€” uch()ni bo'sh qoldirish β€” yana yomon: soxta, jim natija (no-op), bu yanada chigal xatolarga olib keladi.

Yaxshi misol βœ… β€” qobiliyatni ajratish

Muammoning ildizi: uchish β€” har qushning xususiyati emas. Demak uni bazaga emas, alohida qobiliyat interfeysiga chiqaramiz:

// βœ… YAXSHI: umumiy baza faqat ROST umumiy xulqni saqlaydi
abstract class Qush {
  constructor(public nom: string) {}
  abstract ovqatlan(): string;     // har qush ovqatlanadi β€” ROST umumiy
}

interface Uchuvchi {               // uchish β€” alohida qobiliyat
  uch(): string;
}

class Chumchuq extends Qush implements Uchuvchi {
  ovqatlan() { return `${this.nom} don yedi`; }
  uch() { return `${this.nom} uchdi`; }
}

class Pingvin extends Qush {        // Uchuvchi'ni IMPLEMENT QILMAYDI
  ovqatlan() { return `${this.nom} baliq yedi`; }
  suz() { return `${this.nom} suzdi`; }
}

function hammasiniBoqamiz(qushlar: Qush[]): string[] {
  return qushlar.map((q) => q.ovqatlan());   // har Qush xavfsiz
}
function faqatUchuvchilar(u: Uchuvchi[]): string[] {
  return u.map((x) => x.uch());              // Pingvin bu yerga UMUMAN o'tmaydi
}

Endi Pingvinni faqatUchuvchilarga berib bo'lmaydi β€” kompilyator ruxsat bermaydi. "Uchmaydigan uchuvchi" muammosi ildizdan yo'qoladi.

LSP: Pingvin Qush'dan uch() metodini meros qilsa shartnoma buziladi; uchish qobiliyatini alohida Uchuvchi interfeysiga ajratish to'g'ri yechim

Diqqat β€” Kvadrat/To'rtburchak paradoksi. LSP'ning eng mashhur misoli: matematikada kvadrat β€” to'rtburchakning xususiy holi, demak Kvadrat extends Tortburchak mantiqiy ko'rinadi. Lekin to'rtburchakda eniniOzgartir(5) faqat enini o'zgartiradi; kvadratda esa ikkala tomonni o'zgartirishga majbur β€” bu "to'rtburchak hulqi"ni buzadi. Funksiya Tortburchak kutib, eni=5, boyi=4 deb yuza 20 chiqishini kutsa, kvadrat 16 yoki 25 qaytaradi. Saboq: "real dunyoda X β€” bu Y" har doim "kod meros"ini oqlamaydi. Yechimini Mashqlar bo'limida ko'ramiz.

Amaliyotda: LSP buzilishini sezishning belgilari β€” pastki sinfda throw new Error(), bo'sh override (no-op), yoki if (obj instanceof Pingvin) kabi tip tekshiruvlar. Bularni ko'rsangiz β€” meros iyerarxiyasini qayta ko'rib chiqing. Ko'pincha yechim: meros o'rniga kompozitsiya yoki interfeys ajratish (06-bobdagi "meros o'rniga kompozitsiya").


ISP β€” Interface Segregation (interfeysni ajratish)

Ta'rif

"Mijozlarni ular ishlatmaydigan metodlarga bog'lanishga majburlamang." β€” Robert C. Martin

Ma'nosi: bitta semiz interfeys (ko'p metodli) o'rniga, bir nechta kichik, maxsus interfeyslar tuzing. Shunda har mijoz faqat o'ziga kerakli metodlarga bog'lanadi.

Nega muhim? Agar interfeysingizda 10 ta metod bo'lsa va sinf faqat 3 tasini ishlatsa β€” qolgan 7 tasini ham bajarishga majbur (odatda bo'sh yoki throw bilan β€” bu LSP'ni ham buzadi). Bundan tashqari, interfeysning ishlatilmaydigan qismi o'zgarsa, sizning sinfingiz ham qayta kompilyatsiya/qayta test bo'ladi β€” keraksiz bog'liqlik.

Yomon misol ❌ β€” semiz interfeys

// ❌ YOMON: hamma "ishchi" ovqatlanadi va uxlaydi deb taxmin
interface Ishchi {
  ishla(): string;
  ovqatlan(): string;
  uxla(): string;
}

class Odam implements Ishchi {
  ishla() { return "ishladi"; }
  ovqatlan() { return "tushlik qildi"; }
  uxla() { return "uxladi"; }
}

class Robot implements Ishchi {
  ishla() { return "24/7 ishladi"; }
  ovqatlan() { throw new Error("Robot ovqatlanmaydi"); } // ❌ majburiy, ma'nosiz
  uxla() { throw new Error("Robot uxlamaydi"); }         // ❌
}

Robot Ishchi'ga bog'langani uchun ovqatlan va uxlani ham bajarishga majbur β€” garchi ular mantiqsiz bo'lsa ham. Bu throw lar yana LSP'ni buzadi.

Yaxshi misol βœ… β€” kichik maxsus interfeyslar

// βœ… YAXSHI: har qobiliyat β€” alohida kichik interfeys
interface Ishlovchi { ishla(): string; }
interface Ovqatlanuvchi { ovqatlan(): string; }

class OdamIshchi implements Ishlovchi, Ovqatlanuvchi {
  ishla() { return "Odam ishladi"; }
  ovqatlan() { return "Odam tushlik qildi"; }
}

class RobotIshchi implements Ishlovchi {
  // Faqat o'ziga keraklisi β€” ovqatlanishga BOG'LANMAYDI
  ishla() { return "Robot 24/7 ishladi"; }
}

function smenanIshlat(ishchilar: Ishlovchi[]): string[] {
  return ishchilar.map((i) => i.ishla());  // Odam ham, Robot ham mos
}

Endi smenanIshlat faqat Ishlovchi'ga bog'langan, ovqatlanishga emas. RobotIshchi ma'nosiz metodlarni bajarmaydi. Kerak bo'lsa, sinf bir nechta kichik interfeysni birga bajaradi (OdamIshchi).

Eslatma: ISP β€” bu SRP'ning interfeyslar darajasidagi ko'rinishi. SRP "sinf bitta sababga", ISP "interfeys bitta rolga" deydi. Ikkalasi ham bir g'oyaga xizmat qiladi: keraksiz bog'liqlikni yo'qotish.

Trade-off: Interfeyslarni juda mayda bo'lakka bo'lib yuborish ham yomon β€” yuzlab bir metodli interfeys kodni o'qishni qiyinlashtiradi. Rol bo'yicha guruhlang: bir-biri bilan doim birga ishlatiladigan metodlar bitta interfeysda qolsin. ISP "interfeyslar mayda bo'lsin" emas, "mijoz ishlatmaydiganga bog'lanmasin" deydi.


DIP β€” Dependency Inversion (bog'liqlik inversiyasi)

Ta'rif β€” eng muhim printsip

"(a) Yuqori darajali modullar past darajali modullarga bog'lanmasin; ikkalasi ham abstraksiyaga bog'lansin. (b) Abstraksiyalar detallarga bog'lanmasin; detallar abstraksiyaga bog'lansin." β€” Robert C. Martin

Bu printsipni tushunish uchun ikki tushuncha kerak:

  • Yuqori daraja β€” biznes mantig'i, "nima qilish kerak" (masalan BuyurtmaXizmati).
  • Past daraja β€” texnik detal, "qanday qilish kerak" (masalan EmailXabarchi, baza, fayl).

Odatdagi (yomon) bog'liqlik: yuqori daraja pastdan foydalanadi, demak unga to'g'ridan-to'g'ri bog'lanadi. DIP buni teskari aylantiradi ("inversion"): o'rtaga abstraksiya (interfeys) qo'yamiz va ikkala daraja ham o'sha abstraksiyaga bog'lanadi. Endi past daraja abstraksiyaga "bo'ysunadi".

Yomon misol ❌ β€” biznes mantiq konkret klassga bog'langan

// ❌ YOMON: yuqori daraja KONKRET EmailXabarchi'ni o'zi yaratadi
class EmailXabarchi {
  yubor(kimga: string, matn: string) { return `EMAIL -> ${kimga}: ${matn}`; }
}

class BuyurtmaXizmati {
  private xabarchi = new EmailXabarchi();  // ❌ qattiq bog'langan (new)
  tasdiqla(mijoz: string) {
    return this.xabarchi.yubor(mijoz, "Buyurtmangiz qabul qilindi");
  }
}

Muammolar: 1. SMSga o'tmoqchi bo'lsak β€” biznes sinfini (BuyurtmaXizmati) o'zgartiramiz. OCP ham buzildi. 2. Test: BuyurtmaXizmatini tekshirsak, har test haqiqiy email yuborib yuboradi β€” buni "soxta" (mock) bilan almashtirish mumkin emas, chunki new ichkarida qotib qolgan.

Yaxshi misol βœ… β€” abstraksiyaga bog'lanish + injection

// βœ… YAXSHI: ikkala daraja ham Xabarchi abstraksiyasiga bog'langan
interface Xabarchi {
  yubor(kimga: string, matn: string): string;
}

class EmailXabarchi implements Xabarchi {
  yubor(kimga: string, matn: string) { return `EMAIL -> ${kimga}: ${matn}`; }
}
class SmsXabarchi implements Xabarchi {
  yubor(kimga: string, matn: string) { return `SMS -> ${kimga}: ${matn}`; }
}

class BuyurtmaXizmati {
  // Konkretga EMAS, Xabarchi abstraksiyasiga bog'langan.
  // Bog'liqlik TASHQARIDAN beriladi (dependency injection):
  constructor(private xabarchi: Xabarchi) {}
  tasdiqla(mijoz: string): string {
    return this.xabarchi.yubor(mijoz, "Buyurtmangiz qabul qilindi");
  }
}

const emailBilan = new BuyurtmaXizmati(new EmailXabarchi());
const smsBilan = new BuyurtmaXizmati(new SmsXabarchi());

Endi BuyurtmaXizmati Xabarchini qanday bajarilishini bilmaydi β€” faqat shartnomani biladi. SMS'ga o'tish β€” biznes kodiga tegmasdan, faqat new BuyurtmaXizmati(new SmsXabarchi()). Test esa endi oson:

// Test uchun soxta (mock) β€” biznes kodiga TEGMASDAN ulanadi
class SoxtaXabarchi implements Xabarchi {
  yuborilganlar: string[] = [];
  yubor(kimga: string, matn: string) {
    const yozuv = `MOCK ${kimga}:${matn}`;
    this.yuborilganlar.push(yozuv);
    return yozuv;
  }
}
const soxta = new SoxtaXabarchi();
new BuyurtmaXizmati(soxta).tasdiqla("Davron");
// soxta.yuborilganlar.length === 1  -> haqiqiy email yubormasdan tekshirildi

DIP: avval yuqori daraja konkret klassga bog'langan, keyin ikkala daraja ham abstraksiyaga bog'lanadi va o'q yo'nalishi teskari bo'ladi

Diqqat β€” DIP β‰  DI. Ikki tushunchani aralashtirmang. DIP (Dependency Inversion Principle) β€” dizayn printsipi: "abstraksiyaga bog'lan". DI (Dependency Injection) β€” texnika: bog'liqlikni tashqaridan (konstruktor orqali) berish. DI β€” DIP'ga erishishning bir usuli, lekin ular sinonim emas. DI konteyneri (Spring, NestJS) β€” shunchaki bu uzatishni avtomatlashtiruvchi vosita.

Amaliyotda: DIP β€” bu butun arxitektura poydevori, faqat sinf darajasidagi hiyla emas. Diagrammadagi o'q yo'nalishiga e'tibor bering: infratuzilma (past daraja) markazga (abstraksiyaga) ishora qiladi. Aynan shu g'oya hexagonal arxitektura (portlar va adapterlar, 12-bob) va Clean Architecture'ning (13-bob) "dependency rule"i: bog'liqlik o'qi doim ichkariga, biznes mantig'iga qaratilgan. Biznes mantiq baza, framework, tashqi API'ni bilmaydi β€” ular abstraksiya orqali ulanadi. SOLID'dan tizim arxitekturasiga ko'prik aynan shu yerda.


SOLID birga ishlaydi

Bu printsiplar alohida emas, birga ishlaydi va ko'pincha biri ikkinchisini qo'llab-quvvatlaydi:

  • DIP + OCP β€” abstraksiyaga bog'lanish yangi implementatsiya qo'shishni oson qiladi (yopiq sinf, ochiq kengayish).
  • ISP + LSP β€” kichik, to'g'ri interfeyslar "uchmaydigan uchuvchi" kabi shartnoma buzilishlarining oldini oladi.
  • SRP + ISP β€” biri sinf darajasida, biri interfeys darajasida bir xil maqsadga (keraksiz bog'liqlikni kesish) xizmat qiladi.

Yagona umumiy maqsad β€” bobning boshida aytganimiz: o'zgarishni arzonlashtirish. Har bir printsipni qo'llashdan oldin so'rang: "bu yerda o'zgarish kelishi ehtimoli bormi? Bu abstraksiya o'sha o'zgarishni arzonlashtiradimi?" Agar javob "yo'q" bo'lsa β€” sodda qoldiring.

Anti-pattern β€” SOLID over-engineering. Eng keng tarqalgan SOLID xatosi β€” printsiplarni mexanik, kontekstsiz qo'llash: har sinf uchun interfeys (ko'pincha bitta implementatsiyali), har funksiya uchun alohida sinf, beshta qatlam abstraksiya. Natija β€” "AbstractFactoryProviderManagerImpl" jahannami: o'qib bo'lmaydigan, kuzatib bo'lmaydigan kod. SOLID o'qishni osonlashtirishi kerak, qiyinlashtirishi emas. Agar abstraksiya hech qanday haqiqiy o'zgarishni arzonlashtirmasa β€” u faqat shovqin (06-bobdagi YAGNI va KISS bilan muvozanat saqlang).


Mashqlar

Yechimlardagi barcha TS kod arx-probe muhitida tsc --strict va tsx orqali ishga tushirib tekshirilgan.

Oson

  1. Harflarni esla. SOLID β€” bu qaysi beshta printsip? Har biriga bir jumlali ta'rif yozing (qaramasdan).
  2. Tushunmovchilikni tuzat. Hamkasbingiz "SRP degani har sinfda faqat bitta metod bo'lishi" deydi. U nimada yanglishyapti? To'g'ri ta'rifni ayting.
  3. Printsipni top. Quyidagi kod qaysi SOLID printsipini buzadi?
    class Hisobotchi {
      hisobla() { /* ... */ }
      pdfGa() { /* ... */ }
      emailYubor() { /* ... */ }
    }
    
  4. DIP yoki DI? "Bog'liqlikni konstruktor orqali berish" β€” bu DIP printsipimi yoki DI texnikasimi? Farqini ayting.

O'rta

  1. Switch'ni ayblang. Quyidagi kod qaysi printsipni buzadi va nega?
    function narx(turi: string, asos: number): number {
      if (turi === "oddiy") return asos;
      if (turi === "vip") return asos * 0.9;
      if (turi === "bayram") return asos * 0.8;
      throw new Error("Noma'lum tur");
    }
    
  2. OCP'ga refaktor qiling (KOD). 5-mashqdagi narx funksiyasini OCP'ga moslang: har chegirma turini alohida sinf qiling, shunda yangi tur qo'shganda mavjud kod o'zgarmasin. Kodni yozing.
  3. ISP buzilishini top. Bir interfeysda chop(), skanerla(), faksYubor() metodlari bor. Oddiy printer faqat chop()ni qila oladi. Qaysi printsip buziladi, qanday tuzatasiz?
  4. DIP bilan ulang (KOD). RoyxatdanOtkazish sinfi foydalanuvchini saqlashi kerak, lekin u qayerda saqlanishini (xotira, baza) bilmasligi kerak. FoydalanuvchiRepozitoriysi interfeysi va bitta xotira-implementatsiyasini yozing, ularni DI bilan ulang.

Qiyin

  1. Kvadrat/To'rtburchak (KOD). Kvadrat extends Tortburchak merosi LSP'ni nega buzadi β€” kod bilan ko'rsating. So'ng LSP'ni buzmaydigan muqobil dizaynni (meros o'rniga) yozing.
  2. Telegram-bot to'lov tizimi (dizayn). Botingizda Click, Payme, Uzum to'lov provayderlari bor; ertaga yana ikkitasi qo'shilishi mumkin. Qaysi SOLID printsiplari bu yerda eng muhim? Komponentlarni va abstraksiyalarni qisqacha loyihalang (kod yoki diagramma-matn).
  3. Anti-misolni tuzating. Quyidagi sinf bir vaqtning o'zida bir necha SOLID printsipini buzadi. Qaysilarini, va qanday refaktor qilasiz?
    class UserManager {
      saveToMySQL(u: User) { /* SQL */ }
      sendWelcomeEmail(u: User) { /* SMTP */ }
      validatePassword(p: string) { /* regex */ }
      renderProfileHTML(u: User) { /* HTML */ }
    }
    
  4. Qachon SOLIDni QO'LLAMASLIK kerak? Bir martalik 30 qatorlik skript yozyapsiz (CSV o'qib, jami chiqaradi, qayta ishlatilmaydi). SOLID'ni to'liq qo'llash kerakmi? Javobingizni "trade-off" tilida asoslang.
  5. DIP -> arxitektura (konseptual). DIP'dagi "bog'liqlik o'qini teskari aylantirish" g'oyasi hexagonal/Clean arxitekturada qanday kattalashtiriladi? Bog'liqlik o'qi qaysi tomonga qaragan bo'lishi kerak va nega?

Yechimlar

1-mashq yechimi

  • S β€” SRP: sinf bitta o'zgarish sababiga (bitta aktyorga) ega bo'lsin.
  • O β€” OCP: kengaytirishga ochiq, o'zgartirishga yopiq (yangi kod qo'sh, eskini tahrirlama).
  • L β€” LSP: pastki tip ota tipni dasturni buzmasdan almashtira olsin.
  • I β€” ISP: mijoz ishlatmaydigan metodlarga bog'lanmasin; kichik maxsus interfeyslar.
  • D β€” DIP: abstraksiyaga bog'lan, konkretga emas; ikkala daraja ham abstraksiyaga.

2-mashq yechimi

Hamkasb SRP'ni metod soni bilan chalkashtiryapti. SRP metodlar soni haqida emas β€” bitta sinfda o'nlab metod bo'lishi mumkin, agar ularning hammasi bitta mas'uliyatga (bitta o'zgarish sababiga, bitta aktyorga) xizmat qilsa. To'g'ri ta'rif: "sinf o'zgarishi uchun faqat bitta sabab bo'lsin". Ko'rsatkich β€” "bu sinfni o'zgartirishni kim so'raydi?" Agar bir nechta turli manfaatdor tomon bo'lsa, SRP buzilgan.

3-mashq yechimi

SRP buziladi. Hisobotchi uchta turli sababga o'zgaradi: hisobla() (biznes mantig'i), pdfGa() (taqdimot/format), emailYubor() (xabar yuborish infratuzilmasi). Uch aktyor, uch sabab. Yechim: HisobotGenerator, PdfFormatlovchi, XabarYuboruvchi ga ajratish.

4-mashq yechimi

Bu DI (Dependency Injection) texnikasi β€” bog'liqlikni tashqaridan, konstruktor orqali uzatish. DIP esa printsip: "abstraksiyaga bog'lan". DI β€” DIP'ga erishish usullaridan biri, lekin agar konstruktorga konkret klass uzatsangiz (new BuyurtmaXizmati(new EmailXabarchi()) da tip EmailXabarchi bo'lsa), DI bor, DIP yo'q. DIP uchun uzatilayotgan narsa interfeys/abstraksiya bo'lishi shart.

5-mashq yechimi

OCP buziladi. Har yangi chegirma turi (student, qishki ...) qo'shilganda shu funksiyani ochib, tahrirlash kerak β€” "o'zgartirishga yopiq" emas. Qo'shimcha: turlar ko'paygani sayin if zanjiri o'sib, test qilish va o'qish qiyinlashadi.

6-mashq yechimi (ishga tushirib tekshirilgan)

interface ChegirmaQoidasi {
  qollanadimi(summa: number): boolean;
  hisobla(summa: number): number;
}
class OddiyMijoz implements ChegirmaQoidasi {
  qollanadimi() { return true; }
  hisobla(summa: number) { return summa; }
}
class VipMijoz implements ChegirmaQoidasi {
  qollanadimi(summa: number) { return summa > 0; }
  hisobla(summa: number) { return summa * 0.9; }
}
class BayramAksiya implements ChegirmaQoidasi {
  qollanadimi(summa: number) { return summa >= 100000; }
  hisobla(summa: number) { return summa * 0.8; }
}
class Kassa {
  // Yangi qoida qo'shilsa Kassa O'ZGARMAYDI (OCP)
  yakuniyNarx(summa: number, qoida: ChegirmaQoidasi): number {
    return qoida.qollanadimi(summa) ? qoida.hisobla(summa) : summa;
  }
}
// yakuniyNarx(100000, new VipMijoz())    -> 90000
// yakuniyNarx(100000, new BayramAksiya()) -> 80000
Endi yangi QishkiAksiya qo'shish β€” yangi sinf, Kassaga tegmaysiz. Bu Strategy patterni (09-bob).

7-mashq yechimi

ISP buziladi (qo'shimcha β€” LSP ham, agar OddiyPrinter skanerla()/faksYubor()ni throw qilsa). Oddiy printer skanerla() va faksYubor()ga bog'lanishga majbur, garchi ularni qila olmasa. Yechim β€” semiz interfeysni ajratish:

interface Chopuvchi { chop(): void; }
interface Skanerlovchi { skanerla(): void; }
interface Fakslovchi { faksYubor(): void; }
class OddiyPrinter implements Chopuvchi { chop() {} }
class CtotamasaMFP implements Chopuvchi, Skanerlovchi, Fakslovchi {
  chop() {} skanerla() {} faksYubor() {}
}

8-mashq yechimi (ishga tushirib tekshirilgan)

interface Foydalanuvchi { id: number; ism: string; }

interface FoydalanuvchiRepozitoriysi {
  saqla(f: Foydalanuvchi): void;
  topId(id: number): Foydalanuvchi | undefined;
}
// Konkret adapter (DIP'da "detal abstraksiyaga bo'ysunadi")
class XotiraRepozitoriysi implements FoydalanuvchiRepozitoriysi {
  private jadval = new Map<number, Foydalanuvchi>();
  saqla(f: Foydalanuvchi) { this.jadval.set(f.id, f); }
  topId(id: number) { return this.jadval.get(id); }
}
// Yuqori daraja β€” faqat ABSTRAKSIYAGA bog'langan
class RoyxatdanOtkazish {
  constructor(private repo: FoydalanuvchiRepozitoriysi) {}  // DI
  otkaz(ism: string): Foydalanuvchi {
    const f = { id: Math.floor(Math.random() * 1e6), ism };
    this.repo.saqla(f);
    return f;
  }
}
const xizmat = new RoyxatdanOtkazish(new XotiraRepozitoriysi());
Ertaga PostgresRepozitoriysi yozsangiz β€” RoyxatdanOtkazishga tegmaysiz, faqat konstruktorga boshqa adapter berasiz. Bu DIP + Repository patterni (14-bobdagi DDD'da kengayadi).

9-mashq yechimi (ishga tushirib tekshirilgan)

Nega buziladi: Kvadrat extends Tortburchak da kvadrat enini o'zgartirsangiz, bo'yini ham o'zgartirishga majbur (kvadrat tomonlari teng). Funksiya Tortburchak qabul qilib, eni=5, boyi=4 -> yuza=20 deb kutsa, kvadrat bu shartnomani buzadi (16 yoki 25 qaytaradi). Pastki tip ota tip xulqini buzdi -> LSP buzildi.

LSP'ni buzmaydigan muqobil β€” meros emas, umumiy interfeys + o'zgarmas (immutable) shakllar:

interface Shakl { yuza(): number; }
class Tortburchak implements Shakl {
  constructor(private eni: number, private boyi: number) {}
  yuza() { return this.eni * this.boyi; }
}
class Kvadrat implements Shakl {
  constructor(private tomon: number) {}
  yuza() { return this.tomon * this.tomon; }
}
function umumiyYuza(shakllar: Shakl[]): number {
  return shakllar.reduce((s, sh) => s + sh.yuza(), 0);
}
// umumiyYuza([new Tortburchak(2,3), new Kvadrat(4)]) -> 6 + 16 = 22
Endi "enini o'zgartir" kabi xavfli mutatsiya yo'q; ikkalasi ham faqat yuza() shartnomasini bajaradi va xavfsiz almashtiriladi.

10-mashq yechimi (namunaviy + muqobillar)

Bu yerda DIP, OCP, ISP eng muhim. Namunaviy dizayn:

interface TolovProvayderi {
  tola(summa: number, mijoz: string): TolovNatija
}
ClickProvayderi   implements TolovProvayderi
PaymeProvayderi   implements TolovProvayderi
UzumProvayderi    implements TolovProvayderi

TolovXizmati  -> TolovProvayderi (abstraksiyaga bog'langan, DI bilan)
- OCP/Strategy: yangi provayder = yangi sinf, TolovXizmatiga tegmaysiz. - DIP: bot biznes mantig'i konkret Click SDK'siga emas, TolovProvayderiga bog'langan -> test uchun SoxtaProvayder ulanadi (haqiqiy pul harakatisiz). - ISP: agar ba'zi provayderlar "qaytarib berish" (refund) qila olsa, ba'zilari yo'q β€” Qaytaruvchi interfeysini alohida ajrating, hammasini bitta semiz interfeysga tiqmang.

Muqobil/trade-off: agar provayderlar atigi bitta bo'lsa va o'zgarmasligi aniq bo'lsa β€” bu abstraksiya ortiqcha (YAGNI). Lekin "ertaga yana ikkitasi qo'shiladi" deyilgani uchun bu yerda abstraksiya oqlanadi. Real bot misolini tgbot-js kitobida ko'rishingiz mumkin.

11-mashq yechimi

UserManager kamida SRP va DIP ni buzadi (potensial OCP ham): - SRP: to'rt sabab bir sinfda β€” saqlash (DBA), email (infra), validatsiya (biznes), HTML (dizayn). - DIP: saveToMySQL konkret MySQL'ga, sendWelcomeEmail konkret SMTP'ga qattiq bog'langan β€” abstraksiya yo'q, test qilib bo'lmaydi.

Refaktor:

UserRepository (interface)     <- MySqlUserRepository implements
EmailSender    (interface)     <- SmtpEmailSender implements
PasswordValidator              (alohida sinf, biznes qoidasi)
UserView                       (HTML render, alohida)
RegisterUser (use case)  -> UserRepository, EmailSender, PasswordValidator (DI)
Har biri bitta mas'uliyat (SRP), biznes konkret texnologiyani bilmaydi (DIP). Bu β€” 13-bobdagi Clean Architecture "use case" tuzilishiga olib boradigan yo'l.

12-mashq yechimi

Yo'q, to'liq qo'llash shart emas β€” bu trade-off. SOLID'ning narxi β€” qo'shimcha abstraksiya, ko'proq fayl, ko'proq murakkablik. Uning foydasi β€” kelajakdagi o'zgarishni arzonlashtirish. Bir martalik, qayta ishlatilmaydigan 30 qatorlik skriptda kelajakdagi o'zgarish yo'q, demak foyda nolga teng, narx esa qoladi. Bu holda SOLID'ni to'liq qo'llash β€” over-engineering, YAGNI va KISS buzilishi (06-bob). To'g'ri qaror: sodda, to'g'ridan-to'g'ri kod yozing. SOLID β€” o'zgarish ehtimoli yuqori, uzoq yashaydigan kod uchun vosita.

13-mashq yechimi

DIP'dagi "o'qni teskari aylantirish" tizim darajasida dependency rulega kattalashadi: bog'liqlik o'qi doim ichkariga, biznes mantig'iga qaragan bo'lishi kerak. Hexagonal (12-bob)'da biznes "olti burchak" markazda; tashqi dunyo (UI, baza, API) β€” adapterlar, ular portlar (interfeyslar) orqali ichkariga ulanadi. Clean Architecture (13-bob)'da entity va use case markazda; framework, DB, web β€” tashqi halqada va markazga ishora qiladi. Nega? Chunki biznes mantig'i β€” eng barqaror, eng qimmatli qism; u baza yoki framework o'zgarsa o'zgarmasligi kerak. Demak bog'liqlik undan tashqariga emas, unga tomon yo'naltiriladi β€” bu aynan DIP, faqat butun tizim miqyosida.


⬅️ Oldingi: 04 β€” Coupling va cohesion Β· 🏠 README Β· Keyingi: 06 β€” Boshqa printsiplar: DRY, KISS, YAGNI ➑️