11-bo‘lim
Composite
Bitta element bilan ham, butun daraxt bilan ham bir xil ishlash.
Ushbu bo‘lim mundarijasi
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.
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;
jami hajm: 6700 bayt
instanceof har amalda takrorlanadiKod ishlaydi. Lekin endi yana beshta amal kerak bo'lsin: nomlarni chiqarish, qidirish, sanash, nusxalash, o'chirish.
Har birida yana o'sha instanceof yoziladi:
| Amal | instanceof bormi |
|---|---|
hajmHisobla | Ha |
nomlarniChiqar | Ha |
qidir | Ha |
fayllarSoni | Ha |
Uchinchi tur qo'shilsa (masalan Yorliq), hammasini
tahrirlash kerak - va bittasi unutiladi.
Bu 1-bo'limdagi if zanjirining daraxt ustidagi shakli.
Yechim: umumiy interfeys #
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(),
);
}
echo daraxtQur()->korsat(), PHP_EOL;
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:
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;
}
loyiha 18600 bayt
bitta.txt 42 bayt
index.php 1200 bayt
README.md 800 bayt
src 16600 bayt
bosh 0 bayt
Yangi amal qo'shish #
// 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);
eng katta: Markdown.php (9600 b)
instanceof yana paydo bo'ldiDiqqat 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:
| Yechim | Baho |
|---|---|
Amalni interfeysga qo'shish (engKatta()) | Toza, lekin interfeys shishadi |
Visitor naqshi | Kuchli, 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:
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);
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:
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;
}
Xato: Faylga element qoshib bolmaydi
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:
| Yondashuv | qosh() qayerda | Baho |
|---|---|---|
| Shaffof | Umumiy interfeysda | Mijoz turni bilmaydi, lekin Liskov buziladi |
| Xavfsiz | Faqat Papka da | Liskov 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:
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);
yangi yil(10% + mijoz(5% + -20000))
500000 -> 407500
Hisob: 500000 → 450000 (10%) → 427500 (5%) → 407500
(-20000).
Narxi va cheklovlari #
// 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;
a hajmi: 0
halqa yaratilmadi - Papka ota-onasini bilmaydi
| Muammo | Izoh |
|---|---|
| Halqa | $a ichida $b, $b ichida $a - cheksiz rekursiya |
| Chuqurlik | Juda chuqur daraxtda stek to'lishi mumkin |
| Tur cheklovlari | "Papkada faqat 100 ta fayl" kabi qoidalar interfeysda ifodalanmaydi |
| Sekinlik | Har 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.
instanceofbilan ishlaydigan daraxt hisoblovchisini yozing.- Unga ikkinchi amal qo'shing va
instanceofnecha marta yozilganini sanang. - Umumiy interfeys yozib,
FaylvaPapkani unga o'tkazing. - Mijoz funksiyasidan
instanceofni olib tashlang. - Daraxtni chiroyli qilib chiqaruvchi
korsat()yozing. - Yangi amal (
eng chuqur fayl) qo'shing. - Shaffof va xavfsiz variantlarni yozib solishtiring.
- Nima uchun shaffof variant Liskov ni buzishini tushuntiring.
- Chegirmalar daraxtini yozing va tartib natijaga ta'sirini tekshiring.
- 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
instanceofhar 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.
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.