Asosiy kontentga o'tish
Full Stack

Taʼlim dargohlarida oʻrgatilmaydigan dasturlash tamoyillari

Bilishingiz kerak boʻlgan muhim prinsiplar – RY, KISS, SOLID.

Oʻquv dargohlari sizga qanday dasturlashni va majburiy, diskret matematika, hisoblashni oʻrgatadi. Bu birinchi kurs sifatidagi bilimlarim va yuqori kursdagilarning tajribalariga asoslanib chiqarilgan xulosa. Lekin universitetni tamomlab, sanoat sohasiga kirganingizda bularning oʻzi kamlik qiladi. Oddiy bir oʻzgarishni amalga oshirish uchun ham konsepsiya va prinsiplarni bilishingiz zarur boʻladi.

KISS tamoyili

Buni osonlashtir, goʻdakkina!

Koʻpincha jamoa boʻlib ishlashga toʻgʻri keladi. Dasturchilar guruhi loyihaning turli aspektlarida ish olib boradi. Oʻz ishingizni qoʻyib, boshqa dasturchining kodi ustida ishlashni davom ettirsangiz, nima boʻladi. Oʻsha vaziyatda hech qanday izohlarsiz yoki oʻzgaruvchilarsiz holatda qolib ketgan betartib kodingizni koʻrish yoqadimi? Yoki loyihaning tegishli qismini ifodalaydigan yaxshi yozilgan kod koʻproq maʼqul keladimi? Ikkinchisi yoqishi aniq.

Tasavvur qiling. Juda ham murakkab boʻlgan dasturni yozyapsiz, lekin tugatmasdan kasal boʻlib qoldingiz. Oradan bir hafta oʻtib, tuzalganingizdan soʻng loyihani qayta koʻrib chiqasiz. Albatta, bu vaziyatda dasturlash tempidan chiqib ketgan boʻlasiz. Lekin sizga qaytganingizdan soʻng umuman tushunarsiz boʻlgan kodni koʻrish yoqqan boʻlarmidi? Yoki bir qarashingizdayoq kodni tushunib olib, ish boshlashni istarmidingiz? Albatta, yana ikkinchisi.

Yana bir bor tasavvur qiling. Bitta loyiha ustida yolgʻiz oʻzingiz ishlayapsiz. Jarayonda baglarga duch keldingiz. Endi esa ularni toʻgʻrilashingiz kerak. Oʻylab koʻring, kodingiz murakkablashgandan keyin bagni tuzatish osonmi yoki undan avval toʻgʻrilagan maʼqulmi?

Yuqoridagi vaziyatlarning barchasi bir narsaga ishora qilyapti — Buni osonlashtir, goʻdakkina!

“Kompyuter tushuna oladigan kodni har qanday ovsar odam ham yoza oladi. Yaxshi dasturchilar esa insonlar tushunadigan kodni yozadi”, Martin Fowler (ingliz dasturiy taʼminot ishlab chiqaruvchisi, shu yoʻnalish boʻyicha xalqaro maʼruzachi va muallif).

Oxir-oqibat topshiriqni bajarish uchun murakkab yoki sodda kod ishlab chiqqaningizning mashinaga umuman qizigʻi yoʻq. Bu uni oʻqishga va tushunishga harakat qiladigan (jumladan, siz) odamlarga qiziq boʻladi xolos.

Lekin men KISSʼni qanday amalga oshiraman? (gʻalati eshitiladi, bilaman)

“Student” model klasini tasavvur qiling. U xotirasida kaliti va qiymati ikkala juftlikda boʻlgan 2 ta buyum va 1 ta xaritani saqlaydi. Birinchi juftlik oʻzida 2 ta stringʼni saqlaydi: modulning nomi va identifikator. Ikkinchi juftlik 2 ta dubldan iborat boʻlib, modulning erishilgan va eng yuqori darajasini belgilaydi.

data class Student(
  val name: String,
  val age: Int,
  val moduleMarks: Map<Pair<String, String>, Pair<Int, Int>>
)

