Bu mavzu ko'p joyda yoritilgan maqolalarimiz ketma-ketligida boshida bu mavzuni qoldirib ketmoqchi ham bo'ldim.
Lekin fikrimdan qaytdim. Agar bu mavzuni yaxshi tushunaman desangiz qoldirib boshqa maqolarimni o'qishi mumkin.
Loyihalashning yana beshta tamoyilini ko‘rib chiqamiz, ular SOLID nomi bilan tanilgan. Bu tamoyillar birinchi marta Robert Martin tomonidan "Agile Software Development" kitobida bayon etilgan.
S
Single Responsibility Principle (SRP)
Yagona mas’uliyat tamoyili
Sinf yoki Class da o‘zgartirishga faqat bitta sabab bo‘lishi kerak, ya’ni u faqat bitta mas’uliyatni bajarishi kerak.
Misol:
User classi faqat foydalanuvchi ma’lumotlari bilan ishlasa yaxshi.
Agar u bir vaqtning o‘zida foydalanuvchini bazaga saqlasa, email yuborsa, hisobot chiqarsa — unda classni o‘zgartirish uchun bir nechta sabab paydo bo‘ladi:
Email formati o‘zgarsa — User o‘zgaradi.
Database o‘zgarsa — User o‘zgaradi.
Hisobot formati o‘zgarsa — User o‘zgaradi.
SRP bo‘yicha bular alohida classlarga ajratiladi:
User — foydalanuvchi modeliUserRepository — bazaga saqlashEmailService — email yuborishUserReportService — hisobot tayyorlash
Har bir class dasturning faqat bitta funksional qismi uchun javob bersin. Shu funksionallik to‘liq shu class ichida jamlangan, ya’ni tashqi tomondan yashirilgan bo‘lishi kerak.
Yagona javobgarlik prinsipi koddagi murakkablikni kamaytirish uchun kerak. Agar ilovangiz atigi 200 qatordan iborat bo‘lsa, kuchli arxitektura yoki murakkab dizayn shart emas. 5–7 ta metodni tartibli yozsangiz ham yetarli bo‘ladi.
Muammolar esa tizim kattalashganda paydo bo‘ladi. class haddan tashqari kattalashib ketsa, uni tushunish qiyinlashadi. Kod ichida harakatlanish murakkablashadi, boshqa masalaga tegishli keraksiz detallar ko‘zga tashlanadi. Natijada tushunilishi kerak bo‘lgan narsalar soni ortib ketadi va siz kod ustidan nazoratni yo‘qota boshlaysiz.
Agar class bir vaqtning o‘zida juda ko‘p ishni bajarsa, shu ishlardan bittasi o‘zgarganda ham classni o‘zgartirishga majbur bo‘lasiz. Bunda siz umuman tegmoqchi bo‘lmagan boshqa qismlarni buzib qo‘yish xavfi paydo bo‘ladi.
O
Open/Closed Principle (OCP)
Ochiqlik va Yopiqlik tamoyili
Classlarni kengaytirish mumkin bo‘lsin, lekin ularning avval yozilgan kodini o‘zgartirib bo'lmasin.
Bu tamoyilning asosiy g‘oyasi shuki: dasturga yangi imkoniyat qo‘shayotganda mavjud, ishlayotgan kodni buzib qo‘ymaslik kerak.
class kengaytirish uchun ochiq bo‘lishi kerak. Ya’ni kerak bo‘lsa, uning funksionalligini yangi subclass, interface yoki boshqa mexanizm orqali kengaytirish mumkin bo‘lsin.
Shu bilan birga, class o‘zgartirish uchun yopiq bo‘lishi kerak. Ya’ni class allaqachon yozilgan, tekshirilgan, test qilingan va loyihada ishlatilayotgan bo‘lsa, uning ichki kodini qayta-qayta o‘zgartirish yaxshi emas.
Buning o‘rniga, mavjud classning kodiga tegmasdan, uning asosiy xatti-harakatini boshqa class orqali kengaytirish kerak.
Oddiy qilib aytganda:
Yangi talab chiqsa, eski kodni buzib o‘zgartirmang; yangi kod qo‘shib kengaytiring.
Masalan, to‘lov tizimi bor:
PaymentService
Agar keyin karta, Payme, Click, Uzum kabi yangi to‘lov turlari qo‘shilsa, har safar PaymentService ichidagi kodni o‘zgartirish o‘rniga, har bir to‘lov turi uchun alohida class yaratish yaxshiroq:
CardPaymentPaymePaymentClickPaymentUzumPayment
Shunda yangi to‘lov turi qo‘shilganda eski, ishlayotgan kodga kamroq tegiladi va uni buzib qo‘yish xavfi kamayadi.
L
Liskov Substitution Principle (LSP)
Liskovning o‘rnini almashtirish tamoyili
SInf(Class) osti yoki shu sinfdan meros olgan sinflar bazaviy sinfning xatti-harakatini buzmasligi, aksincha uni to‘ldirishi kerak. Ya’ni, agar dasturda bazaviy class obyektlari ishlatilayotgan bo‘lsa, ularning o‘rniga subclass obyektlarini qo‘yganda ham dastur buzilmasligi kerak.
Oddiy qilib aytganda:
Farzand class ota class o‘rnida ishlay olishi kerak. Agar ota class ishlagan joyda farzand class ishlamasa, bu LSP buzilgan bo‘ladi.
Bu prinsip ayniqsa kutubxona va frameworklar yaratishda muhim. Chunki siz yozgan classlardan boshqa dasturchilar foydalanadi. Ularning kodini siz nazorat qila olmaysiz. Shuning uchun subclasslar mavjud kodni buzmaydigan qilib yaratilishi kerak.
LSP boshqa SOLID prinsiplariga qaraganda aniqroq qoidalarga ega. U asosan subclassda qayta yozilgan metodlarga tegishli.
1. Metod parametrlarini toraytirib yubormaslik kerak
Subclass metodining parametrlari bazaviy metod parametrlariga mos bo‘lishi yoki undan ham umumiyroq bo‘lishi kerak.
Masalan, bazaviy classda shunday metod bor:
feed(Cat c)Bu metod uy mushuklarini ovqatlantiradi. Client kod buni biladi va har doim unga Cat beradi.
Yaxshi holat:
feed(Animal c)Subclass metodni shunday o‘zgartirdiki, u har qanday hayvonni ovqatlantira oladi. Bu muammo emas. Chunki client kod baribir mushuk beradi, metod esa barcha hayvonlarni ovqatlantira oladi. Demak, mushukni ham ovqatlantiradi.
Yomon holat:
feed(BengalCat c)Bu metod faqat bengal mushugini ovqatlantira oladi. Client kod esa oddiy Cat yuboradi. Natijada subclass bazaviy class o‘rnida ishlay olmaydi va kod buziladi.
2. Qaytariladigan qiymat turi mos bo‘lishi kerak
Subclass metodining qaytaradigan qiymati bazaviy metod qaytaradigan tur bilan bir xil yoki undan aniqroq tur bo‘lishi kerak.
Masalan, bazaviy metod:
buyCat(): CatClient kod bu metoddan oddiy uy mushugini kutadi.
Yaxshi holat:
buyCat(): BengalCatBengal mushugi ham Cat hisoblanadi. Shuning uchun client kod uchun muammo bo‘lmaydi.
Yomon holat:
buyCat(): AnimalBu yerda metod istalgan hayvonni qaytarishi mumkin. Masalan, timsoh qaytishi ham ehtimol. Client kod esa mushuk kutayotgan edi. Natijada dastur buzilishi mumkin.
Dinamik tipli tillarda ham shunday muammo bo‘ladi. Masalan, bazaviy metod String qaytaradi, subclassdagi metod esa Number qaytaradi. Bu ham LSPga zid.
3. Metod old shartlarni kuchaytirmasligi kerak
Subclass metod bazaviy metodga qaraganda qattiqroq talab qo‘ymasligi kerak.
Masalan, bazaviy metod int qiymat bilan ishlaydi. U musbat sonni ham, manfiy sonni ham qabul qiladi.
Agar subclass shu metodni qayta yozib, “qiymat faqat 0 dan katta bo‘lishi kerak” desa, bu old shartni kuchaytirish bo‘ladi.
Chunki oldin client kod manfiy son yuborsa ham ishlagan. Endi subclass ishlatilganda kod buziladi.
4. Metod keyingi shartlarni zaiflashtirmasligi kerak
Bazaviy metod ish tugagach biror kafolat bersa, subclass ham shu kafolatni saqlashi kerak.
Masalan, bazaviy metod tugagandan keyin barcha database ulanishlarini yopadi.
Agar subclass metod ulanishlarni ochiq qoldirsa, “keyin yana ishlataman” desa, bu noto‘g‘ri. Chunki client kod bazaviy class qoidalariga ishonadi. U metoddan keyin ulanishlar yopilgan deb o‘ylaydi.
Natijada tizimda ortiqcha ochiq jarayonlar qolishi yoki dastur noto‘g‘ri ishlashi mumkin.
5. class invariantlari o‘zgarmasligi kerak
Invariant — obyekt mantiqan to‘g‘ri bo‘lishi uchun doim saqlanishi kerak bo‘lgan shartlar.
Masalan, mushuk uchun invariantlar:
to‘rt oyog‘i bor;
dumi bor;
miyovlay oladi;
mushuk sifatida o‘zini tutadi.
Dasturlashda invariantlar metodlardagi tekshiruvlar, unit testlar yoki client kod kutayotgan qoidalar orqali bilinishi mumkin.
Subclass bu qoidalarni buzmasligi kerak. Aks holda u bazaviy class o‘rnida ishlay olmaydi.
Eng xavfsiz subclass — bazaviy classning ichki holatini buzmaydigan, faqat yangi metod yoki yangi maydonlar qo‘shadigan subclassdir.
6. Subclass kutilmagan exception tashlamasligi kerak
Subclassdagi metod bazaviy metod tashlamaydigan g‘alati exceptionlarni tashlamasligi kerak.
Client kod odatda bazaviy metod tashlashi mumkin bo‘lgan exceptionlarga tayyor bo‘ladi:
try {
service.doSomething();
} catch (KnownException e) {
// exceptionni ushlash
}Agar subclass boshqa, kutilmagan exception tashlasa, client kod uni ushlay olmasligi mumkin. Natijada dastur ishdan chiqadi.
Shuning uchun subclassdagi exceptionlar bazaviy metod exceptionlariga mos yoki ulardan aniqroq bo‘lishi kerak.
Ko‘p zamonaviy qat’iy tipli tillarda, masalan Java va C# da, bunday cheklovlarning bir qismi kompilyator tomonidan tekshiriladi. Shuning uchun ayrim LSP buzilishlariga kompilyatorning o‘zi yo‘l qo‘ymaydi.
7. Subclass bazaviy classning private maydonlarini o‘zgartirmasligi kerak
Subclass bazaviy classning private fieldlarini buzmasligi kerak.
Bu qoida g‘alati eshitilishi mumkin, chunki ko‘p tillarda private maydonlarga to‘g‘ridan-to‘g‘ri kirib bo‘lmaydi. Lekin ayrim tillarda reflection orqali bunga erishish mumkin. Python yoki JavaScript kabi tillarda esa private himoya har doim ham qattiq emas.
Shuning uchun subclass bazaviy classning ichki holatini chetlab o‘tib o‘zgartirmasligi kerak.
Qisqa xulosa
Liskov prinsipi shuni aytadi: subclass bazaviy class o‘rnida ishlatilganda, client kod buni sezmasligi kerak.
Ya’ni:
Ota class ishlagan joyda farzand class ham muammosiz ishlashi kerak.
Agar subclass ota classdan meros olsa-yu, lekin uning o‘rnida ishlay olmasa, bu noto‘g‘ri merosxo‘rlik hisoblanadi.
I
Interface Segregation Principle (ISP)
Interfeysni ajratish tamoyili
Mijozlar o‘zlari foydalanmaydigan usullar yoki metodlarga qaram bo‘lmasligi kerak.
Ya’ni interfeyslar juda katta va umumiy bo‘lib ketmasligi kerak. Har bir interfeys aniq, kichik va kerakli vazifani bajaradigan bo‘lishi lozim. Shunda classlar o‘ziga kerak bo‘lmagan metodlarni majburan implementatsiya qilmaydi.
Oddiy qilib aytganda:
Katta va “semiz” interfeyslarni kichik, aniq vazifali interfeyslarga bo‘lish kerak.
Bu prinsipga ko‘ra, mijoz faqat o‘ziga kerak bo‘lgan metodlarni bilishi kerak. Agar interfeysdagi biror metod o‘zgarsa, bu metoddan foydalanmaydigan mijozlar ta’sirlanmasligi lozim.
Ko‘p obyektga yo‘naltirilgan dasturlash tillarida class faqat bitta parent kcassdan meros olishi mumkin, lekin bir nechta interfeysni implementatsiya qila oladi. Shuning uchun hamma xatti-harakatlarni bitta katta interfeysga tiqishtirish shart emas. Buning o‘rniga, classga bir nechta kichik interfeyslarni biriktirish mumkin.
Misol
Tasavvur qiling, cloud provayderlar bilan ishlaydigan kutubxona bor.
Birinchi versiyada faqat Amazon qo‘llab-quvvatlangan. Amazon esa cloud xizmatlarning deyarli to‘liq to‘plamiga ega. Shuning uchun dastlabki interfeys ham juda katta qilib loyihalangan.
Masalan, interfeysda quyidagi metodlar bo‘lishi mumkin:
interface CloudProvider {
void storeFile();
void getFile();
void createServer();
void listServers();
void createDatabase();
void getDatabase();
}Amazon bu metodlarning hammasini bajara oladi.
Lekin keyin boshqa providerlar qo‘shildi. Ularning ba’zilari faqat fayl saqlash xizmatini beradi, ba’zilari esa faqat server bilan ishlaydi. Ular katta CloudProvider interfeysining hamma metodlarini bajara olmaydi.
Yomon holat:
class DropboxProvider implements CloudProvider {
public void storeFile() {
// faylni saqlaydi
}
public void getFile() {
// faylni oladi
}
public void createServer() {
// Dropbox server yaratmaydi
throw new UnsupportedOperationException();
}
public void listServers() {
// Dropbox server ro'yxatini bermaydi
throw new UnsupportedOperationException();
}
public void createDatabase() {
// Dropbox database yaratmaydi
throw new UnsupportedOperationException();
}
public void getDatabase() {
// Dropbox database bermaydi
throw new UnsupportedOperationException();
}
}Bu yerda class o‘ziga kerak bo‘lmagan metodlarni majburan yozmoqda. Bu ISP prinsipiga zid.
Yaxshi holat — interfeyslarni bo‘lish:
interface FileStorageProvider {
void storeFile();
void getFile();
}
interface ServerProvider {
void createServer();
void listServers();
}
interface DatabaseProvider {
void createDatabase();
void getDatabase();
}Endi har bir provider faqat o‘zi bajara oladigan interfeysni implementatsiya qiladi:
class DropboxProvider implements FileStorageProvider {
public void storeFile() {
// faylni saqlaydi
}
public void getFile() {
// faylni oladi
}
}Amazon esa barcha xizmatlarni qo‘llab-quvvatlasa, bir nechta interfeysni birdan implementatsiya qilishi mumkin:
class AmazonProvider implements FileStorageProvider, ServerProvider, DatabaseProvider {
public void storeFile() {}
public void getFile() {}
public void createServer() {}
public void listServers() {}
public void createDatabase() {}
public void getDatabase() {}
}Qisqa xulosa
Interfeys faqat kerakli metodlardan iborat bo‘lishi kerak.
Agar class interfeysdagi metodlarning bir qismini ishlatmasa yoki bo‘sh qoldirishga majbur bo‘lsa, demak interfeys juda katta. Uni kichikroq, aniqroq interfeyslarga ajratish kerak.
D
Dependency Inversion Principle (DIP)
Bog‘liqliklarni inversiya qilish tamoyili
Yuqori darajadagi classlar past darajadagi classlarga bevosita bog‘lanib qolmasligi kerak. Ikkalasi ham abstraksiyaga, ya’ni interface yoki abstract classga bog‘lanishi kerak.
Abstraksiya detallarga bog‘liq bo‘lmasligi kerak. Aksincha, detallar abstraksiyaga bog‘liq bo‘lishi kerak.
Oddiy qilib aytganda:
Biznes logika konkret classga emas, umumiy interfeysga bog‘lanishi kerak.
Odatda dasturda classlarni ikki darajaga ajratish mumkin:
Past darajadagi classlar — texnik ishlarni bajaradi. Masalan:
fayl bilan ishlash;
internet orqali ma’lumot yuborish;
databasega ulanish;
API bilan ishlash.
Yuqori darajadagi classlar — dasturning asosiy biznes logikasini bajaradi. Masalan:
hisobot yaratish;
buyurtmani hisoblash;
to‘lov jarayonini boshqarish;
foydalanuvchi ro‘yxatdan o‘tishini boshqarish.
Ko‘pincha avval past darajadagi classlar yoziladi, keyin esa biznes logika ularga bog‘lab qo‘yiladi. Bunday holatda yuqori darajadagi classlar past darajadagi classlarga qattiq bog‘lanib qoladi.
Masalan, ReportService bevosita MySQLDatabase classini ishlatsa, MySQLDatabasedagi o‘zgarish ReportServicega ham ta’sir qiladi.
Bu esa kodni o‘zgartirish, test qilish va kengaytirishni qiyinlashtiradi.
DIP nima taklif qiladi?
DIP bog‘liqlik yo‘nalishini o‘zgartirishni taklif qiladi.
Avval biznes logikaga kerak bo‘lgan operatsiyalar uchun interface yaratiladi. Keyin yuqori darajadagi class konkret classga emas, shu interfacega bog‘lanadi.
Past darajadagi class esa shu interfaceni implementatsiya qiladi.
Ya’ni:
ReportService → MySQLDatabase
o‘rniga:
ReportService → DataStorage interface ← MySQLDatabase
bo‘ladi.
Misol
Yomon holat:
class BudgetReport {
private MySQLDatabase database;
public BudgetReport() {
this.database = new MySQLDatabase();
}
public void generate() {
String data = database.loadData();
// hisobot tayyorlash
database.saveData(data);
}
}
class MySQLDatabase {
public String loadData() {
return "data from mysql";
}
public void saveData(String data) {
// mysqlga saqlash
}
}Bu yerda BudgetReport bevosita MySQLDatabasega bog‘langan.
Agar ertaga PostgreSQL, MongoDB yoki faylga saqlash kerak bo‘lsa, BudgetReport kodini o‘zgartirishga to‘g‘ri keladi.
Bu DIP prinsipiga zid.
Yaxshi holat:
interface DataStorage {
String loadData();
void saveData(String data);
}Endi yuqori darajadagi class konkret database bilan emas, interface bilan ishlaydi:
class BudgetReport {
private DataStorage storage;
public BudgetReport(DataStorage storage) {
this.storage = storage;
}
public void generate() {
String data = storage.loadData();
// hisobot tayyorlash
storage.saveData(data);
}
}Past darajadagi class esa shu interfaceni implementatsiya qiladi:
class MySQLDatabase implements DataStorage {
public String loadData() {
return "data from mysql";
}
public void saveData(String data) {
// mysqlga saqlash
}
}Keyinchalik boshqa saqlash usulini qo‘shish oson:
class FileStorage implements DataStorage {
public String loadData() {
return "data from file";
}
public void saveData(String data) {
// faylga saqlash
}
}Endi BudgetReport kodiga tegmasdan, unga turli storage berish mumkin:
BudgetReport report = new BudgetReport(new MySQLDatabase());yoki:
BudgetReport report = new BudgetReport(new FileStorage());Bu yerda bog‘liqlik qanday o‘zgardi?
Oldin:
Yuqori daraja past darajaga bog‘liq edi.
Ya’ni BudgetReport → MySQLDatabase
Endi:
Past darajadagi class yuqori darajadagi interfacega bog‘liq bo‘ldi.
Ya’ni MySQLDatabase → DataStorage
BudgetReport ham, MySQLDatabase ham DataStorage abstraksiyasi orqali bog‘lanadi.
Shu sababli bu prinsip “dependency inversion”, ya’ni bog‘liqlik yo‘nalishini teskari qilish deb ataladi.
OCP bilan bog‘liqligi
Bu prinsip ko‘pincha Open/Closed Principle bilan birga ishlaydi.
Chunki siz yangi past darajadagi class qo‘shishingiz mumkin:
MySQLDatabasePostgresDatabaseMongoDatabaseFileStorageApiStorage
Lekin BudgetReport kodini o‘zgartirmaysiz.
Ya’ni kod kengaytirish uchun ochiq, ammo o‘zgartirish uchun yopiq bo‘ladi.
Qisqa xulosa
Yuqori darajadagi biznes logika konkret texnik classlarga bog‘lanmasin. U interface yoki abstract classga bog‘lansin.
Eng sodda formula:
Konkret classga emas, abstraksiyaga bog‘lan.
Yoki yana tabiiyroq qilib:
Biznes logika “ma’lumot qayerdan kelishini” bilmasligi kerak. U faqat kerakli metodlar borligini bilsa yetarli.