11-bo‘lim

Composite

Bitta element bilan ham, butun daraxt bilan ham bir xil ishlash.

🕑 12 daqiqa o‘qish 📄 686 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo: mijoz turlarni ajratishga majbur
  2. Yechim: umumiy interfeys
  3. Yangi amal qo'shish
  4. Xavfsizlik va shaffoflik
  5. Boshqa misol: chegirmalar daraxti
  6. Narxi va cheklovlari
  7. Xulosa

Composite - daraxtsimon tuzilmalarni qurish naqshi. Uning asosiy va'dasi: bitta element bilan ham, butun shox bilan ham mijoz kod bir xil ishlaydi.

Muammo: mijoz turlarni ajratishga majbur #

Fayl tizimini modellashtiramiz. Ikki xil narsa bor: fayl va papka.

PHP
final class OddiyFayl
{
    public function __construct(
        public string $nom,
        public int $hajm,
    ) {}
}

final class OddiyPapka
{
    /** @var list<OddiyFayl|OddiyPapka> */
    public array $ichi = [];

    public function __construct(public string $nom) {}
}

// Mijoz kod har safar turini tekshirishi kerak
function hajmHisobla(OddiyFayl|OddiyPapka $narsa): int
{
    if ($narsa instanceof OddiyFayl) {
        return $narsa->hajm;
    }

    $jami = 0;
    foreach ($narsa->ichi as $bola) {
        $jami += hajmHisobla($bola);   // turini yana tekshiradi
    }
    return $jami;
}

$ildiz = new OddiyPapka('loyiha');
$ildiz->ichi[] = new OddiyFayl('index.php', 1200);
$src = new OddiyPapka('src');
$src->ichi[] = new OddiyFayl('Model.php', 3400);
$src->ichi[] = new OddiyFayl('View.php', 2100);
$ildiz->ichi[] = $src;

echo 'jami hajm: ', hajmHisobla($ildiz), ' bayt', PHP_EOL;
Natija
jami hajm: 6700 bayt
instanceof har amalda takrorlanadi

Kod ishlaydi. Lekin endi yana beshta amal kerak bo'lsin: nomlarni chiqarish, qidirish, sanash, nusxalash, o'chirish.

Har birida yana o'sha instanceof yoziladi:

Amalinstanceof bormi
hajmHisoblaHa
nomlarniChiqarHa
qidirHa
fayllarSoniHa

Uchinchi tur qo'shilsa (masalan Yorliq), hammasini tahrirlash kerak - va bittasi unutiladi.

Bu 1-bo'limdagi if zanjirining daraxt ustidagi shakli.

Yechim: umumiy interfeys #

PHP
interface TizimElementi
{
    public function nomi(): string;
    public function hajm(): int;
    public function korsat(int $chuqurlik = 0): string;
}

final class Fayl implements TizimElementi
{
    public function __construct(
        private readonly string $nom,
        private readonly int $hajm,
    ) {}

    public function nomi(): string { return $this->nom; }

    public function hajm(): int { return $this->hajm; }

    public function korsat(int $chuqurlik = 0): string
    {
        return sprintf('%s%-20s %6d b',
            str_repeat('  ', $chuqurlik), $this->nom, $this->hajm);
    }
}

final class Papka implements TizimElementi
{
    /** @var list<TizimElementi> */
    private array $ichi = [];

    public function __construct(private readonly string $nom) {}

    public function qosh(TizimElementi ...$elementlar): self
    {
        foreach ($elementlar as $e) {
            $this->ichi[] = $e;
        }
        return $this;
    }

    public function nomi(): string { return $this->nom; }

    // Bola fayl ham, papka ham bo'lishi mumkin - farqi yo'q
    public function hajm(): int
    {
        $jami = 0;
        foreach ($this->ichi as $bola) {
            $jami += $bola->hajm();
        }
        return $jami;
    }

    public function korsat(int $chuqurlik = 0): string
    {
        $qatorlar = [sprintf('%s%-20s %6d b (%d ta)',
            str_repeat('  ', $chuqurlik), $this->nom . '/',
            $this->hajm(), count($this->ichi))];

        foreach ($this->ichi as $bola) {
            $qatorlar[] = $bola->korsat($chuqurlik + 1);
        }

        return implode("\n", $qatorlar);
    }