Shu kabi dizaynga aloqador tanlov qilganingizda sizdan 80% dan yuqori natija koʻrsatgan “students” va “modules” nomlarini yozib qoʻyishingiz talab qilinadi.

fun scholars(students: List<Student>): Map<String, List<String>> {
  val scholars = mutableMapOf<String, List<String>>()
  students.forEach { student ->
    scholars[student.name] = student.moduleMarks
      .filter { (_, (a, m)) -> a / m > 0.8}
      .map { ((n, _), _) -> n }
  }
  return scholars
}

Shunga oʻxshash kodga hattoki bir necha kun oʻtib qaytib kelishning oʻzi dahshatli boʻlardi. Toʻgʻri, bu oson kod, lekin uni yana ham soddalashtirishning imkoni bor. Imkon qadar koʻproq tushunchalar va oʻzgaruvchilarni tanishtiring, deyishingiz mumkin.

data class Student(
  val name: String,
  val age: Int,
  val moduleMarks: Map<Module, Mark>
)

data class Module(
  val name: String,
  val id: String
)

data class Mark(
  val achieved: Double,
  val maximum: Double
) {
  fun isAbove(percentage: Double): Boolean {
    return achieved / maximum * 100 > percentage
}

fun scholars(students: List<Student>): Map<String, List<String>> {
  val scholars = mutableMapOf<String, List<String>>()
  students.forEach { student ->
    val modulesAbove80 = student.moduleMarks
      .filter { (_, mark) -> mark.isAbove(80.0)}
      .map { (module, _) -> module.name }

    scholars[student.name] = modulesAbove80
  }
  return scholars
}

Bu koʻplab kodlarni qoʻshib hisoblaydi. Lekin muhimrogʻi kod tozaroq koʻrinadi va ingliz tili kabi oʻqiladi.

DRY tamoyili

Bitta kodni qayta-qayta yozmang

Agar siz bir xil kodni qayta-qayta yozayotgan boʻlsangiz, funksiya yarating va uni ishlating. Men universitetdan berilgan topshiriqda obyektlar toʻplami (cell dasturlash tili) ustida ishladim. Koʻpgina (agar hammasi boʻlmasa) aniqlangan funksiyalar mendan maʼlum bir obyektni qidirish, olish va u bilan ishlashni talab qildi.

public class Spreadsheet implements BasicSpreadsheet {
  private final Set<Cell> cells;

  @Override
  public double getCellValue(CellLocation location) {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
    
    return cell == null ? 0d : cell.getValue();
  }

  @Override
  public String getCellExpression(CellLocation location) {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
    
    return cell == null ? "" : cell.getExpression();
  }

  @Override
  public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
    Cell cell = cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);

    // ...
  }

  // ...
}

Yuqorida katta kodga misol berilgan. Lekin uni yozayotganimda bir necha joylarda bir xil kod bloklarini qaytarayotganimni aniqladim. Shuning uchun uni hamma joyda qayta ishlata oladigan funksiyalarga ajratib tashladim.

public class Spreadsheet implements BasicSpreadsheet {
  private final Set<Cell> cells;

  @Override
  public double getCellValue(CellLocation location) {
    return getFromCell(location, Cell::getValue, 0d);
  }

  @Override
  public String getCellExpression(CellLocation location) {
    return getFromCell(location, Cell::getExpression, "");
  }

  @Override
  public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
    Cell cell = findCell(location);

    // ...
  }

  // ...

  private Cell findCell(CellLocation location) {
    return cells.stream()
        .filter(cell -> cell.location.equals(location))
        .findFirst()
        .orElse(null);
  }

  private <T> T getFromCell(CellLocation location,
                            Function<Cell, T> function,
                            T defaultValue) {
    Cell cell = findCell(location);
    return cell == null ? defaultValue : function.apply(cell);
  }
}

Agar men shu payt kodimda bag borligini bilsam, xatolikni kodning har xil n joylarini oʻzgartirishga majbur boʻlmayman. Funksiya ichida uni darhol oʻzgartiraman va hamma joydagi baglar bartaraf boʻladi.

SOLID tamoyili

