Seriya: JavaScript Basics Dars 32

Encapsulation Pattern

Ichki holatni himoya qilishning to'rt usuli — closure, private field (#), getter/setter validatsiya bilan va Object.freeze — va ularni birlashtirgan to'liq BankAccount misoli.

32-dars

Encapsulation Pattern

1. Encapsulation nima — asosiy maqsad

Encapsulation — obyektning ichki holatini (state) tashqi dunyodan yashirish, va unga faqat nazorat qilingan interfeys (metodlar) orqali murojaat qilishga ruxsat berish.

// ❌ Encapsulation YO'Q — hamma narsa ochiq, nazoratsiz
const account = { balance: 1000 };
account.balance = -99999; // hech qanday tekshiruv yo'q, to'g'ridan-to'g'ri buzib bo'ladi

// ✅ Encapsulation BOR — nazorat orqali
class Account {
  #balance;
  constructor(balance) { this.#balance = balance; }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error('Yetarli mablag\' yo\'q');
    this.#balance -= amount;
  }

  getBalance() { return this.#balance; }
}

Bunga olib keladigan barcha texnik vositalarni allaqachon alohida-alohida ko'rdik. Endi ularni bitta tizim sifatida yig'amiz.


2. Usul 1 — Closure orqali (ES5 davri, hali ham dolzarb)

function createAccount(initialBalance) {
  let balance = initialBalance; // CLOSURE orqali "ushlab qolinadi"

  return {
    deposit(amount) { balance += amount; },
    withdraw(amount) {
      if (amount > balance) throw new Error('Yetarli mablag\' yo\'q');
      balance -= amount;
    },
    getBalance() { return balance; }
  };
}

const acc = createAccount(1000);
acc.getBalance(); // 1000
acc.balance;      // undefined — TASHQARIDAN KO'RINMAYDI!

V8 ichida nima bo'ladi? balance heapda, createAccountning Context obyektida yashaydi. Qaytarilgan obyekt ({deposit, withdraw, getBalance}) — bu Context'ga pointer orqali ulangan closure'lar. Tashqaridan balancega hech qanday yo'l bilan (hatto Object.keys, for...in orqali ham) yetib bo'lmaydi, chunki u obyektning o'zida emas, funksiyaning ichki scope'ida.

Kamchilik: har bir createAccount() chaqiruvi — yangi deposit, withdraw, getBalance funksiyalarini yaratadi ("yomon pattern" bilan bir xil muammo):

const acc1 = createAccount(1000);
const acc2 = createAccount(2000);
console.log(acc1.getBalance === acc2.getBalance); // false — IKKI XIL funksiya!

Minglab account yaratilsa — minglab alohida funksiya nusxasi, xotira behuda sarflanadi (prototype orqali bo'lishilmaydi).


3. Usul 2 — Private Field (#) orqali (zamonaviy, ES2022+)

class Account {
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) { this.#balance += amount; }
  withdraw(amount) {
    if (amount > this.#balance) throw new Error('Yetarli mablag\' yo\'q');
    this.#balance -= amount;
  }
  getBalance() { return this.#balance; }
}

const acc1 = new Account(1000);
const acc2 = new Account(2000);
console.log(acc1.getBalance === acc2.getBalance); // TRUE! — bitta funksiya, prototype orqali bo'lishilgan

Bu yerda Closure'ning kamchiligi yo'q — metodlar Account.prototypeda (bitta marta yaratilgan), faqat #balance — har instance uchun alohida, maxsus private slotda. Bu — xotira jihatdan optimal va haqiqiy maxfiylik (SyntaxError darajasida himoyalangan) birlashtirilgan holat.


4. Usul 3 — Getter/Setter orqali validatsiya bilan birlashtirilgan encapsulation

class Account {
  #balance;

  constructor(balance) {
    this.balance = balance; // SETTER orqali o'tadi, validatsiya ishlaydi!
  }

  set balance(value) {
    if (value < 0) throw new Error('Balans manfiy bo\'lishi mumkin emas');
    this.#balance = value;
  }

  get balance() {
    return this.#balance;
  }
}

const acc = new Account(1000);
acc.balance = 500;   // ✅ oddiy property kabi yoziladi, lekin ICHKARIDA setter ishlaydi
console.log(acc.balance); // 500 — getter orqali o'qiladi

acc.balance = -100; // ❌ Error: Balans manfiy bo'lishi mumkin emas

Bu — Accessor Descriptor bilan Private Field'ning birlashtirilishi: tashqi tomondan acc.balance — xuddi oddiy property kabi ko'rinadi va ishlatiladi (chiroyli sintaksis), lekin ichkarida har bir yozish/o'qish nazorat ostida (funksiya chaqiruvi orqali).


5. Usul 4 — Object.freeze orqali (immutability asosida encapsulation)

function createPoint(x, y) {
  return Object.freeze({
    getX: () => x,
    getY: () => y,
  });
}

const p = createPoint(3, 4);
p.getX(); // 3
p.newProp = 'test'; // sokin ignore (freeze qilingan)

Bu — Object.freeze bilan Closure'ning birlashtirilishi: x, y closure orqali yashiringan, va qaytarilgan obyektning o'zi freeze qilingan — hech kim unga yangi metod qo'sha olmaydi yoki mavjudini almashtira olmaydi.


6. To'rt usulni solishtirish — qachon qaysi birini ishlatish kerak

Usul Xotira samaradorligi Haqiqiy maxfiylik Qachon ishlatiladi
Closure ❌ Past (har instance — yangi funksiyalar) ✅ To'liq (scope orqali) Kam sonli obyekt, funksional stil
Private field (#) ✅ Yuqori (prototype orqali bo'lishiladi) ✅ To'liq (syntax darajasida) Zamonaviy class-based kod, tavsiya etiladi
Getter/Setter + # ✅ Yuqori ✅ To'liq + validatsiya Nazorat kerak bo'lgan property'lar
Object.freeze O'rtacha Qisman (closure orqali) Immutable, funksional obyektlar

7. Encapsulation'ning haqiqiy maqsadi — nima uchun shuncha mexanizm kerak

Buni bir savol orqali tushuntiray: nega #balanceni to'g'ridan-to'g'ri, ochiq (this.balance) qilib qo'yib, "iltimos to'g'ridan-to'g'ri o'zgartirmang" deb hujjatda yozib qo'ymaymiz?

// Agar HIMOYASIZ bo'lsa:
class Account {
  constructor(balance) { this.balance = balance; } // OCHIQ
}

const acc = new Account(1000);
acc.balance = 'ko\'p pul'; // ❌ HECH KIM TO'XTATMAYDI — endi balance STRING!
acc.balance += 100;         // NaN — dastur BUZILADI, lekin xato tashlamaydi darhol

Bu — dasturning keyinroq, kutilmagan joyda (masalan, balansni ko'paytirish/kamaytirish funksiyasida) sirli tarzda ishlamay qolishiga olib keladi — xato darhol emas, kechroq, boshqa joyda paydo bo'ladi, va uni topish qiyinlashadi. Encapsulation — bu muammoni eng boshida, xato joyida, darhol ushlab qolish uchun.


8. Yakuniy, to'liq misol — hammasi birlashgan holda

class BankAccount {
  #balance;
  #transactionHistory = [];

  constructor(initialBalance = 0) {
    if (initialBalance < 0) throw new Error('Boshlang\'ich balans manfiy bo\'lishi mumkin emas');
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error('Miqdor musbat bo\'lishi kerak');
    this.#balance += amount;
    this.#logTransaction('deposit', amount);
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error('Yetarli mablag\' yo\'q');
    this.#balance -= amount;
    this.#logTransaction('withdraw', amount);
  }

  #logTransaction(type, amount) { // PRIVATE METOD
    this.#transactionHistory.push({ type, amount, date: new Date() });
  }

  get balance() {
    return this.#balance; // faqat O'QISH uchun ochiq, YOZISH uchun yopiq (setter yo'q)
  }

  get history() {
    return [...this.#transactionHistory]; // NUSXA qaytariladi, asl massiv himoyalanadi!
  }
}

const acc = new BankAccount(1000);
acc.deposit(500);
acc.withdraw(200);

console.log(acc.balance); // 1300 — faqat o'qish
acc.balance = 99999;      // sokin ignore qilinadi (setter yo'q, faqat getter bor)

console.log(acc.history); // barcha tranzaksiyalar, lekin NUSXA — asl massivga ta'sir qilmaydi
acc.history.push('hack'); // bu FAQAT nusxaga qo'shiladi, asl #transactionHistory O'ZGARMAYDI
console.log(acc.history.length); // hali ham 2 (hack qo'shilmagan!)

Diqqat qiling — get history() nega nusxa ([...this.#transactionHistory]) qaytaradi? Chunki agar massivning o'zi (pointeri) qaytarilsa, tashqaridan uni to'g'ridan-to'g'ri o'zgartirish mumkin bo'lib qolardi (acc.history.push(...) asl massivga ta'sir qilardi). Nusxa qaytarish orqali — ichki holat butunlay himoyalangan bo'ladi, hatto "faqat o'qish uchun" deb berilgan narsa orqali ham buzib bo'lmaydi.


Xulosa

Encapsulation — bu yakuniy maqsad ("ichki holatni himoya qilish, faqat nazorat qilingan interfeys orqali kirish"), unga erishish uchun JS to'rtta texnik vosita taklif qiladi: Closure (funksiya scope orqali, eski, lekin xotira jihatdan qimmat), Private field # (zamonaviy, prototype bilan birga ishlaydigan, xotira samarali), Getter/Setter (validatsiya bilan, tashqi ko'rinishni oddiy property kabi saqlab qolgan holda), va Object.freeze (butun obyektni immutable qilish). Odatda Private field + Getter/Setter birgalikda ishlatiladi, chunki bu ikkalasi ham xotira samaradorligini, ham haqiqiy maxfiylikni, ham validatsiya imkoniyatini bir vaqtda beradi.