18-bo‘lim

Chain of Responsibility va Iterator

So'rovni ishlovchilar zanjiri bo'ylab uzatish va to'plamlarni aylanishni yashirish.

🕑 16 daqiqa o‘qish 📄 858 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Chain of Responsibility
  2. Yechim: zanjir
  3. Iterator
  4. Yechim: IteratorAggregate
  5. Generator bir marta ishlaydi
  6. Iterator va xotira
  7. yield from - iteratorlarni birlashtirish
  8. Xulosa

Bu bo'limda ikkita naqsh: Chain of Responsibility - so'rovni ishlovchilar zanjiri bo'ylab uzatadi; Iterator - to'plamni aylanish usulini yashiradi.

Chain of Responsibility #

Muammo: qo'llab-quvvatlash murojaatini kim ko'radi?

PHP
function murojaatniYonaltir(string $tur, int $ogirlik): string
{
    if ($tur === 'parol' && $ogirlik <= 1) {
        return 'bot: parolni tiklash havolasi yuborildi';
    }
    if ($ogirlik <= 2) {
        return 'operator: javob berdi';
    }
    if ($ogirlik <= 4) {
        return 'muhandis: tekshirmoqda';
    }
    return 'rahbar: shaxsan qaraydi';
}

foreach ([['parol', 1], ['tolov', 2], ['xato', 4], ['buzilish', 5]] as $m) {
    printf('%-10s %d -> %s%s', $m[0], $m[1],
        murojaatniYonaltir(...$m), PHP_EOL);
}
Natija
parol      1 -> bot: parolni tiklash havolasi yuborildi
tolov      2 -> operator: javob berdi
xato       4 -> muhandis: tekshirmoqda
buzilish   5 -> rahbar: shaxsan qaraydi
Bu funksiya butun tashkilotni biladi
MuammoIzoh
Bosqichlar tartibi kodda qotganYangi daraja o'rtaga qo'shilmaydi
Har daraja qoidasi bir joydaBot jamoasi operator qoidasini ko'radi
Zanjirni sozlamadan o'zgartirib bo'lmaydiKodni tahrirlash kerak
Bir darajani o'chirishYana kod o'zgaradi

Yechim: zanjir #

PHP
final readonly class Murojaat
{
    public function __construct(
        public string $tur,
        public int $ogirlik,
        public string $matn,
    ) {}
}

abstract class Ishlovchi
{
    private ?Ishlovchi $keyingi = null;

    public function keyin(Ishlovchi $k): Ishlovchi
    {
        $this->keyingi = $k;
        return $k;   // zanjirni qurishni osonlashtiradi
    }

    final public function ishla(Murojaat $m): string
    {
        if ($this->menikimi($m)) {
            return $this->bajar($m);
        }

        echo "  [{$this->nomi()}] menga tegishli emas, uzatdim", PHP_EOL;

        return $this->keyingi?->ishla($m)
            ?? 'HECH KIM QARAMADI: murojaat navbatda qoldi';
    }

    abstract public function nomi(): string;
    abstract protected function menikimi(Murojaat $m): bool;
    abstract protected function bajar(Murojaat $m): string;
}

final class Bot extends Ishlovchi
{
    public function nomi(): string { return 'bot'; }

    protected function menikimi(Murojaat $m): bool
    {
        return $m->tur === 'parol' && $m->ogirlik <= 1;
    }

    protected function bajar(Murojaat $m): string
    {
        return 'bot: parolni tiklash havolasi yuborildi';
    }
}

final class Operator extends Ishlovchi
{
    public function nomi(): string { return 'operator'; }

    protected function menikimi(Murojaat $m): bool
    {
        return $m->ogirlik <= 2;
    }

    protected function bajar(Murojaat $m): string
    {
        return "operator: \"{$m->matn}\" boyicha javob berdi";
    }
}

final class Muhandis extends Ishlovchi
{
    public function nomi(): string { return 'muhandis'; }

    protected function menikimi(Murojaat $m): bool
    {
        return $m->ogirlik <= 4;
    }

    protected function bajar(Murojaat $m): string
    {
        return 'muhandis: jurnallar tekshirilmoqda';
    }
}

function zanjirQur(): Ishlovchi
{
    $bot = new Bot();
    $bot->keyin(new Operator())->keyin(new Muhandis());

    return $bot;
}
PHP
$zanjir = zanjirQur();

foreach ([
    new Murojaat('parol', 1, 'parolni unutdim'),
    new Murojaat('tolov', 2, 'pul yechildi, buyurtma yoq'),
    new Murojaat('xato', 4, 'sayt ochilmayapti'),
] as $m) {
    printf('--- %s (ogirlik %d)%s', $m->tur, $m->ogirlik, PHP_EOL);
    echo $zanjir->ishla($m), PHP_EOL;
}
Natija
--- parol (ogirlik 1)
bot: parolni tiklash havolasi yuborildi
--- tolov (ogirlik 2)
  [bot] menga tegishli emas, uzatdim