Bu bitta tamoyil emas. SOLID dasturiy taʼminot ishlab chiqishda bilish zarur boʻlgan 5 prinsipni oʻz ichiga oladi.

S — Yagona majburiyat (Single responsibility)

Klassni oʻzgartirish uchun bitta va faqatgina bitta sabab boʻlishi kerak.

Taxminimizcha, tushunish eng oson boʻlgan tamoyil. Siz aniqlaydigan har bir klass/funksiya faqatgina bitta topshiriqni bajarishi kerak. Shuningdek, netvorking ilovasi yaratayotganingizni ham hisobga oling.

class Repository(
  private val api: MyRemoteDatabase,
  private val local: MyLocalDatabase
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    // Saving data in the cache
    var model = Model.parse(response.payload)
    val success = local.addModel(model)
    if (!success) {
      emit(Error("Error caching the remote data"))
      return@flow
    }

    // Returning data from a single source of truth
    model = local.find(model.key)
    emit(Success(model))
  }
}

Yuqoridagi kodda Yagona majburiyat tamoyili buzilgan. Funksiya nafaqat masofaviy maʼlumotlarni toʻplash, balki ularni qurilma xotirasida saqlash uchun ham javobgar boʻlgan. Bu vazifa boshqa klassga yuklatilishi kerak.

class Repository(
  private val api: MyRemoteDatabase,
  private val cache: MyCachingService /* Notice I changed the dependency */
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    val model = cache.save(response.payload)

    // Sending back the data
    model?.let {
      emit(Success(it))
    } ?: emit(Error("Error caching the remote data"))
  }
}

// Shifted all caching logic to another class
class MyCachingService(
  private val local: MyLocalDatabase
) {
  suspend fun save(payload: Payload): Model? {
    var model = Model.parse(payload)
    val success = local.addModel(model)
    return if (success)
      local.find(model.key)
    else
      null
  }
}

Eʼtibor bering, MyCachingService faqat kiruvchi maʼlumotlarni qurilmaning maʼlumotlar bazasiga saqlash vazifasini bajaradi. Repozitoriy esa faqat yuqoridagi modelni yuborish uchun maʼlumotlarni olish bilan shugʻullanadi. Bu yondashuvni qoʻllash yaxshi amaliyot. Chunki u vazifalarni bir-biridan ajratish tamoyiliga asoslanadi. Bu esa nosozliklarni aniqlash va dasturni sinovdan oʻtkazish jarayonini yaxshilaydi.

O — Ochiq/yopiq

Dasturiy taʼminot birliklari (klass, modul, funksiya) kengaytirish ishlari uchun doimo ochiq, oʻzgartirishlar uchun esa yopiq boʻlishi kerak.

Bu tamoyil kelajakdagi oʻzgarishlarda client-side kodni buzmaydigan dasturiy taʼminot kodini ishlab chiqish kerakligini anglatyapti. Siz Kotlinʼda web development API yaratayotganingizni hisobga oling. Siz ParagraphTag, AnchorTag va ImageTagʼni loyihalashtirdingiz. Sizdan kodingizda ikki elementning kattaliklarini solishtirish soʻraladi.

class ParagraphTag(
  val width: Int,
  val height: Int
)

class AnchorTag(
  val width: Int,
  val height: Int
)

class ImageTag(
  val width: Int,
  val height: Int
)

// Client-code
infix fun ParagraphTag.tallerThan(anchor: AnchorTag): Boolean {
  return this.height > anchor.height
}

infix fun AnchorTag.tallerThan(anchor: ParagraphTag): Boolean {
  return this.height > anchor.height
}

infix fun ParagraphTag.tallerThan(anchor: ImageTag): Boolean {
  return this.height > anchor.height
}

// ... more functions

Bu koʻp mehnat talab qiladi. Endi sizdan Heading tagʼni ham tarkibga kiritishingiz talab qilinadi. Client-sideʼda yana 6 ta funksiya biriktirishingiz kerak. Bu zerikarli ishni bajargan soʻng client-side kodni dasturingiz ehtiyojlariga mos ravishda oʻzgartirasiz.

