9-bo‘lim

Decorator

Interfeysni saqlagan holda xulq qo'shish - va qatlamlar tartibi nima uchun muhim.

🕑 13 daqiqa o‘qish 📄 790 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo: vorislar portlashi
  2. Yechim: o'rovchi qatlamlar
  3. Ikkinchi va uchinchi qatlam
  4. Tartib muhim
  5. Dekoratorni dinamik yig'ish
  6. PHP ning o'zida Decorator
  7. Naqshning narxi
  8. Xulosa

Decorator - obyektni o'sha interfeysdagi boshqa obyekt bilan o'rab, unga yangi xulq qo'shish naqshi.

Adapter interfeysni o'zgartiradi, Decorator esa uni saqlaydi. Shu sababli dekoratorlarni bir-birining ustiga xohlaganingizcha qo'yish mumkin.

Muammo: vorislar portlashi #

Bizda SMS yuboruvchi bor. Unga jurnal qo'shish kerak:

PHP
interface Yuboruvchi
{
    public function yubor(string $manzil, string $matn): bool;
}

// `final` emas - quyida uni ataylab meros bilan kengaytiramiz
class AsosiySms implements Yuboruvchi
{
    public function yubor(string $manzil, string $matn): bool
    {
        echo "    -> {$manzil}: {$matn}", PHP_EOL;
        return true;
    }
}
PHP
// Meros bilan: jurnal qo'shamiz
final class JurnalliSms extends AsosiySms
{
    public function yubor(string $manzil, string $matn): bool
    {
        echo "  [jurnal] boshlandi", PHP_EOL;
        $natija = parent::yubor($manzil, $matn);
        echo "  [jurnal] natija: ", $natija ? 'ok' : 'xato', PHP_EOL;
        return $natija;
    }
}

(new JurnalliSms())->yubor('+998901112233', 'Salom');
Natija
  [jurnal] boshlandi
    -> +998901112233: Salom
  [jurnal] natija: ok

Ishladi. Endi qayta urinish ham kerak. Keyin tezlik chegarasi. Keyin shifrlash.

Vorislar soni ikki darajali o'sadi

Uchta qo'shimcha xulq uchun barcha kombinatsiyalarni yozish kerak:

KombinatsiyaSinf nomi
jurnalJurnalliSms
qayta urinishQaytaSms
chegaraChegaraliSms
jurnal + qayta urinishJurnalliQaytaSms
jurnal + chegaraJurnalliChegaraliSms
qayta urinish + chegaraQaytaChegaraliSms
uchalasiJurnalliQaytaChegaraliSms

7 ta sinf uchta xulq uchun. To'rtinchi xulq qo'shilsa - 15 ta. Formulasi: 2ⁿ - 1.

Ustiga - bularning hammasi AsosiySms dan meros oladi. Agar Telegram yuboruvchi ham kerak bo'lsa, hammasi ikki barobar ko'payadi.

Bu sinflar portlashi deb ataladi.

Yechim: o'rovchi qatlamlar #

Dekorator ham Yuboruvchi, lekin ichida boshqa Yuboruvchi ni saqlaydi:

PHP
final class JurnalQatlami implements Yuboruvchi
{
    public function __construct(private readonly Yuboruvchi $ichki) {}

    public function yubor(string $manzil, string $matn): bool
    {
        echo "  [jurnal] boshlandi: {$manzil}", PHP_EOL;
        $natija = $this->ichki->yubor($manzil, $matn);
        echo '  [jurnal] natija: ', $natija ? 'ok' : 'xato', PHP_EOL;

        return $natija;
    }
}
PHP
$y = new JurnalQatlami(new AsosiySms());
$y->yubor('+998901112233', 'Salom');
Natija
  [jurnal] boshlandi: +998901112233
    -> +998901112233: Salom
  [jurnal] natija: ok

Natija bir xil, lekin muhim farq bor: JurnalQatlami AsosiySms haqida hech narsa bilmaydi. U faqat Yuboruvchi interfeysini biladi.

Demak uni istalgan yuboruvchi ustiga qo'yish mumkin.

Ikkinchi va uchinchi qatlam #

PHP
// Ataylab xato beruvchi yuboruvchi - qayta urinishni sinash uchun
final class NosozSms implements Yuboruvchi
{
    private int $urinish = 0;