operator: "pul yechildi, buyurtma yoq" boyicha javob berdi
--- xato (ogirlik 4)
  [bot] menga tegishli emas, uzatdim
  [operator] menga tegishli emas, uzatdim
muhandis: jurnallar tekshirilmoqda

Hech kim qabul qilmasa:

PHP
$zanjir = zanjirQur();
echo $zanjir->ishla(new Murojaat('buzilish', 5, 'butun tizim ishlamayapti')),
    PHP_EOL;
Natija
  [bot] menga tegishli emas, uzatdim
  [operator] menga tegishli emas, uzatdim
  [muhandis] menga tegishli emas, uzatdim
HECH KIM QARAMADI: murojaat navbatda qoldi
Zanjirning oxiri har doim o'ylanishi kerak

Yuqoridagi kodda ?? 'HECH KIM QARAMADI' bo'lmasa, null qaytar edi va chaqiruvchi kod tushunarsiz xato berardi.

Ikki yechim bor:

YechimQachon
Zanjir oxirida standart ishlovchiHar so'rov ishlanishi shart
null qaytarish va chaqiruvda tekshirishIshlanmasligi normal

Birinchisi xavfsizroq: Rahbar sinfi qo'shiladi va uning menikimi() si doim true qaytaradi.

Bu yerdagi jiddiy xato - zanjir oxirida hech narsa bo'lmasa va buni hech kim sezmasa. Murojaat yo'qoladi.

Zanjirni sozlamadan yig'ish:

PHP
$sozlama = ['operator', 'muhandis'];   // bot o'chirilgan

$xarita = [
    'bot' => static fn(): Ishlovchi => new Bot(),
    'operator' => static fn(): Ishlovchi => new Operator(),
    'muhandis' => static fn(): Ishlovchi => new Muhandis(),
];

$ishlovchilar = array_map(
    static fn(string $n): Ishlovchi => $xarita[$n](), $sozlama);

for ($i = 0; $i < count($ishlovchilar) - 1; $i++) {
    $ishlovchilar[$i]->keyin($ishlovchilar[$i + 1]);
}

echo 'zanjir: ', implode(' -> ', array_map(
    static fn(Ishlovchi $x): string => $x->nomi(), $ishlovchilar)), PHP_EOL;

echo $ishlovchilar[0]->ishla(new Murojaat('parol', 1, 'parol')), PHP_EOL;
Natija
zanjir: operator -> muhandis
operator: "parol" boyicha javob berdi

Bot o'chirilgani uchun oddiy parol murojaatini endi operator oldi - kodni o'zgartirmasdan.

Chain of Responsibility - so'rov zanjir bo'ylab yuradi Murojaat ogirlik: 4 Bot menikimi() = false Operator menikimi() = false Muhandis menikimi() = true standart ishlovchi Har halqa faqat o'zidan keyingisini biladi - butun zanjirni emas Iterator - aylanish usuli yashiringan Toplam massiv? fayl? baza? generator? tarmoq? Mijoz BILMAYDI va bilishi shart emas foreach ($t as $e) Bir xil sintaksis Xotira nazorat ostida Cheksiz ketma-ketlik mumkin Kechiktirilgan hisoblash PHP da Iterator IteratorAggregate Generator yield / yield from
Zanjir - so'rov uchun; iterator - ma'lumot uchun

Iterator #

Muammo: to'plamning ichki tuzilishi mijozga sizib chiqadi.

PHP
final class Katalog
{
    /** @var array<string, list<string>> */
    private array $bolimlar = [
        'texnika' => ['Klaviatura', 'Sichqoncha'],
        'mebel' => ['Stol', 'Stul'],
    ];

    /** @return array<string, list<string>> */
    public function bolimlar(): array
    {
        return $this->bolimlar;
    }
}

// Mijoz ichki tuzilishni bilishga majbur - ikki qavat halqa
foreach ((new Katalog())->bolimlar() as $bolim => $mahsulotlar) {
    foreach ($mahsulotlar as $m) {
        echo "  {$bolim}: {$m}", PHP_EOL;
    }
}
Natija
  texnika: Klaviatura
  texnika: Sichqoncha
  mebel: Stol
  mebel: Stul

Agar ichki tuzilma o'zgarsa (masalan uch qavatli bo'lsa), har chaqiruv joyi o'zgaradi.

Yechim: IteratorAggregate #

PHP
final class Katalog2 implements IteratorAggregate, Countable
{
    /** @var array<string, array<string, list<string>>> */
    private array $malumot = [
        'texnika' => [
            'kirish' => ['Klaviatura', 'Sichqoncha'],
            'chiqish' => ['Monitor'],
        ],
        'mebel' => [
            'ish' => ['Stol', 'Stul'],
        ],
    ];