    /** @return list<TizimElementi> */
    public function ichi(): array
    {
        return $this->ichi;
    }
}

function daraxtQur(): Papka
{
    return (new Papka('loyiha'))->qosh(
        new Fayl('index.php', 1200),
        new Fayl('README.md', 800),
        (new Papka('src'))->qosh(
            new Fayl('Model.php', 3400),
            new Fayl('View.php', 2100),
            (new Papka('Support'))->qosh(
                new Fayl('Markdown.php', 9600),
                new Fayl('Str.php', 1500),
            ),
        ),
        (new Papka('bosh'))->qosh(),
    );
}
PHP
echo daraxtQur()->korsat(), PHP_EOL;
Natija
loyiha/               18600 b (4 ta)
  index.php              1200 b
  README.md               800 b
  src/                  16600 b (3 ta)
    Model.php              3400 b
    View.php               2100 b
    Support/              11100 b (2 ta)
      Markdown.php           9600 b
      Str.php                1500 b
  bosh/                     0 b (0 ta)

Endi mijoz kod turni umuman bilmaydi:

PHP
function hisobotYoz(TizimElementi $e): string
{
    // instanceof yo'q - fayl ham, papka ham bir xil ishlanadi
    return sprintf('%-12s %6d bayt', $e->nomi(), $e->hajm());
}

$daraxt = daraxtQur();

echo hisobotYoz($daraxt), PHP_EOL;
echo hisobotYoz(new Fayl('bitta.txt', 42)), PHP_EOL;

foreach ($daraxt->ichi() as $bola) {
    echo hisobotYoz($bola), PHP_EOL;
}
Natija
loyiha        18600 bayt
bitta.txt        42 bayt
index.php      1200 bayt
README.md       800 bayt
src           16600 bayt
bosh              0 bayt
Composite - barg va shox bir xil interfeysda TizimElementi nomi() · hajm() · korsat() Fayl (barg) hajm() = o'z hajmi Papka (shox) hajm() = bolalar yig'indisi bolalar Rekursiya papkaning ichida yashiringan loyiha/ index.php README.md src/ Model.php Support/
Mijoz ildizga murojaat qiladi - qolgani o'z-o'zidan bo'ladi

Yangi amal qo'shish #

PHP
// Faqat interfeysdagi metodlardan foydalanadi
function engKattaFayl(TizimElementi $e): array
{
    if (!$e instanceof Papka) {
        return [$e->nomi(), $e->hajm()];
    }

    $eng = ['(bosh)', 0];
    foreach ($e->ichi() as $bola) {
        $nomzod = engKattaFayl($bola);
        if ($nomzod[1] > $eng[1]) {
            $eng = $nomzod;
        }
    }
    return $eng;
}

[$nom, $hajm] = engKattaFayl(daraxtQur());
printf('eng katta: %s (%d b)%s', $nom, $hajm, PHP_EOL);
Natija
eng katta: Markdown.php (9600 b)
Bu yerda instanceof yana paydo bo'ldi

Diqqat qilgan bo'lsangiz, engKattaFayl da instanceof qaytib keldi.

Sabab: bu amal bolalarni aylanib chiqishni talab qiladi, lekin ichi() metodi faqat Papka da bor.

Ikki yechim bor:

YechimBaho
Amalni interfeysga qo'shish (engKatta())Toza, lekin interfeys shishadi
Visitor naqshiKuchli, lekin murakkab

Eng ko'p ishlatiladigan amallar interfeysda bo'lishi kerak. Kamdan-kam kerak bo'lganlari uchun instanceof ham qabul qilinadi - bu jiddiy gunoh emas.

Amalni interfeysga ko'chirsak:

PHP
interface Element2
{
    public function nomi(): string;
    public function hajm(): int;
    /** @return array{string, int} */
    public function engKatta(): array;
}

final class Fayl2 implements Element2
{
    public function __construct(
        private readonly string $nom,
        private readonly int $hajm,
    ) {}

    public function nomi(): string { return $this->nom; }
    public function hajm(): int { return $this->hajm; }

    public function engKatta(): array { return [$this->nom, $this->hajm]; }
}

