18-bo‘lim
Chain of Responsibility va Iterator
So'rovni ishlovchilar zanjiri bo'ylab uzatish va to'plamlarni aylanishni yashirish.
Ushbu bo‘lim mundarijasi
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?
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);
}
parol 1 -> bot: parolni tiklash havolasi yuborildi
tolov 2 -> operator: javob berdi
xato 4 -> muhandis: tekshirmoqda
buzilish 5 -> rahbar: shaxsan qaraydi
| Muammo | Izoh |
|---|---|
| Bosqichlar tartibi kodda qotgan | Yangi daraja o'rtaga qo'shilmaydi |
| Har daraja qoidasi bir joyda | Bot jamoasi operator qoidasini ko'radi |
| Zanjirni sozlamadan o'zgartirib bo'lmaydi | Kodni tahrirlash kerak |
| Bir darajani o'chirish | Yana kod o'zgaradi |
Yechim: zanjir #
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;
}
$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;
}
--- 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:
$zanjir = zanjirQur();
echo $zanjir->ishla(new Murojaat('buzilish', 5, 'butun tizim ishlamayapti')),
PHP_EOL;
[bot] menga tegishli emas, uzatdim
[operator] menga tegishli emas, uzatdim
[muhandis] menga tegishli emas, uzatdim
HECH KIM QARAMADI: murojaat navbatda qoldi
Yuqoridagi kodda ?? 'HECH KIM QARAMADI' bo'lmasa, null
qaytar edi va chaqiruvchi kod tushunarsiz xato berardi.
Ikki yechim bor:
| Yechim | Qachon |
|---|---|
| Zanjir oxirida standart ishlovchi | Har so'rov ishlanishi shart |
null qaytarish va chaqiruvda tekshirish | Ishlanmasligi 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:
$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;
zanjir: operator -> muhandis
operator: "parol" boyicha javob berdi
Bot o'chirilgani uchun oddiy parol murojaatini endi operator oldi - kodni o'zgartirmasdan.
Iterator #
Muammo: to'plamning ichki tuzilishi mijozga sizib chiqadi.
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;
}
}
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 #
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());
}
}
$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);
}
jami: 5
texnika/kirish Klaviatura
texnika/kirish Sichqoncha
texnika/chiqish Monitor
mebel/ish Stol
mebel/ish Stul
Generator - PHP dagi eng qulay iteratoryield bo'lgan funksiya avtomatik Generator qaytaradi.
Uni qo'lda Iterator interfeysini yozishdan afzal ko'ring:
Qo'lda Iterator | Generator |
|---|---|
5 ta metod: current, key, next, rewind, valid | Faqat yield |
| Holatni qo'lda saqlash | PHP o'zi saqlaydi |
| Ichma-ich halqalar qiyin | Tabiiy yoziladi |
| Qayta aylanish mumkin | Bir marta aylanadi |
Oxirgi qator - generatorning yagona jiddiy cheklovi.
Ikkinchi marta foreach qilib bo'lmaydi.
Generator bir marta ishlaydi #
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;
birinchi: 1, 2, 3
ikkinchi: Exception - Cannot traverse an already closed generator
yangi: 1, 2, 3
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:
| Yechim | Izoh |
|---|---|
| Har safar yangi generator yaratish | Eng oddiy |
IteratorAggregate ishlatish | getIterator() har chaqiruvda yangisini beradi |
| Natijani massivga yig'ish | Xotira ko'p ketadi |
Katalog2 da aynan ikkinchi yo'l ishlatilgan - getIterator()
har chaqiruvda yangi generator qaytaradi, shuning uchun uni
istalgancha aylanish mumkin.
$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;
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:
$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);
}
kalitlar bilan: 3
kalitsiz: 5
texnika/kirish Sichqoncha
texnika/chiqish Monitor
mebel/ish Stul
iterator_to_array ning ikkinchi argumentiStandart 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 #
// 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);
massiv: 6144 KB
generator: 0 KB
yigindi bir xil: ha
1 GB lik CSV faylni o'qish kerak bo'lsa:
| Yondashuv | Xotira |
|---|---|
file() yoki file_get_contents() | ~1 GB |
fgetcsv() + yield | Bir qator |
Generator bilan yozilgan o'quvchi cheksiz hajmdagi manbani ishlay oladi:
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 #
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);
}
0. Klaviatura
1. Sichqoncha
0. Stol
1. Stul
0. Qalam
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:
yield $nomer++ => $qiymat;
Yoki iterator_to_array($g, false) bilan kalitlarni
tashlab yuborish.
- To'rt darajali
ifzanjirini yozing. - Uni ishlovchilar zanjiriga ajrating.
- Hech kim qabul qilmaydigan so'rov yuboring.
- Zanjir oxiriga standart ishlovchi qo'shing.
- Zanjirni sozlama massividan yig'ing va bitta darajani o'chiring.
- Ikki qavatli massivni
IteratorAggregatebilan bir qavat qiling. - Generatorni ikki marta aylanishga urinib ko'ring.
iterator_to_arraynifalsebilan vafalsesiz solishtiring.- 200 000 elementli massiv va generator xotirasini o'lchang.
yield fromda 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_arraystandart holda kalitlarni saqlaydi - takrorlansa element yo'qoladi.- Generator xotirani keskin tejaydi: 200 000 element uchun massiv ~8 MB, generator ~0.
yield fromkalitlarni ham uzatadi - ular takrorlanishi mumkin.
Keyingi bo'limda naqshlarni noto'g'ri va ortiqcha ishlatish haqida gaplashamiz.
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.