14-bo‘lim

Observer

Bir hodisadan ko'p obyektni xabardor qilish - va bu qaysi muammolarni keltiradi.

🕑 15 daqiqa o‘qish 📄 899 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo: bitta metod hammani biladi
  2. Yechim: obuna ro'yxati
  3. Birinchi muammo: bitta istisno hammasini to'xtatadi
  4. Tartib kafolatlanmaydi
  5. PHP dagi SplSubject va SplObserver
  6. Hodisa obyekti bilan
  7. Kuzatuvchi qachon noto'g'ri tanlov
  8. Xulosa

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:

PHP
final readonly class Buyurtma
{
    public function __construct(
        public int $id,
        public string $mijoz,
        public int $summa,
    ) {}
}
PHP
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));
Natija
buyurtma #1 qabul qilindi
  [email] Husanboy ga xat yuborildi
  [sms] tasdiq kodi yuborildi
  [ombor] mahsulot ajratildi
  [statistika] +500000 som
  [jurnal] buyurtma #1
Bu metod hech qachon to'xtamaydi

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:

MuammoOqibati
BuyurtmaXizmati 5 ta tizimni biladiHar biri o'zgarsa - shu fayl o'zgaradi
Yangi ish = shu metodni ochishOchiq/yopiq buziladi
Sinash uchun 5 ta soxta obyekt kerakTest og'irlashadi
Xat yuborish tushib qolsaButun buyurtma yiqiladi

Oxirgisi eng jiddiy. Marketing xabari yuborilmagani uchun mijoz buyurtma bera olmasligi - mantiqsiz.

Yechim: obuna ro'yxati #

PHP
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;
    }
}
PHP
$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));
Natija
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:

PHP
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));
Natija
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
Observer - manba obunachilarni bilmaydi BuyurtmaManbasi obuna() / bekor() qabul() → xabar berish faqat interfeysni biladi xabar($b) EmailKuzatuvchi OmborKuzatuvchi StatistikaKuzatuv... Telegram... yangi obunachi qo'shish manbani ochmaydi Xavf Biri istisno tashlasa qolganlari ishlamay qoladi Naqshning ikki yuzi Foyda Manba obunachilarni bilmaydi Yangi ish - yangi sinf Har biri alohida sinaladi Ish vaqtida obuna/bekor Narx Oqimni kuzatish qiyinlashadi Tartib kafolatlanmaydi Istisno zanjirni uzadi Obuna bekor qilinmasa - xotira
Bo'shashgan bog'lanishning narxi - ko'rinmas oqim

Birinchi muammo: bitta istisno hammasini to'xtatadi #

PHP
final class NosozKuzatuvchi implements Kuzatuvchi
{
    public function nomi(): string { return 'nosoz'; }

    public function xabar(Buyurtma $b): void
    {
        throw new RuntimeException('SMTP server javob bermayapti');
    }
}
PHP
$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;
}
Natija
buyurtma #5 qabul qilindi
YIQILDI: SMTP server javob bermayapti

Ombor hech narsa ajratmadi, statistika hisoblamadi - chunki xat yuborilmadi.

Yechim - har kuzatuvchini alohida himoyalash:

PHP
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;
    }
}
PHP
$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;
}
Natija
buyurtma #5 qabul qilindi
  [!] nosoz xato berdi, davom etamiz
  [ombor] #5 uchun mahsulot ajratildi
  [statistika] jami: 400000 som
--- xatolar
  nosoz: SMTP server javob bermayapti
Lekin bu ham to'g'ri javob emas

Endi hamma kuzatuvchi ishladi. Ammo yangi savol tug'iladi: qaysi ishlar majburiy, qaysilari emas?

KuzatuvchiYiqilsa nima bo'lishi kerak
Ombor mahsulot ajratishiButun buyurtma bekor bo'lishi kerak
To'lov yozuviBekor
Marketing xatiE'tiborsiz qoldirilsin
StatistikaE'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 #