final class Papka2 implements Element2
{
    /** @var list<Element2> */
    private array $ichi = [];

    public function __construct(private readonly string $nom) {}

    public function qosh(Element2 ...$e): self
    {
        foreach ($e as $bir) { $this->ichi[] = $bir; }
        return $this;
    }

    public function nomi(): string { return $this->nom; }

    public function hajm(): int
    {
        return array_sum(array_map(
            static fn(Element2 $b): int => $b->hajm(), $this->ichi));
    }

    public function engKatta(): array
    {
        $eng = ['(bosh)', 0];
        foreach ($this->ichi as $bola) {
            $nomzod = $bola->engKatta();
            if ($nomzod[1] > $eng[1]) { $eng = $nomzod; }
        }
        return $eng;
    }
}

$d = (new Papka2('loyiha'))->qosh(
    new Fayl2('index.php', 1200),
    (new Papka2('src'))->qosh(
        new Fayl2('Markdown.php', 9600),
        new Fayl2('Str.php', 1500),
    ),
);

printf('%s -> %s (%d b)%s', $d->nomi(), ...[...$d->engKatta(), PHP_EOL]);
printf('jami: %d b%s', $d->hajm(), PHP_EOL);
Natija
loyiha -> Markdown.php (9600 b)
jami: 12300 b

Xavfsizlik va shaffoflik #

Klassik kitobda qosh() va olib tashla() metodlari umumiy interfeysga qo'yiladi. Bu "shaffof" variant:

PHP
interface ShaffofElement
{
    public function hajm(): int;
    public function qosh(ShaffofElement $e): void;
}

final class ShaffofFayl implements ShaffofElement
{
    public function __construct(private readonly int $hajm) {}

    public function hajm(): int { return $this->hajm; }

    // Faylga bola qo'shish MANTIQSIZ, lekin interfeys majburlaydi
    public function qosh(ShaffofElement $e): void
    {
        throw new BadMethodCallException('Faylga element qoshib bolmaydi');
    }
}

$f = new ShaffofFayl(100);
try {
    $f->qosh(new ShaffofFayl(50));
} catch (BadMethodCallException $x) {
    echo 'Xato: ', $x->getMessage(), PHP_EOL;
}
Natija
Xato: Faylga element qoshib bolmaydi
Bu Liskov tamoyilini buzadi

2-bo'limda ko'rgan edik: voris ota-sinfning va'dasini buzmasligi kerak. Bu yerda esa ShaffofFayl interfeysdagi metodni bajara olmaydi va istisno tashlaydi.

Ikki yondashuv bor:

Yondashuvqosh() qayerdaBaho
ShaffofUmumiy interfeysdaMijoz turni bilmaydi, lekin Liskov buziladi
XavfsizFaqat Papka daLiskov saqlanadi, lekin instanceof kerak bo'lishi mumkin

Zamonaviy amaliyot - xavfsiz variant. Bizning TizimElementi interfeysimizda qosh() yo'q, u faqat Papka da bor.

Aslida bu tanlov kim daraxtni quradi degan savolga bog'liq: agar daraxt bir marta qurilib keyin faqat o'qilsa, qosh() ni interfeysga qo'yish umuman kerak emas.

Boshqa misol: chegirmalar daraxti #

Composite faqat fayl tizimi uchun emas:

PHP
interface Chegirma
{
    public function nomi(): string;
    public function qollash(int $summa): int;
}

final class Foiz implements Chegirma
{
    public function __construct(private readonly int $foiz) {}

    public function nomi(): string { return "{$this->foiz}%"; }

    public function qollash(int $summa): int
    {
        return $summa - intdiv($summa * $this->foiz, 100);
    }
}

final class Aniq implements Chegirma
{
    public function __construct(private readonly int $miqdor) {}

    public function nomi(): string { return "-{$this->miqdor}"; }

    public function qollash(int $summa): int
    {
        return max(0, $summa - $this->miqdor);
    }
}

// Kompozit: ichidagilarni ketma-ket qo'llaydi
final class ChegirmaGuruhi implements Chegirma
{
    /** @var list<Chegirma> */
    private array $ichi = [];

    public function __construct(private readonly string $nom) {}