Oʻrniga interfeysni tasdiqlang — PageTag

interface PageTag {
  val width: Int
  val height: Int
}

class ParagraphTag(
  override val width: Int,
  override val height: Int
) : PageTag

class AnchorTag(
  override val width: Int,
  override val height: Int
) : PageTag

class ImageTag(
  override val width: Int,
  override val height: Int
) : PageTag


// Client Code
infix fun PageTag.tallerThan(other: PageTag): Boolean {
  return this.height > other.height
}

Endi esa qoʻshimcha oʻzgartirishlar uchun client codeʼni yopasiz. Lekin u funksional imkoniyatlarni oshirish maqsadida yangi klass yaratish va PageTagʼni tatbiq etish uchun ochiq. Nihoyat hamma narsa mukammal tarzda ishlaydi.

L — Liskov almashtirish tamoyili 

Agar Sʼni Tʼning kichik turi deb oladigan boʻlsak, oʻshanda T maʼqullaydigan hamma xossalar S ga ham taalluqli boʻladi.

Oh. Matematikami? Bu yaxshi emas. Aksincha, bu tushunish oson boʻlgan tamoyil. Qani, yangi misol bilan koʻramiz.

open class Bird {
  open fun fly() {
    // ... performs code to fly
  }

  open fun eat() {
    // ...
  }
}

class Penguin : Bird() {
  override fun fly() {
    throw UnsupportedOperationException("Penguins cannot fly")
  }
}

E’tibor bering, yuqoridagi Bird klasi hech qanday istisno chiqarmaydi. Penguin klasi esa chiqaradi. Mijoz kodini buzmasdan yoki oʻzgartirmasdan turib, Penguin klasini Bird bilan almashtira olmaysiz. Bu Liskov almashtirish tamoyiliga zid boʻladi. Penguin klasi Birdʼni kengaytirishi client-side kodni buzadi. Ochiq/yopiq tamoyiliga qarshi boradi.

Buni bartaraf etishning yoʻli — dizayn joriy etish metodini oʻzgartirishdir.

open class FlightlessBird {
  open fun eat() {
    // ...
  }
}

open class Bird : FlightlessBird() {
  open fun fly() {
    // ...
  }
}

class Penguin : FlightlessBird() {
   // ...
}

class Eagle : Bird() {
  // ...
}

Yuqoridagi koddan tushunishingiz mumkin. Agar FlightlessBird yeya olsa, FlightlessBirdʼning tarkibidagi barcha kichik klasslar ham yeya oladi. Xuddi shunga oʻxshab, agar Bird ucha olsa, Birdʼning barcha kichik klasslari ham ucha olishi shart.

I — Interfeys boʻlinishi 

Interfeyslar dasturlarini oʻzlari foydalanmaydigan metodlarga bogʻliq boʻlishga majburlamaydi.

Bu izoh qoʻrqinchli koʻrinmayapti. Aslida ham qoʻrqinchli emas. Keling, mashina, samolyot va velosiped ishlab chiqaryapsiz, deb tasavvur qilamiz. Shunda ularning hammasi avtomobil boʻlgani uchun Transport interfeysini tatbiq etasiz.

interface Vehicle {
  fun turnOn()
  fun turnOff()
  fun drive()
  fun fly()
  fun pedal()
}

class Car : Vehicle {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun drive() { /* Implementation */ }
  override fun fly() = Unit
  override fun pedal() = Unit
}

class Aeroplane : Vehicle {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun drive() = Unit
  override fun fly() { /* Implementation */ }
  override fun pedal() = Unit
}

class Bicycle : Vehicle {
  override fun turnOn() = Unit
  override fun turnOff() = Unit
  override fun drive() = Unit
  override fun fly() = Unit
  override fun pedal() { /* Implementation */ }
}

Klasslarning o‘ziga kerak bo‘lmagan usullarni majburan amalga oshirayotganini ko‘ryapsizmi? Bundan tashqari, men klassni mavhum deb ayta olmayman. Interfeys ajratish prinsipiga muvofiq quyidagi dizaynga ega bo‘lishimiz kerak.