    // Uch qavatli tuzilma - lekin tashqaridan bir qavat ko'rinadi
    public function getIterator(): Generator
    {
        foreach ($this->malumot as $bolim => $guruhlar) {
            foreach ($guruhlar as $guruh => $mahsulotlar) {
                foreach ($mahsulotlar as $m) {
                    yield "{$bolim}/{$guruh}" => $m;
                }
            }
        }
    }

    public function count(): int
    {
        return iterator_count($this->getIterator());
    }
}
PHP
$katalog = new Katalog2();

echo 'jami: ', count($katalog), PHP_EOL;

// Mijoz tuzilmani bilmaydi - oddiy foreach
foreach ($katalog as $yol => $mahsulot) {
    printf('  %-16s %s%s', $yol, $mahsulot, PHP_EOL);
}
Natija
jami: 5
  texnika/kirish   Klaviatura
  texnika/kirish   Sichqoncha
  texnika/chiqish  Monitor
  mebel/ish        Stol
  mebel/ish        Stul
Generator - PHP dagi eng qulay iterator

yield bo'lgan funksiya avtomatik Generator qaytaradi. Uni qo'lda Iterator interfeysini yozishdan afzal ko'ring:

Qo'lda IteratorGenerator
5 ta metod: current, key, next, rewind, validFaqat yield
Holatni qo'lda saqlashPHP o'zi saqlaydi
Ichma-ich halqalar qiyinTabiiy yoziladi
Qayta aylanish mumkinBir marta aylanadi

Oxirgi qator - generatorning yagona jiddiy cheklovi. Ikkinchi marta foreach qilib bo'lmaydi.

Generator bir marta ishlaydi #

PHP
function sonlar(): Generator
{
    yield 1;
    yield 2;
    yield 3;
}

$g = sonlar();
echo 'birinchi: ', implode(', ', iterator_to_array($g)), PHP_EOL;

// o'sha generatorni qayta aylanishga urinamiz
try {
    foreach ($g as $s) {
        echo $s;
    }
} catch (Throwable $x) {
    echo 'ikkinchi: ', get_class($x), ' - ', $x->getMessage(), PHP_EOL;
}

// Yechim: har safar yangi generator
echo 'yangi:    ', implode(', ', iterator_to_array(sonlar())), PHP_EOL;
Natija
birinchi: 1, 2, 3
ikkinchi: Exception - Cannot traverse an already closed generator
yangi:    1, 2, 3
Generatorni ikki marta ishlatib bo'lmaydi

iterator_to_array($g) generatorni oxirigacha aylanib yopdi. Undan keyin har qanday urinish Cannot traverse an already closed generator istisnosini beradi.

Qayta aylanish kerak bo'lsa:

YechimIzoh
Har safar yangi generator yaratishEng oddiy
IteratorAggregate ishlatishgetIterator() har chaqiruvda yangisini beradi
Natijani massivga yig'ishXotira ko'p ketadi

Katalog2 da aynan ikkinchi yo'l ishlatilgan - getIterator() har chaqiruvda yangi generator qaytaradi, shuning uchun uni istalgancha aylanish mumkin.

PHP
$katalog = new Katalog2();

echo 'birinchi aylanish: ', count(iterator_to_array($katalog)), ' ta', PHP_EOL;
echo 'ikkinchi aylanish: ', count(iterator_to_array($katalog)), ' ta', PHP_EOL;
Natija
birinchi aylanish: 3 ta
ikkinchi aylanish: 3 ta

Uch ta? Beshta bo'lishi kerak edi. Sabab - iterator_to_array kalitlarni ishlatadi va bir xil kalitlar bir-birini ustiga yozadi:

PHP
$katalog = new Katalog2();

echo 'kalitlar bilan: ', count(iterator_to_array($katalog)), PHP_EOL;
echo 'kalitsiz:       ', count(iterator_to_array($katalog, false)), PHP_EOL;

foreach (iterator_to_array($katalog) as $kalit => $qiymat) {
    printf('  %-16s %s%s', $kalit, $qiymat, PHP_EOL);
}
Natija
kalitlar bilan: 3
kalitsiz:       5
  texnika/kirish   Sichqoncha
  texnika/chiqish  Monitor
  mebel/ish        Stul
iterator_to_array ning ikkinchi argumenti

Standart qiymati true - kalitlar saqlanadi. Generator bir xil kalitni bir necha marta bersa, oxirgisi qoladi.

Klaviatura va Stol yo'qoldi, chunki ularning kalitlari keyingi elementlar bilan bir xil edi.

foreach da bunday muammo yo'q - u har elementni alohida beradi. Muammo faqat massivga aylantirganda paydo bo'ladi.