    public function qosh(Chegirma ...$c): self
    {
        foreach ($c as $bir) { $this->ichi[] = $bir; }
        return $this;
    }

    public function nomi(): string
    {
        return $this->nom . '(' . implode(' + ', array_map(
            static fn(Chegirma $c): string => $c->nomi(), $this->ichi)) . ')';
    }

    public function qollash(int $summa): int
    {
        foreach ($this->ichi as $c) {
            $summa = $c->qollash($summa);
        }
        return $summa;
    }
}

$aksiya = (new ChegirmaGuruhi('yangi yil'))->qosh(
    new Foiz(10),
    (new ChegirmaGuruhi('mijoz'))->qosh(new Foiz(5), new Aniq(20000)),
);

$summa = 500000;
printf("%s%s", $aksiya->nomi(), PHP_EOL);
printf('%d -> %d%s', $summa, $aksiya->qollash($summa), PHP_EOL);
Natija
yangi yil(10% + mijoz(5% + -20000))
500000 -> 407500

Hisob: 500000 → 450000 (10%) → 427500 (5%) → 407500 (-20000).

Narxi va cheklovlari #

PHP
// Halqa yaratsak - cheksiz rekursiya
$a = new Papka('a');
$b = new Papka('b');
$a->qosh($b);

echo 'a hajmi: ', $a->hajm(), PHP_EOL;
echo 'halqa yaratilmadi - Papka ota-onasini bilmaydi', PHP_EOL;
Natija
a hajmi: 0
halqa yaratilmadi - Papka ota-onasini bilmaydi
Composite ning kuchsiz joylari
MuammoIzoh
Halqa$a ichida $b, $b ichida $a - cheksiz rekursiya
ChuqurlikJuda chuqur daraxtda stek to'lishi mumkin
Tur cheklovlari"Papkada faqat 100 ta fayl" kabi qoidalar interfeysda ifodalanmaydi
SekinlikHar hajm() chaqiruvi butun daraxtni aylanadi

Oxirgisi amalda ko'p uchraydi: korsat() ichida har papka uchun hajm() chaqirilsa, katta daraxtda bir xil hisob ko'p marta takrorlanadi.

Yechim - natijani keshlash, lekin unda daraxt o'zgarganda keshni tozalash kerak bo'ladi.

Amaliy topshiriq
  1. instanceof bilan ishlaydigan daraxt hisoblovchisini yozing.
  2. Unga ikkinchi amal qo'shing va instanceof necha marta yozilganini sanang.
  3. Umumiy interfeys yozib, Fayl va Papka ni unga o'tkazing.
  4. Mijoz funksiyasidan instanceof ni olib tashlang.
  5. Daraxtni chiroyli qilib chiqaruvchi korsat() yozing.
  6. Yangi amal (eng chuqur fayl) qo'shing.
  7. Shaffof va xavfsiz variantlarni yozib solishtiring.
  8. Nima uchun shaffof variant Liskov ni buzishini tushuntiring.
  9. Chegirmalar daraxtini yozing va tartib natijaga ta'sirini tekshiring.
  10. Ikki papkani bir-birining ichiga qo'yib halqa yaratishga urinib ko'ring.

Xulosa #

  • Composite - daraxtsimon tuzilmalar uchun naqsh.
  • Asosiy va'da: bitta element ham, butun shox ham bir xil ishlanadi.
  • Naqshsiz kodda instanceof har amalda takrorlanadi.
  • Barg (Fayl) va shox (Papka) bitta interfeysni bajaradi.
  • Shox metodni bolalariga uzatadi - rekursiya ichkarida yashiringan.
  • Ko'p ishlatiladigan amallar interfeysda bo'lishi kerak.
  • qosh() ni umumiy interfeysga qo'yish Liskov ni buzadi - xavfsiz variant afzal.
  • Naqsh fayl tizimidan tashqari chegirmalar, menyular, huquqlar uchun ham ishlaydi.
  • Halqalar cheksiz rekursiyaga olib keladi - qurishda tekshirish kerak.
  • Har chaqiruvda butun daraxt aylanadi - katta daraxtda keshlash kerak bo'ladi.

Keyingi bo'limda obyekt oldiga nazorat qatlami qo'yadigan Proxy 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.