interface SystemRunnable {
  fun turnOn()
  fun turnOff()
}

interface Drivable() {
  fun drive()
}

interface Flyable() {
  fun fly()
}

interface Pedalable() {
  fun pedal()
}

class Car : SystemRunnable, Drivable {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun drive() { /* Implementation */ }
}

class Aeroplane : SystemRunnable, Flyable {
  override fun turnOn() { /* Implementation */ }
  override fun turnOff() { /* Implementation */ }
  override fun fly() { /* Implementation */ }
}

class Bicycle : Pedalable {
  override fun pedal() { /* Implementation */ }
}

Endi bunisi tartibliroq koʻrinyapti. Shuningdek, interfeyslarning turli xil imkoniyatlarini koʻrsatish ham osonroq.

D — Bogʻliqlik inversiyasi

  1. Yuqori darajali modullar past darajali modullarga bogʻliq boʻlmaydi. Ikkalasi ham abstraksiya usuliga bogʻliq boʻlishi kerak.

  2. Abstraksiyalar detallarga bogʻliq boʻlmasligi kerak. Aksincha, detallar abstraksiyalarga bogʻliq boʻlishi zarur.

Buni qanday tushunish mumkin? Biznes yoki foydalanuvchi interfeysi aniqlaydigan modullar yuqori darajali hisoblanadi. Past darajadagi modullar esa dasturning murakkabliklarini boshqaradi. Solid responsibility tamoyilidagi misolimni eslang:

class Repository(
  private val api: MyRemoteDatabase,
  private val cache: MyCachingService
) {
  fun fetchRemoteData() = flow {
    // Fetching API data
    val response = api.getData()

    val model = cache.save(response.payload)

    // Sending back the data
    model?.let {
      emit(Success(it))
    } ?: emit(Error("Error caching the remote data"))
  }
}

class MyRemoteDatabase {
  suspend fun getData(): Response { /* ... */ }
}

class MyCachingService(
  private val local: MyLocalDatabase
) {
  suspend fun save(): Model? { /* ... */ }
}

class MyLocalDatabase {
  suspend fun add(model: Model): Boolean { /* ... */ }
  suspend fun find(key: Model.Key): Model { /* ... */ }
}

"Bunda hammasi joyida va mukammal tarzda ishlaydi. Biroq kelajakda men 

  • Local maʼlumotlar bazasini PostgreSqlʼdan MongoDB tizimiga oʻzgartirmoqchi boʻlsam; 

  • maʼlumotlarni saqlash mexanizmimni butkul oʻzgartirishga qaror qilsam

butun dastur amaliyotlarini va client-side codeʼni oʻzgartirishga majbur boʻlardim. Buning sababi shuki, yuqori darajali modullar past darajali aniq modullarga bogʻliq boʻladi."

Bu to‘g‘ri emas. Buning o‘rniga funksionallikni interfeys ko‘rinishida mavhumlashtirishingiz kerak. Aniq amalga oshirishlar esa bu interfeysni kengaytirishlari kerak boʻladi.

interface CachingService {
  suspend fun save(): Model?
}

interface SomeLocalDb() {
  suspend fun add(model: Model): Boolean
  suspend fun find(key: Model.Key): Model
}

class Repository(
  private val api: SomeRemoteDb,
  private val cache: CachingService
) { /* Implementation */ }

class MyCachingService(
  private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class MyAltCachingService(
  private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class PostgreSQLLocalDb : SomeLocalDb { /* Implement methods */ }
class MongoLocalDb : SomeLocalDb { /* Implement methods */ }

Buni birgina soʻzni oʻzgartirish orqali hal qiling. Shunda butun ilovangizdagi repozitoriy uchun turli xil amaliyotlar orasida osongina almashtirishingiz mumkin.



Manba: Programming Principles They Don’t Teach You In School

Mohirdev Telegram

Telegram kanalimizga obuna bo’lishni unutmang

Obuna bo'lish
medium.com

medium.com

O'xshash maqolalar