    public function __construct(private readonly int $nechtaXato) {}

    public function yubor(string $manzil, string $matn): bool
    {
        $this->urinish++;
        if ($this->urinish <= $this->nechtaXato) {
            echo "    -> urinish {$this->urinish}: XATO", PHP_EOL;
            return false;
        }
        echo "    -> urinish {$this->urinish}: yuborildi", PHP_EOL;
        return true;
    }
}

final class QaytaUrinish implements Yuboruvchi
{
    public function __construct(
        private readonly Yuboruvchi $ichki,
        private readonly int $marta = 3,
    ) {}

    public function yubor(string $manzil, string $matn): bool
    {
        for ($i = 1; $i <= $this->marta; $i++) {
            if ($this->ichki->yubor($manzil, $matn)) {
                return true;
            }
            echo "  [qayta] {$i}-urinish muvaffaqiyatsiz", PHP_EOL;
        }
        return false;
    }
}

final class TezlikChegarasi implements Yuboruvchi
{
    private int $yuborilgan = 0;

    public function __construct(
        private readonly Yuboruvchi $ichki,
        private readonly int $chegara,
    ) {}

    public function yubor(string $manzil, string $matn): bool
    {
        if ($this->yuborilgan >= $this->chegara) {
            echo "  [chegara] limit tugadi ({$this->chegara})", PHP_EOL;
            return false;
        }
        $this->yuborilgan++;

        return $this->ichki->yubor($manzil, $matn);
    }
}
PHP
// Ikki marta xato beradi, uchinchisida yuboradi
$y = new QaytaUrinish(new NosozSms(2), 4);

$natija = $y->yubor('+998901112233', 'Salom');
echo 'natija: ', $natija ? 'ok' : 'xato', PHP_EOL;
Natija
    -> urinish 1: XATO
  [qayta] 1-urinish muvaffaqiyatsiz
    -> urinish 2: XATO
  [qayta] 2-urinish muvaffaqiyatsiz
    -> urinish 3: yuborildi
natija: ok

Endi uchalasini birga qo'yamiz:

PHP
$y = new JurnalQatlami(
        new TezlikChegarasi(
            new QaytaUrinish(new NosozSms(1), 3),
            2,
        ),
     );

foreach (['+998901112233', '+998935556677', '+998977778899'] as $m) {
    echo "--- {$m}", PHP_EOL;
    $y->yubor($m, 'Test');
}
Natija
--- +998901112233
  [jurnal] boshlandi: +998901112233
    -> urinish 1: XATO
  [qayta] 1-urinish muvaffaqiyatsiz
    -> urinish 2: yuborildi
  [jurnal] natija: ok
--- +998935556677
  [jurnal] boshlandi: +998935556677
    -> urinish 3: yuborildi
  [jurnal] natija: ok
--- +998977778899
  [jurnal] boshlandi: +998977778899
  [chegara] limit tugadi (2)
  [jurnal] natija: xato
Decorator - piyoz qatlamlari JurnalQatlami TezlikChegarasi QaytaUrinish AsosiySms yubor() 1. jurnal 2. chegara 3. qayta urinish 4. haqiqiy ish Har qatlam bir xil interfeysni bajaradi - shuning uchun ustma-ust qo'yiladi Tartib natijani o'zgartiradi Chegara(Qayta(sms), 2) → 3 urinish = 1 limit birligi (qayta urinishlar limitga kirmaydi) Qayta(Chegara(sms, 2), 3) → 3 urinish = 3 limit birligi (har urinish limitni yeydi)
Qatlamlar tartibi - dekoratorning eng nozik joyi

Tartib muhim #

PHP
// A: chegara tashqarida - qayta urinishlar limitga kirmaydi
echo '=== Chegara(Qayta(...))', PHP_EOL;
$a = new TezlikChegarasi(new QaytaUrinish(new NosozSms(2), 4), 2);
var_dump($a->yubor('+998901112233', 'Salom'));

// B: chegara ichkarida - har urinish limitni yeydi
echo '=== Qayta(Chegara(...))', PHP_EOL;
$b = new QaytaUrinish(new TezlikChegarasi(new NosozSms(2), 2), 4);
var_dump($b->yubor('+998901112233', 'Salom'));
Natija
=== Chegara(Qayta(...))
    -> urinish 1: XATO
  [qayta] 1-urinish muvaffaqiyatsiz
    -> urinish 2: XATO
  [qayta] 2-urinish muvaffaqiyatsiz
    -> urinish 3: yuborildi