Amaliy qoida: generatorda takrorlanuvchi kalit bo'lishi mumkin bo'lsa, iterator_to_array($g, false) yozing.

Iterator va xotira #

PHP
// Massiv - hammasi xotirada
$boshlanish = memory_get_usage();
$massiv = range(1, 200000);
$massivXotira = memory_get_usage() - $boshlanish;
unset($massiv);

// Generator - bir vaqtda bitta element
function katta(int $n): Generator
{
    for ($i = 1; $i <= $n; $i++) {
        yield $i;
    }
}

$boshlanish = memory_get_usage();
$gen = katta(200000);
$jami = 0;
foreach ($gen as $s) {
    $jami += $s;
}
$genXotira = memory_get_usage() - $boshlanish;

printf('massiv:    %6d KB%s', intdiv($massivXotira, 1024), PHP_EOL);
printf('generator: %6d KB%s', intdiv($genXotira, 1024), PHP_EOL);
printf('yigindi bir xil: %s%s',
    $jami === array_sum(range(1, 200000)) ? 'ha' : 'yoq', PHP_EOL);
Natija
massiv:      6144 KB
generator:      0 KB
yigindi bir xil: ha
Bu naqshning eng amaliy foydasi

1 GB lik CSV faylni o'qish kerak bo'lsa:

YondashuvXotira
file() yoki file_get_contents()~1 GB
fgetcsv() + yieldBir qator

Generator bilan yozilgan o'quvchi cheksiz hajmdagi manbani ishlay oladi:

KOD
function qatorlar(string $fayl): Generator {
    $f = fopen($fayl, 'r');
    try {
        while (($q = fgetcsv($f)) !== false) {
            yield $q;
        }
    } finally {
        fclose($f);
    }
}

finally muhim: foreach yarmida break qilinsa ham fayl yopiladi.

yield from - iteratorlarni birlashtirish #

PHP
function texnika(): Generator
{
    yield 'Klaviatura';
    yield 'Sichqoncha';
}

function mebel(): Generator
{
    yield 'Stol';
    yield 'Stul';
}

function hammasi(): Generator
{
    yield from texnika();
    yield from mebel();
    yield 'Qalam';
}

foreach (hammasi() as $i => $m) {
    printf('  %d. %s%s', $i, $m, PHP_EOL);
}
Natija
  0. Klaviatura
  1. Sichqoncha
  0. Stol
  1. Stul
  0. Qalam
Kalitlar takrorlandi

yield from kalitlarni ham uzatadi. Har generator o'z kalitlarini 0 dan boshlaydi, tashqi generatorning o'z hisoblagichi esa alohida yuradi.

Natijada 0, 1, 0, 1, 0 chiqdi - beshta elementdan uchtasi 0 kalitiga ega.

Bu kutilmagan natija va ko'p xatoga sabab bo'ladi. Yechim - kalitlarni o'zingiz berish:

KOD
yield $nomer++ => $qiymat;

Yoki iterator_to_array($g, false) bilan kalitlarni tashlab yuborish.

Amaliy topshiriq
  1. To'rt darajali if zanjirini yozing.
  2. Uni ishlovchilar zanjiriga ajrating.
  3. Hech kim qabul qilmaydigan so'rov yuboring.
  4. Zanjir oxiriga standart ishlovchi qo'shing.
  5. Zanjirni sozlama massividan yig'ing va bitta darajani o'chiring.
  6. Ikki qavatli massivni IteratorAggregate bilan bir qavat qiling.
  7. Generatorni ikki marta aylanishga urinib ko'ring.
  8. iterator_to_array ni false bilan va false siz solishtiring.
  9. 200 000 elementli massiv va generator xotirasini o'lchang.
  10. yield from da kalitlar takrorlanishini ko'rsating va tuzating.

Xulosa #

  • Chain of Responsibility - so'rov ishlovchilar zanjiri bo'ylab yuradi.
  • Har halqa faqat o'zidan keyingisini biladi, butun zanjirni emas.
  • Zanjirni sozlamadan yig'ish mumkin - kod o'zgarmaydi.
  • Zanjir oxiri har doim o'ylanishi kerak, aks holda so'rov yo'qoladi.
  • Iterator to'plamning ichki tuzilishini yashiradi.
  • IteratorAggregate + Generator - PHP dagi eng qulay shakl.
  • Generator bir marta aylanadi; qayta kerak bo'lsa yangisini yarating.
  • iterator_to_array standart holda kalitlarni saqlaydi - takrorlansa element yo'qoladi.
  • Generator xotirani keskin tejaydi: 200 000 element uchun massiv ~8 MB, generator ~0.
  • yield from kalitlarni ham uzatadi - ular takrorlanishi mumkin.

Keyingi bo'limda naqshlarni noto'g'ri va ortiqcha ishlatish haqida gaplashamiz.

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.