PHP
$manba = (new BuyurtmaManbasi())->obuna(
    new StatistikaKuzatuvchi(),
    new EmailKuzatuvchi(),
    new OmborKuzatuvchi(),
);

$manba->qabul(new Buyurtma(6, 'Dilnoza', 250000));
Natija
buyurtma #6 qabul qilindi
  [statistika] jami: 250000 som
  [email] Dilnoza ga xat yuborildi
  [ombor] #6 uchun mahsulot ajratildi
Tartib obuna tartibiga bog'liq

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:

KutubxonaTartib
Symfony EventDispatcherUstuvorlik raqami bo'yicha
Laravel EventsRo'yxatga olish tartibi
PSR-14Ta'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:

PHP
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);
}
Natija
     30  normal
     70  normal
    120  OSHDI
Standart interfeyslarning kamchiligi

SplObserver::update(SplSubject $subject) faqat manbani uzatadi - qanday hodisa bo'lganini emas.

Shuning uchun kuzatuvchi:

  1. instanceof bilan turni tekshirishga majbur
  2. Manbadan kerakli ma'lumotni o'zi olishi kerak
  3. Bir necha hodisani ajrata olmaydi

Zamonaviy PHP kodida SplSubject deyarli ishlatilmaydi. O'rniga hodisa obyekti uzatiladi - buni quyida ko'ramiz.

Hodisa obyekti bilan #

PHP
// 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;
    }
}
PHP
$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'));
Natija
qabul:
  [email] Husanboy, buyurtma qabul qilindi
  [ombor] #7 ajratildi (2025-01-15 10:30)
bekor:
  [ombor] #7 qaytarildi: mijoz voz kechdi
Bu PSR-14 ning soddalashtirilgan shakli

Zamonaviy PHP loyihalarida shu yondashuv ishlatiladi:

ElementNima
HodisaOddiy readonly obyekt
Tinglovchicallable yoki bitta metodli sinf
TarqatuvchiHodisa sinfiga qarab tinglovchilarni topadi

Ustunliklari:

  • Har hodisaning o'z ma'lumotlari va turi bor
  • instanceof kerak 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 #

PHP
// 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;
Natija
tarqat(BuyurtmaQabulQilindi) -> ??? 
tinglovchilar soni: 0 (hech kim obuna bolmagan)
hech narsa bolmadi va hech kim sezmadi
Observer ning eng katta narxi - ko'rinmas oqim

Kodni o'qiyotgan odam $tarqatuvchi->tarqat(...) ni ko'radi va nima bo'lishini bilmaydi. Buni aniqlash uchun:

  1. Barcha tingla(BuyurtmaQabulQilindi::class, ...) chaqiruvlarini qidirish
  2. Ular ro'yxatga olinadigan faylni topish
  3. Har tinglovchini o'qish
  4. Tartibini aniqlashga urinish

IDE "bu metodni kim chaqiradi" savoliga javob bera olmaydi.

BelgiNima qilish
Tinglovchi soni 2-3 dan oshmaydiNaqsh kerak emas bo'lishi mumkin
Tartib muhimNaqsh noto'g'ri tanlov
Natija kerakNaqsh noto'g'ri - hodisa javob qaytarmaydi
Yon ta'sirlar, soni o'zgaruvchanNaqsh 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.

Amaliy topshiriq
  1. Beshta ishni bajaradigan bitta metod yozing.
  2. Uni kuzatuvchilarga ajrating.
  3. Yangi kuzatuvchi qo'shing - manba fayli o'zgardimi?
  4. Obunani bekor qilishni yozing va sinang.
  5. Bitta kuzatuvchida istisno tashlang - qolganlari ishladimi?
  6. Har kuzatuvchini try/catch bilan himoyalang.
  7. Qaysi ishlar majburiy, qaysilari yon ta'sir ekanini ajrating.
  8. SplSubject/SplObserver bilan yozing va kamchiliklarini sanang.
  9. Hodisa obyektli tarqatuvchini yozing.
  10. 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/SplObserver interfeyslari 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.

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.