bool(true)
=== Qayta(Chegara(...))
    -> urinish 1: XATO
  [qayta] 1-urinish muvaffaqiyatsiz
    -> urinish 2: XATO
  [qayta] 2-urinish muvaffaqiyatsiz
  [chegara] limit tugadi (2)
  [qayta] 3-urinish muvaffaqiyatsiz
  [chegara] limit tugadi (2)
  [qayta] 4-urinish muvaffaqiyatsiz
bool(false)
Bir xil qatlamlar, boshqa natija

Ikkala holatda ham aynan uchta sinf ishlatildi. Faqat tartib boshqacha - va natija true dan false ga o'zgardi.

TartibMa'nosi
Chegara(Qayta(...))Chegara mantiqiy xabarlarni sanaydi
Qayta(Chegara(...))Chegara har urinishni sanaydi

Ikkalasi ham to'g'ri bo'lishi mumkin - bu talabga bog'liq. Muhimi shuki, tartibni bilib turib tanlash kerak.

Amaliy tavsiya:

QatlamOdatda qayerda
Jurnal / o'lchovEng tashqarida (hamma narsani ko'rish uchun)
KeshTashqariroq (keraksiz ishni oldini oladi)
Qayta urinishIchkariroq (faqat haqiqiy chaqiruvni takrorlaydi)
Xavfsizlik tekshiruviEng tashqarida

Dekoratorni dinamik yig'ish #

Qatlamlar ro'yxatdan yig'ilishi mumkin:

PHP
/**
 * @param list<callable(Yuboruvchi): Yuboruvchi> $qatlamlar
 */
function qatlamlarniYig(Yuboruvchi $asos, array $qatlamlar): Yuboruvchi
{
    // teskari tartibda o'raymiz: ro'yxatdagi birinchisi eng tashqarida bo'ladi
    foreach (array_reverse($qatlamlar) as $qatlam) {
        $asos = $qatlam($asos);
    }
    return $asos;
}

$sozlama = ['jurnal', 'chegara'];

$xarita = [
    'jurnal'  => fn(Yuboruvchi $y): Yuboruvchi => new JurnalQatlami($y),
    'chegara' => fn(Yuboruvchi $y): Yuboruvchi => new TezlikChegarasi($y, 1),
    'qayta'   => fn(Yuboruvchi $y): Yuboruvchi => new QaytaUrinish($y, 2),
];

$y = qatlamlarniYig(new AsosiySms(),
    array_map(fn(string $n): callable => $xarita[$n], $sozlama));

$y->yubor('+998901112233', 'Birinchi');
$y->yubor('+998935556677', 'Ikkinchi');
Natija
  [jurnal] boshlandi: +998901112233
    -> +998901112233: Birinchi
  [jurnal] natija: ok
  [jurnal] boshlandi: +998935556677
  [chegara] limit tugadi (1)
  [jurnal] natija: xato
Bu - PSR-15 middleware ning aynan o'zi

HTTP freymvorklardagi middleware - Decorator naqshining eng mashhur qo'llanilishi.

Har middleware so'rovni oladi, biror ish qiladi, keyingisiga uzatadi, javobni oladi va yana biror ish qiladi:

MiddlewareDekorator qatlami
CORS sarlavhalariJavobni o'zgartiradi
AutentifikatsiyaIchkariga o'tkazmasligi mumkin
JurnalIkkala tomonni yozadi
GzipJavobni siqadi

Sozlamalar faylidagi middleware ro'yxati - yuqoridagi $sozlama massivining aynan o'zi. Va u yerda ham tartib muhim.

PHP ning o'zida Decorator #

PHP
// Oqim (stream) filtrlari - o'rnatilgan dekoratorlar
$fayl = tempnam(sys_get_temp_dir(), 'dec');
file_put_contents($fayl, "birinchi\nikkinchi\nuchinchi\n");

// oddiy o'qish
echo '--- oddiy', PHP_EOL;
echo file_get_contents($fayl);

