14-bo‘lim
Observer
Bir hodisadan ko'p obyektni xabardor qilish - va bu qaysi muammolarni keltiradi.
Ushbu bo‘lim mundarijasi
Observer - obyekt holati o'zgarganda unga obuna bo'lganlarni avtomatik xabardor qiladigan naqsh.
Boshqa nomlari: publish/subscribe, event listener, signal/slot.
Muammo: bitta metod hammani biladi #
Buyurtma qabul qilinganda beshta ish bo'lishi kerak:
final readonly class Buyurtma
{
public function __construct(
public int $id,
public string $mijoz,
public int $summa,
) {}
}
final class BuyurtmaXizmati
{
public function qabul(Buyurtma $b): void
{
echo "buyurtma #{$b->id} qabul qilindi", PHP_EOL;
// hammasi shu yerda
echo " [email] {$b->mijoz} ga xat yuborildi", PHP_EOL;
echo " [sms] tasdiq kodi yuborildi", PHP_EOL;
echo " [ombor] mahsulot ajratildi", PHP_EOL;
echo " [statistika] +{$b->summa} som", PHP_EOL;
echo " [jurnal] buyurtma #{$b->id}", PHP_EOL;
}
}
(new BuyurtmaXizmati())->qabul(new Buyurtma(1, 'Husanboy', 500000));
buyurtma #1 qabul qilindi
[email] Husanboy ga xat yuborildi
[sms] tasdiq kodi yuborildi
[ombor] mahsulot ajratildi
[statistika] +500000 som
[jurnal] buyurtma #1
Marketing "sodiqlik ballari qo'shilsin" deydi. Buxgalteriya "1C ga yuborilsin" deydi. Ombor "Telegram guruhga xabar kelsin" deydi.
Har talab shu metodga bir qator qo'shadi:
| Muammo | Oqibati |
|---|---|
BuyurtmaXizmati 5 ta tizimni biladi | Har biri o'zgarsa - shu fayl o'zgaradi |
| Yangi ish = shu metodni ochish | Ochiq/yopiq buziladi |
| Sinash uchun 5 ta soxta obyekt kerak | Test og'irlashadi |
| Xat yuborish tushib qolsa | Butun buyurtma yiqiladi |
Oxirgisi eng jiddiy. Marketing xabari yuborilmagani uchun mijoz buyurtma bera olmasligi - mantiqsiz.
Yechim: obuna ro'yxati #
interface Kuzatuvchi
{
public function nomi(): string;
public function xabar(Buyurtma $b): void;
}
final class BuyurtmaManbasi
{
/** @var list<Kuzatuvchi> */
private array $kuzatuvchilar = [];
public function obuna(Kuzatuvchi ...$k): self
{
foreach ($k as $bir) {
$this->kuzatuvchilar[] = $bir;
}
return $this;
}
public function obunaniBekor(Kuzatuvchi $k): void
{
$this->kuzatuvchilar = array_values(array_filter(
$this->kuzatuvchilar,
static fn(Kuzatuvchi $x): bool => $x !== $k,
));
}
public function qabul(Buyurtma $b): void
{
echo "buyurtma #{$b->id} qabul qilindi", PHP_EOL;
foreach ($this->kuzatuvchilar as $k) {
$k->xabar($b);
}
}
public function soni(): int
{
return count($this->kuzatuvchilar);
}
}
final class EmailKuzatuvchi implements Kuzatuvchi
{
public function nomi(): string { return 'email'; }
public function xabar(Buyurtma $b): void
{
echo " [email] {$b->mijoz} ga xat yuborildi", PHP_EOL;
}
}
final class OmborKuzatuvchi implements Kuzatuvchi
{
public function nomi(): string { return 'ombor'; }
public function xabar(Buyurtma $b): void
{
echo " [ombor] #{$b->id} uchun mahsulot ajratildi", PHP_EOL;
}
}
final class StatistikaKuzatuvchi implements Kuzatuvchi
{
private int $jami = 0;
public function nomi(): string { return 'statistika'; }
public function xabar(Buyurtma $b): void
{
$this->jami += $b->summa;
echo " [statistika] jami: {$this->jami} som", PHP_EOL;
}
}
$manba = (new BuyurtmaManbasi())->obuna(
new EmailKuzatuvchi(),
new OmborKuzatuvchi(),
new StatistikaKuzatuvchi(),
);
echo 'obunachilar: ', $manba->soni(), PHP_EOL;
$manba->qabul(new Buyurtma(1, 'Husanboy', 500000));
$manba->qabul(new Buyurtma(2, 'Malika', 320000));
obunachilar: 3
buyurtma #1 qabul qilindi
[email] Husanboy ga xat yuborildi
[ombor] #1 uchun mahsulot ajratildi
[statistika] jami: 500000 som
buyurtma #2 qabul qilindi
[email] Malika ga xat yuborildi
[ombor] #2 uchun mahsulot ajratildi
[statistika] jami: 820000 som
Yangi ish qo'shish - BuyurtmaManbasi ni umuman
ochmasdan:
final class TelegramKuzatuvchi implements Kuzatuvchi
{
public function __construct(private readonly string $guruh) {}
public function nomi(): string { return 'telegram'; }
public function xabar(Buyurtma $b): void
{
echo " [telegram] {$this->guruh}: yangi buyurtma #{$b->id}", PHP_EOL;
}
}
$manba = (new BuyurtmaManbasi())->obuna(
new EmailKuzatuvchi(),
new TelegramKuzatuvchi('@ombor_xodimlari'),
);
$manba->qabul(new Buyurtma(3, 'Nodira', 180000));
// obunani bekor qilish ham mumkin
$email = new EmailKuzatuvchi();
$manba2 = (new BuyurtmaManbasi())->obuna($email, new OmborKuzatuvchi());
$manba2->obunaniBekor($email);
echo 'bekor qilingandan keyin: ', $manba2->soni(), PHP_EOL;
$manba2->qabul(new Buyurtma(4, 'Aziza', 90000));
buyurtma #3 qabul qilindi
[email] Nodira ga xat yuborildi
[telegram] @ombor_xodimlari: yangi buyurtma #3
bekor qilingandan keyin: 1
buyurtma #4 qabul qilindi
[ombor] #4 uchun mahsulot ajratildi
Birinchi muammo: bitta istisno hammasini to'xtatadi #
final class NosozKuzatuvchi implements Kuzatuvchi
{
public function nomi(): string { return 'nosoz'; }
public function xabar(Buyurtma $b): void
{
throw new RuntimeException('SMTP server javob bermayapti');
}
}
$manba = (new BuyurtmaManbasi())->obuna(
new NosozKuzatuvchi(),
new OmborKuzatuvchi(), // bu ishlamay qoladi
new StatistikaKuzatuvchi(), // bu ham
);
try {
$manba->qabul(new Buyurtma(5, 'Kamola', 400000));
} catch (RuntimeException $x) {
echo 'YIQILDI: ', $x->getMessage(), PHP_EOL;
}
buyurtma #5 qabul qilindi
YIQILDI: SMTP server javob bermayapti
Ombor hech narsa ajratmadi, statistika hisoblamadi - chunki xat yuborilmadi.
Yechim - har kuzatuvchini alohida himoyalash:
final class HimoyalanganManba
{
/** @var list<Kuzatuvchi> */
private array $kuzatuvchilar = [];
/** @var list<string> */
private array $xatolar = [];
public function obuna(Kuzatuvchi ...$k): self
{
foreach ($k as $bir) { $this->kuzatuvchilar[] = $bir; }
return $this;
}
public function qabul(Buyurtma $b): void
{
echo "buyurtma #{$b->id} qabul qilindi", PHP_EOL;
foreach ($this->kuzatuvchilar as $k) {
try {
$k->xabar($b);
} catch (Throwable $x) {
// bitta kuzatuvchi yiqilsa - qolganlari davom etadi
$this->xatolar[] = $k->nomi() . ': ' . $x->getMessage();
echo " [!] {$k->nomi()} xato berdi, davom etamiz", PHP_EOL;
}
}
}
/** @return list<string> */
public function xatolar(): array
{
return $this->xatolar;
}
}
$manba = (new HimoyalanganManba())->obuna(
new NosozKuzatuvchi(),
new OmborKuzatuvchi(),
new StatistikaKuzatuvchi(),
);
$manba->qabul(new Buyurtma(5, 'Kamola', 400000));
echo '--- xatolar', PHP_EOL;
foreach ($manba->xatolar() as $x) {
echo ' ', $x, PHP_EOL;
}
buyurtma #5 qabul qilindi
[!] nosoz xato berdi, davom etamiz
[ombor] #5 uchun mahsulot ajratildi
[statistika] jami: 400000 som
--- xatolar
nosoz: SMTP server javob bermayapti
Endi hamma kuzatuvchi ishladi. Ammo yangi savol tug'iladi: qaysi ishlar majburiy, qaysilari emas?
| Kuzatuvchi | Yiqilsa nima bo'lishi kerak |
|---|---|
| Ombor mahsulot ajratishi | Butun buyurtma bekor bo'lishi kerak |
| To'lov yozuvi | Bekor |
| Marketing xati | E'tiborsiz qoldirilsin |
| Statistika | E'tiborsiz qoldirilsin |
Ya'ni hammasini "kuzatuvchi" qilish noto'g'ri. Majburiy qadamlar asosiy jarayonning bir qismi bo'lishi kerak, kuzatuvchi esa faqat yon ta'sirlar uchun.
Amaliy qoida:
Agar biror ish bajarilmasa asosiy amal noto'g'ri bo'ladigan bo'lsa - u kuzatuvchi emas.
Tartib kafolatlanmaydi #
$manba = (new BuyurtmaManbasi())->obuna(
new StatistikaKuzatuvchi(),
new EmailKuzatuvchi(),
new OmborKuzatuvchi(),
);
$manba->qabul(new Buyurtma(6, 'Dilnoza', 250000));
buyurtma #6 qabul qilindi
[statistika] jami: 250000 som
[email] Dilnoza ga xat yuborildi
[ombor] #6 uchun mahsulot ajratildi
Bizning kodimizda kuzatuvchilar qo'shilish tartibida
chaqiriladi. Bu tasodifiy emas - foreach shunday ishlaydi.
Lekin naqshning o'zi buni kafolatlamaydi, va ko'p kutubxonalar tartibni boshqacha aniqlaydi:
| Kutubxona | Tartib |
|---|---|
| Symfony EventDispatcher | Ustuvorlik raqami bo'yicha |
| Laravel Events | Ro'yxatga olish tartibi |
| PSR-14 | Ta'rif bo'yicha aniqlanmagan |
Xavfli holat: StatistikaKuzatuvchi OmborKuzatuvchi dan
oldin ishlasa va ombor buyurtmani rad etsa - statistika
bo'lmagan sotuvni hisoblab qo'yadi.
Agar kuzatuvchilar orasida tartib muhim bo'lsa - bu ular aslida ketma-ket jarayon ekanining belgisi. Kuzatuvchi o'rniga aniq qadamlar yozing.
PHP dagi SplSubject va SplObserver #
PHP da ushbu naqsh uchun tayyor interfeyslar bor:
final class Hisoblagich implements SplSubject
{
private SplObjectStorage $kuzatuvchilar;
private int $qiymat = 0;
public function __construct()
{
$this->kuzatuvchilar = new SplObjectStorage();
}
public function attach(SplObserver $observer): void
{
$this->kuzatuvchilar->attach($observer);
}
public function detach(SplObserver $observer): void
{
$this->kuzatuvchilar->detach($observer);
}
public function notify(): void
{
foreach ($this->kuzatuvchilar as $k) {
$k->update($this);
}
}
public function oshir(int $qancha): void
{
$this->qiymat += $qancha;
$this->notify();
}
public function qiymat(): int { return $this->qiymat; }
}
final class Chegara implements SplObserver
{
public function __construct(private readonly int $chegara) {}
public function update(SplSubject $subject): void
{
if (!$subject instanceof Hisoblagich) {
return;
}
$holat = $subject->qiymat() >= $this->chegara ? 'OSHDI' : 'normal';
printf(" %5d %s%s", $subject->qiymat(), $holat, PHP_EOL);
}
}
$h = new Hisoblagich();
$h->attach(new Chegara(100));
foreach ([30, 40, 50] as $q) {
$h->oshir($q);
}
30 normal
70 normal
120 OSHDI
SplObserver::update(SplSubject $subject) faqat manbani
uzatadi - qanday hodisa bo'lganini emas.
Shuning uchun kuzatuvchi:
instanceofbilan turni tekshirishga majbur- Manbadan kerakli ma'lumotni o'zi olishi kerak
- Bir necha hodisani ajrata olmaydi
Zamonaviy PHP kodida SplSubject deyarli ishlatilmaydi.
O'rniga hodisa obyekti uzatiladi - buni quyida ko'ramiz.
Hodisa obyekti bilan #
// Har hodisa - alohida sinf, o'z ma'lumotlari bilan
final readonly class BuyurtmaQabulQilindi
{
public function __construct(
public Buyurtma $buyurtma,
public string $vaqt,
) {}
}
final readonly class BuyurtmaBekorQilindi
{
public function __construct(
public Buyurtma $buyurtma,
public string $sabab,
) {}
}
final class HodisaTarqatuvchi
{
/** @var array<string, list<callable>> */
private array $tinglovchilar = [];
public function tingla(string $hodisa, callable $tinglovchi): self
{
$this->tinglovchilar[$hodisa][] = $tinglovchi;
return $this;
}
public function tarqat(object $hodisa): object
{
foreach ($this->tinglovchilar[$hodisa::class] ?? [] as $t) {
$t($hodisa);
}
return $hodisa;
}
}
$tarqatuvchi = (new HodisaTarqatuvchi())
->tingla(BuyurtmaQabulQilindi::class, static function ($h): void {
echo " [email] {$h->buyurtma->mijoz}, buyurtma qabul qilindi", PHP_EOL;
})
->tingla(BuyurtmaQabulQilindi::class, static function ($h): void {
echo " [ombor] #{$h->buyurtma->id} ajratildi ({$h->vaqt})", PHP_EOL;
})
->tingla(BuyurtmaBekorQilindi::class, static function ($h): void {
echo " [ombor] #{$h->buyurtma->id} qaytarildi: {$h->sabab}", PHP_EOL;
});
$b = new Buyurtma(7, 'Husanboy', 750000);
echo 'qabul:', PHP_EOL;
$tarqatuvchi->tarqat(new BuyurtmaQabulQilindi($b, '2025-01-15 10:30'));
echo 'bekor:', PHP_EOL;
$tarqatuvchi->tarqat(new BuyurtmaBekorQilindi($b, 'mijoz voz kechdi'));
qabul:
[email] Husanboy, buyurtma qabul qilindi
[ombor] #7 ajratildi (2025-01-15 10:30)
bekor:
[ombor] #7 qaytarildi: mijoz voz kechdi
Zamonaviy PHP loyihalarida shu yondashuv ishlatiladi:
| Element | Nima |
|---|---|
| Hodisa | Oddiy readonly obyekt |
| Tinglovchi | callable yoki bitta metodli sinf |
| Tarqatuvchi | Hodisa sinfiga qarab tinglovchilarni topadi |
Ustunliklari:
- Har hodisaning o'z ma'lumotlari va turi bor
instanceofkerak emas - tarqatuvchi o'zi ajratadi- Bir necha hodisa turi bemalol yashaydi
- Hodisani navbatga qo'yib keyinroq ishlash oson
Oxirgisi muhim: hodisa oddiy obyekt bo'lgani uchun uni serializatsiya qilib navbatga (Redis, RabbitMQ) yuborish mumkin. Shunda kuzatuvchilar asosiy so'rovdan keyin ishlaydi va foydalanuvchini kutdirmaydi.
Kuzatuvchi qachon noto'g'ri tanlov #
// Bu kod nima qiladi? Javob berish uchun 4 faylni ochish kerak
$tarqatuvchi = new HodisaTarqatuvchi();
echo 'tarqat(BuyurtmaQabulQilindi) -> ??? ', PHP_EOL;
echo 'tinglovchilar soni: ', 0, ' (hech kim obuna bolmagan)', PHP_EOL;
$tarqatuvchi->tarqat(new BuyurtmaQabulQilindi(
new Buyurtma(8, 'Test', 1000), '2025-01-15'));
echo 'hech narsa bolmadi va hech kim sezmadi', PHP_EOL;
tarqat(BuyurtmaQabulQilindi) -> ???
tinglovchilar soni: 0 (hech kim obuna bolmagan)
hech narsa bolmadi va hech kim sezmadi
Kodni o'qiyotgan odam $tarqatuvchi->tarqat(...) ni ko'radi
va nima bo'lishini bilmaydi. Buni aniqlash uchun:
- Barcha
tingla(BuyurtmaQabulQilindi::class, ...)chaqiruvlarini qidirish - Ular ro'yxatga olinadigan faylni topish
- Har tinglovchini o'qish
- Tartibini aniqlashga urinish
IDE "bu metodni kim chaqiradi" savoliga javob bera olmaydi.
| Belgi | Nima qilish |
|---|---|
| Tinglovchi soni 2-3 dan oshmaydi | Naqsh kerak emas bo'lishi mumkin |
| Tartib muhim | Naqsh noto'g'ri tanlov |
| Natija kerak | Naqsh noto'g'ri - hodisa javob qaytarmaydi |
| Yon ta'sirlar, soni o'zgaruvchan | Naqsh to'g'ri |
Eng oddiy tekshiruv: "bu ishni to'g'ridan-to'g'ri chaqirsam nima yomonlashadi?" Javob topilmasa - to'g'ridan-to'g'ri chaqiring.
- Beshta ishni bajaradigan bitta metod yozing.
- Uni kuzatuvchilarga ajrating.
- Yangi kuzatuvchi qo'shing - manba fayli o'zgardimi?
- Obunani bekor qilishni yozing va sinang.
- Bitta kuzatuvchida istisno tashlang - qolganlari ishladimi?
- Har kuzatuvchini
try/catchbilan himoyalang. - Qaysi ishlar majburiy, qaysilari yon ta'sir ekanini ajrating.
SplSubject/SplObserverbilan yozing va kamchiliklarini sanang.- Hodisa obyektli tarqatuvchini yozing.
- Kodingizda "kim tinglayapti" savoliga javob topib ko'ring - qancha vaqt ketdi?
Xulosa #
- Observer - hodisadan ko'p obyektni xabardor qiladi.
- Manba obunachilarni bilmaydi - faqat interfeysni biladi.
- Yangi ish qo'shish manba faylini ochmaydi.
- Bitta kuzatuvchidagi istisno qolganlarini to'xtatadi - har birini himoyalash kerak.
- Lekin himoya o'zi savol tug'diradi: qaysi ish majburiy?
- Majburiy qadamlar kuzatuvchi bo'lmasligi kerak - ular asosiy jarayonning qismi.
- Chaqirilish tartibi kutubxonaga qarab har xil - unga tayanmang.
- PHP ning
SplSubject/SplObserverinterfeyslari hodisa turini uzata olmaydi. - Zamonaviy yondashuv - hodisa obyekti va tinglovchilar (PSR-14).
- Eng katta narx - ko'rinmas oqim: chaqiruvga qarab nima bo'lishini bilib bo'lmaydi.
Keyingi bo'limda amalni obyektga aylantiradigan Command ni ko'ramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.