SOLID tamoyillari birinchi marta mashhur kompyuter olimi Robert J. Martin tomonidan 2000-yilda o'z maqolasida taqdim etilgan. Lekin SOLID qisqartmasi keyinchalik Maykl Feathers tomonidan kiritilgan.
Ularning barchasi bir xil maqsadga xizmat qiladi:
"Ko'pgina ishlab chiquvchilar birgalikda ishlashi mumkin bo'lgan tushunarli, o'qilishi mumkin va sinovdan o'tkaziladigan kodni yaratish."
Keling, har bir tamoyilni birma-bir ko'rib chiqaylik.
SOLID :
Yagona javobgarlik printsipi
Open-Cloused printsipi
Liskov almashtirish printsipi
Interfeys segregatsiya printsipi
Bog'liqlik inversiya printsipi
Yagona javobgarlik printsipi
Yagona javobgarlik printsipi , sinf bir narsani qilishi kerakligini va shuning uchun uni o'zgartirish uchun faqat bitta sababga ega bo'lishi kerakligini ta'kidlaydi .
Ushbu printsipni texnik jihatdan ko'proq ta'riflash uchun: Dasturiy ta'minot spetsifikatsiyasidagi faqat bitta potentsial o'zgarish (ma'lumotlar bazasi mantig'i, logging mantig'i va boshqalar) sinf spetsifikatsiyasiga ta'sir qilishi kerak.
Bu shuni anglatadiki, agar sinf Kitob sinfi yoki Student sinfi kabi ma'lumotlar konteyneri bo'lsa va unda ushbu ob'ektga tegishli ba'zi maydonlar bo'lsa, u faqat ma'lumotlar modelini o'zgartirganimizda o'zgarishi kerak.
Yagona javobgarlik tamoyiliga amal qilish muhim. Birinchidan, ko'plab turli jamoalar bir xil loyiha ustida ishlashi va turli sabablarga ko'ra bir sinfni tahrirlashi mumkinligi sababli, bu mos kelmaydigan modullarga olib kelishi mumkin.
Ikkinchidan, bu versiyani boshqarishni osonlashtiradi. Masalan, bizda ma'lumotlar bazasi operatsiyalari bilan shug'ullanadigan qat'iylik klassi bor va biz GitHub majburiyatlarida ushbu fayldagi o'zgarishlarni ko'ramiz. SRP ga rioya qilgan holda, biz uni saqlash yoki ma'lumotlar bazasi bilan bog'liq narsalar bilan bog'liqligini bilib olamiz.
Birlashtirish mojarolari yana bir misol. Ular turli jamoalar bitta faylni o'zgartirganda paydo bo'ladi. Ammo agar SRPga rioya qilinsa, kamroq nizolar paydo bo'ladi - fayllarni o'zgartirish uchun yagona sabab bo'ladi va mavjud bo'lgan nizolarni hal qilish osonroq bo'ladi.
Umumiy tuzoqlar va anti-naqshlar
Ushbu bo'limda biz Yagona javobgarlik tamoyilini buzadigan ba'zi keng tarqalgan xatolarni ko'rib chiqamiz. Keyin ularni tuzatishning ba'zi usullari haqida gapiramiz.
Biz misol tariqasida oddiy kitob do'koni hisob-faktura dasturining kodini ko'rib chiqamiz. Keling, hisob-fakturada foydalanish uchun kitob sinfini belgilashdan boshlaylik.
class Book {
String name;
String authorName;
int year;
int price;
String isbn;
public Book(String name, String authorName, int year, int price, String isbn) {
this.name = name;
this.authorName = authorName;
this.year = year;
this.price = price;
this.isbn = isbn;
}
}
Bu ba'zi sohalarga ega oddiy kitob sinfidir. Qiziqarli narsa yo'q. Qabul qiluvchilar va sozlashchilar bilan shug'ullanishimiz shart emas va buning o'rniga mantiqqa e'tibor qaratishimiz uchun men maydonlarni shaxsiy qilmayapman.
Keling, hisob-fakturani yaratish va umumiy narxni hisoblash mantiqini o'z ichiga olgan hisob-faktura sinfini yarataylik. Hozircha kitob do‘konimizda faqat kitob sotiladi, boshqa hech narsa yo‘q, deb faraz qilaylik.
public class Invoice {
private Book book;
private int quantity;
private double discountRate;
private double taxRate;
private double total;
public Invoice(Book book, int quantity, double discountRate, double taxRate) {
this.book = book;
this.quantity = quantity;
this.discountRate = discountRate;
this.taxRate = taxRate;
this.total = this.calculateTotal();
}
public double calculateTotal() {
double price = ((book.price - book.price * discountRate) * this.quantity);
double priceWithTaxes = price * (1 + taxRate);
return priceWithTaxes;
}
public void printInvoice() {
System.out.println(quantity + "x " + book.name + " " + book.price + "$");
System.out.println("Discount Rate: " + discountRate);
System.out.println("Tax Rate: " + taxRate);
System.out.println("Total: " + total);
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Mana bizning hisob-faktura sinfimiz. Shuningdek, u hisob-faktura va 3 usul haqida ba'zi maydonlarni o'z ichiga oladi:
umumiy narxni hisoblaydigan umumiy hisoblash usuli,
PrintInvoice usuli, bu hisob-fakturani konsolga chop etishi kerak va
saveToFile usuli, hisob-fakturani faylga yozish uchun javobgardir.
Keyingi paragrafni o'qishdan oldin ushbu sinf dizaynida nima noto'g'ri ekanligi haqida o'ylash uchun o'zingizga bir soniya vaqt berishingiz kerak.
Xo'sh, bu erda nima bo'lyapti? Bizning sinfimiz Yagona javobgarlik tamoyilini bir necha jihatdan buzadi.
Birinchi qoidabuzarlik bizning chop etish mantiqimizni o'z ichiga olgan printInvoice usulidir. SRP bizning sinfimiz o'zgarishi uchun faqat bitta sababga ega bo'lishi kerakligini ta'kidlaydi va bu sabab bizning sinfimiz uchun hisob-fakturani o'zgartirish bo'lishi kerak.
Ammo bu arxitekturada, agar biz bosib chiqarish formatini o'zgartirmoqchi bo'lsak, sinfni o'zgartirishimiz kerak bo'ladi. Shuning uchun biz bir sinfda chop etish mantig'ini biznes mantig'i bilan aralashtirib yubormasligimiz kerak.
Bizning sinfimizda SRPni buzadigan yana bir usul mavjud: saveToFile usuli. Qat'iylik mantig'ini biznes mantig'i bilan aralashtirish ham juda keng tarqalgan xatodir.
Faqat faylga yozish nuqtai nazaridan o'ylamang - bu ma'lumotlar bazasiga saqlash, API qo'ng'iroq qilish yoki qat'iylik bilan bog'liq boshqa narsalar bo'lishi mumkin.
Xo'sh, bu chop etish funksiyasini qanday tuzatishimiz mumkin, deb so'rashingiz mumkin.
Biz chop etish va qatʼiylik mantiqimiz uchun yangi sinflarni yaratishimiz mumkin, shuning uchun endi bu maqsadlar uchun hisob-faktura sinfini oʻzgartirishga hojat qolmaydi.
Biz InvoicePrinter va InvoicePersistence 2 sinfini yaratamiz va usullarni o'zgartiramiz.
public class InvoicePrinter {
private Invoice invoice;
public InvoicePrinter(Invoice invoice) {
this.invoice = invoice;
}
public void print() {
System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
System.out.println("Discount Rate: " + invoice.discountRate);
System.out.println("Tax Rate: " + invoice.taxRate);
System.out.println("Total: " + invoice.total + " $");
}
}
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Endi bizning sinf tuzilmasi Yagona javobgarlik printsipiga bo'ysunadi va har bir sinf bizning dasturimizning bir jihati uchun javobgardir. Ajoyib!
Ochiq-yopiq printsip
Ochiq-yopiq printsip sinflar uzaytirish uchun ochiq va o'zgartirish uchun yopiq bo'lishini talab qiladi.
Modifikatsiya mavjud sinf kodini o'zgartirishni, kengaytma esa yangi funksiyalarni qo'shishni anglatadi.
Shunday qilib, bu tamoyil aytmoqchi bo'lgan narsa: biz sinf uchun mavjud kodga tegmasdan yangi funksiyalarni qo'shishimiz kerak. Buning sababi shundaki, biz mavjud kodni o'zgartirganimizda, biz potentsial xatolarni yaratish xavfini o'z zimmamizga olamiz. Shunday qilib, iloji bo'lsa, sinovdan o'tgan va ishonchli (asosan) ishlab chiqarish kodiga tegmaslik kerak.
Ammo sinfga tegmasdan qanday qilib yangi funksiyalarni qo'shamiz, deb so'rashingiz mumkin. Odatda interfeyslar va abstrakt sinflar yordamida amalga oshiriladi.
Endi biz printsip asoslarini ko'rib chiqdik, keling, uni Invoice ilovamizga qo'llaymiz.
Aytaylik, xo'jayinimiz bizga kelib, hisob-fakturalarni osongina qidirishimiz uchun ma'lumotlar bazasida saqlanishini xohlashlarini aytdi. Biz yaxshi deb o'ylaymiz, bu juda oson xo'jayin, menga bir soniya bering!
Biz ma'lumotlar bazasini yaratamiz, unga ulanamiz va InvoicePersistence sinfimizga saqlash usulini qo'shamiz :
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
public void saveToDatabase() {
// Saves the invoice to database
}
}
Afsuski, biz, kitob do'konining dangasa ishlab chiquvchisi sifatida, kelajakda osonlik bilan kengaytirilishi mumkin bo'lgan sinflarni loyihalashtirmadik. Shunday qilib, ushbu xususiyatni qo'shish uchun biz InvoicePersistence sinfini o'zgartirdik.
Agar bizning sinf dizaynimiz Ochiq-yopiq printsipiga bo'ysunsa, biz bu sinfni o'zgartirishimiz shart emas edi.
Shunday qilib, kitob do'konining dangasa, ammo aqlli ishlab chiquvchisi sifatida biz dizayn muammosini ko'ramiz va printsipga bo'ysunish uchun kodni qayta tiklashga qaror qilamiz.
interface InvoicePersistence {
public void save(Invoice invoice);
}
Biz InvoicePersistence turini Interfeysga o'zgartiramiz va saqlash usulini qo'shamiz. Har bir qat'iylik klassi ushbu saqlash usulini amalga oshiradi.
public class DatabasePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to DB
}
}
public class FilePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to file
}
}
Shunday qilib, bizning sinf tuzilmasi endi shunday ko'rinadi:
Endi bizning qat'iyatlilik mantiqimiz osongina kengaytiriladi. Agar xo'jayinimiz bizdan boshqa ma'lumotlar bazasini qo'shishimizni va MySQL va MongoDB kabi ikki xil ma'lumotlar bazasiga ega bo'lishni so'rasa, biz buni osonlikcha bajara olamiz.
Siz interfeyssiz bir nechta sinflarni yaratishimiz va ularning barchasiga saqlash usulini qo'shishimiz mumkin deb o'ylashingiz mumkin.
Aytaylik, biz ilovamizni kengaytiramiz va InvoicePersistence , BookPersistence kabi bir nechta qat'iylik sinflariga egamiz va biz barcha qat'iylik sinflarini boshqaradigan PersistenceManager sinfini yaratamiz:
public class PersistenceManager {
InvoicePersistence invoicePersistence;
BookPersistence bookPersistence;
public PersistenceManager(InvoicePersistence invoicePersistence,
BookPersistence bookPersistence) {
this.invoicePersistence = invoicePersistence;
this.bookPersistence = bookPersistence;
}
}
Endi biz InvoicePersistence interfeysini amalga oshiradigan har qanday sinfni polimorfizm yordamida ushbu sinfga o'tkazishimiz mumkin. Bu interfeyslar taqdim etadigan moslashuvchanlikdir.
Liskov almashtirish printsipi
Liskov almashtirish printsipi quyi sinflar o'zlarining asosiy sinflari bilan almashtirilishi kerakligini aytadi.
Bu shuni anglatadiki, B sinf A sinfining kichik sinfi ekanligini hisobga olsak, biz B sinf ob'ektini A sinf ob'ektini kutadigan har qanday usulga o'tkazishimiz kerak va bu holda usul hech qanday g'alati natija bermasligi kerak.
Bu kutilgan xatti-harakatdir, chunki biz merosdan foydalanganda biz bolalar sinfi yuqori sinfga ega bo'lgan hamma narsani meros qilib oladi deb taxmin qilamiz. Bolalar sinfi xatti-harakatni kengaytiradi, lekin uni hech qachon toraytirmaydi.
Shuning uchun, sinf ushbu printsipga bo'ysunmasa, bu aniqlash qiyin bo'lgan ba'zi yomon xatolarga olib keladi.
Liskov printsipini tushunish oson, lekin kodda aniqlash qiyin. Shunday qilib, keling, bir misolni ko'rib chiqaylik.
class Rectangle {
protected int width, height;
public Rectangle() {
}
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
Bizda oddiy Rectangle sinfi va to‘rtburchak maydonini qaytaruvchi getArea funksiyasi mavjud.
Endi biz Squares uchun boshqa sinf yaratishga qaror qildik. Ma'lumki, kvadrat kengligi balandlikka teng bo'lgan to'rtburchakning maxsus turidir.
class Square extends Rectangle {
public Square() {}
public Square(int size) {
width = height = size;
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
Bizning Square sinfimiz Rectangle sinfini kengaytiradi. Biz konstruktorda balandlik va kenglikni bir xil qiymatga o'rnatdik, lekin biz hech qanday mijoz (kodda bizning sinfimizdan foydalanadigan kishi) balandlik yoki vaznni kvadrat xususiyatini buzadigan tarzda o'zgartirishini xohlamaymiz.
Shuning uchun biz har ikkala xususiyatni ham ulardan biri o'zgartirilganda o'rnatish uchun sozlagichlarni bekor qilamiz. Lekin bu bilan biz Liskov almashtirish tamoyilini buzdik.
GetArea funksiyasida testlarni bajarish uchun asosiy sinf yarataylik.
class Test {
static void getAreaTest(Rectangle r) {
int width = r.getWidth();
r.setHeight(10);
System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea());
}
public static void main(String[] args) {
Rectangle rc = new Rectangle(2, 3);
getAreaTest(rc);
Rectangle sq = new Square();
sq.setWidth(5);
getAreaTest(sq);
}
}
Jamoangizning testeri hozirgina getAreaTest sinov funksiyasini o'ylab topdi va getArea funksiyangiz kvadrat ob'ektlar uchun sinovdan o'ta olmasligini aytadi.
Birinchi testda biz kengligi 2 va balandligi 3 bo'lgan to'rtburchak yaratamiz va getAreaTest ni chaqiramiz. Chiqarish kutilganidek 20 ga teng, ammo kvadratdan o'tganimizda ishlar noto'g'ri bo'ladi. Buning sababi shundaki, testdagi setHeight funksiyasiga qo'ng'iroq kenglikni ham o'rnatadi va kutilmagan natijaga olib keladi.
Interfeysni ajratish printsipi
Segregatsiya narsalarni bir-biridan ajratishni anglatadi va interfeyslarni ajratish printsipi interfeyslarni ajratishdir.
Printsip shuni ko'rsatadiki, ko'plab mijozga xos interfeyslar bitta umumiy maqsadli interfeysdan yaxshiroq. Mijozlar ularga kerak bo'lmagan funktsiyani bajarishga majburlanmasligi kerak.
Bu tushunish va qo'llash uchun oddiy tamoyil, shuning uchun bir misolni ko'rib chiqaylik.
public interface ParkingLot {
void parkCar(); // Decrease empty spot count by 1
void unparkCar(); // Increase empty spots by 1
void getCapacity(); // Returns car capacity
double calculateFee(Car car); // Returns the price based on number of hours
void doPayment(Car car);
}
class Car {
}
Biz juda soddalashtirilgan avtoturargohni modellashtirdik. Bu soatlik to'lovni to'laydigan to'xtash joyi turi. Endi biz bepul avtoturargohni amalga oshirmoqchi ekanligimizni o'ylab ko'ring.
public class FreeParking implements ParkingLot {
@Override
public void parkCar() {
}
@Override
public void unparkCar() {
}
@Override
public void getCapacity() {
}
@Override
public double calculateFee(Car car) {
return 0;
}
@Override
public void doPayment(Car car) {
throw new Exception("Parking lot is free");
}
}
Bizning to'xtash joyi interfeysimiz 2 narsadan iborat edi: to'xtash bilan bog'liq mantiq (avtomobilni to'xtatib turish, mashinani olib tashlash, sig'imni olish) va to'lov bilan bog'liq mantiq.
Ammo bu juda aniq. Shu sababli, bizning FreeParking sinfimiz ahamiyatsiz bo'lgan to'lov bilan bog'liq usullarni qo'llashga majbur bo'ldi. Keling, interfeyslarni ajratamiz yoki ajratamiz.
Endi avtoturargohni ajratdik. Ushbu yangi model yordamida biz hatto uzoqroqqa borishimiz va har xil to'lov turlarini qo'llab-quvvatlash uchun PaidParkingLot -ni ajratishimiz mumkin.
Endi bizning modelimiz ancha moslashuvchan, kengaytirilishi mumkin va mijozlar hech qanday ahamiyatsiz mantiqni amalga oshirishlari shart emas, chunki biz to'xtash joyi interfeysida faqat to'xtash joyi bilan bog'liq funksiyalarni taqdim etamiz.
Bog'liqlik inversiyasi printsipi
Dependency Inversion printsipi shuni ko'rsatadiki, bizning sinflarimiz aniq sinflar va funktsiyalar o'rniga interfeyslarga yoki mavhum sinflarga bog'liq bo'lishi kerak.
Bob amaki o'zining maqolasida (2000) bu tamoyilni quyidagicha umumlashtiradi:
"Agar OCP OO arxitekturasining maqsadini bildirsa, DIP asosiy mexanizmni bildiradi".
Bu ikki tamoyil haqiqatan ham o'zaro bog'liqdir va biz ochiq-yopiq printsipni muhokama qilganimizda bu naqshni ilgari qo'llagan edik.
Biz sinflarimiz kengayish uchun ochiq bo'lishini istaymiz, shuning uchun biz o'z bog'liqliklarimizni aniq sinflar o'rniga interfeyslarga bog'liq qilib qayta tashkil qildik. Bizning PersistenceManager sinfimiz ushbu interfeysni amalga oshiradigan sinflar o'rniga InvoicePersistence-ga bog'liq.
Xulosa
Ushbu maqolada biz SOLID tamoyillari tarixidan boshladik, keyin esa har bir tamoyilning nima uchun va qandayligini aniq tushunishga harakat qildik. Biz hatto SOLID tamoyillariga bo'ysunish uchun oddiy Invoice ilovasini qayta ko'rib chiqdik.
Maqolani to'liq o'qishga vaqt ajratganingiz uchun sizga rahmat aytmoqchiman va yuqoridagi tushunchalar aniq deb umid qilaman.
Kodingiz yanada toza, kengaytiriladigan va sinovdan o'tkazilishi uchun kodingizni loyihalash, yozish va qayta tiklashda ushbu tamoyillarni yodda saqlashni taklif qilaman.
Agar siz shunga o'xshash ko'proq maqolalarni o'qishga qiziqsangiz, yangi maqola chop etganimda xabar olish uchun mening blogimning pochta ro'yxatiga obuna bo'lishingiz mumkin.
Top comments (0)