// filtr qatlami bilan o'qish
echo '--- katta harf filtri', PHP_EOL;
echo file_get_contents('php://filter/read=string.toupper/resource=' . $fayl);

// ikkita filtr ketma-ket
echo '--- toupper + rot13', PHP_EOL;
echo file_get_contents(
    'php://filter/read=string.toupper|string.rot13/resource=' . $fayl);

unlink($fayl);
Natija
--- oddiy
birinchi
ikkinchi
uchinchi
--- katta harf filtri
BIRINCHI
IKKINCHI
UCHINCHI
--- toupper + rot13
OVEVAPUV
VXXVAPUV
HPUVAPUV

Filtrlar | bilan zanjirlanadi va ular ham tartibga bog'liq - aynan dekorator kabi.

Naqshning narxi #

PHP
$chuqur = new JurnalQatlami(
            new TezlikChegarasi(
              new QaytaUrinish(
                new JurnalQatlami(new AsosiySms()), 2), 5));

echo 'tashqi tur: ', $chuqur::class, PHP_EOL;
echo 'AsosiySms mi: ', $chuqur instanceof AsosiySms ? 'ha' : 'yoq', PHP_EOL;
echo 'Yuboruvchi mi: ', $chuqur instanceof Yuboruvchi ? 'ha' : 'yoq', PHP_EOL;
Natija
tashqi tur: JurnalQatlami
AsosiySms mi: yoq
Yuboruvchi mi: ha
instanceof va konkret tur yo'qoladi

O'ralgan obyekt endi AsosiySms emas. Agar kodingizda instanceof AsosiySms yoki $obj::class === 'AsosiySms' tekshiruvi bo'lsa - u buziladi.

Boshqa narxlar:

NarxIzoh
Chaqiruv steki chuqurlashadiNosozlik qidirish qiyinlashadi
Obyektlar soni ko'payadiHar qatlam alohida obyekt
Qaysi qatlam nima qilganini topishJurnalsiz deyarli imkonsiz
Interfeysga metod qo'shishHar dekoratorga qo'shish kerak

Oxirgisi eng og'riqli: Yuboruvchi ga holat() metodi qo'shsangiz, uni beshta dekoratorda ham yozish kerak - va ularning barchasi shunchaki $this->ichki->holat() deydi.

Shu sababli dekorator uchun interfeys kichik bo'lishi kerak (2-bo'limdagi I tamoyili).

Amaliy topshiriq
  1. Bir interfeysni bajaruvchi asosiy sinf yozing.
  2. Uchta xulqni meros bilan qo'shsangiz, nechta sinf kerakligini sanang.
  3. Uchta dekorator yozing: jurnal, qayta urinish, chegara.
  4. Ularni turli tartibda birlashtiring.
  5. Tartib natijani o'zgartirgan holatni toping va sababini yozing.
  6. Qatlamlarni ro'yxatdan dinamik yig'uvchi funksiya yozing.
  7. php://filter bilan ikkita filtrni zanjirlang.
  8. Filtrlar tartibini almashtiring - natija o'zgardimi?
  9. O'ralgan obyektda instanceof ni tekshiring.
  10. Interfeysga yangi metod qo'shing va nechta faylni tahrirlaganingizni sanang.

Xulosa #

  • Decorator - interfeysni saqlagan holda xulq qo'shadi.
  • Meros bilan qilinsa, sinflar soni 2ⁿ - 1 ga o'sadi.
  • Dekorator o'zi ham interfeysni bajaradi va ichida o'sha interfeysdagi obyektni saqlaydi.
  • Shu sababli qatlamlarni cheksiz ustma-ust qo'yish mumkin.
  • Qatlam o'ralgan obyektning konkret turini bilmaydi.
  • Tartib natijani o'zgartiradi - buni bilib turib tanlash kerak.
  • Jurnal va xavfsizlik odatda tashqarida, qayta urinish ichkarida turadi.
  • Qatlamlar ro'yxatdan dinamik yig'ilishi mumkin - bu PSR-15 middleware.
  • PHP ning php://filter oqim filtrlari - tilga o'rnatilgan dekorator.
  • Narxi: instanceof yo'qoladi, stek chuqurlashadi, interfeysga metod qo'shish qimmat.

Keyingi bo'limda murakkab tizimga oddiy kirish nuqtasi beradigan Facade 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.