17-bo‘lim
State
Holatni satr emas, obyekt qilish - va noto'g'ri o'tishlarni imkonsiz qilish.
Ushbu bo‘lim mundarijasi
State - obyektning xulqi uning holatiga qarab o'zgarganda, har holatni alohida sinfga ajratish naqshi.
Asosiy foyda: noto'g'ri o'tishlar kodda umuman mumkin bo'lmaydi.
Muammo: holat - satr, tekshiruvlar tarqoq #
final class Buyurtma
{
private string $holat = 'yangi';
/** @var list<string> */
private array $tarix = [];
public function tola(): string
{
if ($this->holat !== 'yangi') {
return "xato: {$this->holat} holatida tolab bolmaydi";
}
$this->holat = 'tolangan';
return 'tolandi';
}
public function jonat(): string
{
if ($this->holat !== 'tolangan') {
return "xato: {$this->holat} holatida jonatib bolmaydi";
}
$this->holat = 'jonatilgan';
return 'jonatildi';
}
public function bekor(): string
{
// DIQQAT: jo'natilganini ham bekor qilib bo'ladi - xato
if ($this->holat === 'yetkazilgan') {
return 'xato: yetkazilganini bekor qilib bolmaydi';
}
$this->holat = 'bekor';
return 'bekor qilindi';
}
public function holat(): string { return $this->holat; }
}
$b = new Buyurtma();
echo $b->tola(), ' -> ', $b->holat(), PHP_EOL;
echo $b->jonat(), ' -> ', $b->holat(), PHP_EOL;
echo $b->bekor(), ' -> ', $b->holat(), PHP_EOL;
tolandi -> tolangan
jonatildi -> jonatilgan
bekor qilindi -> bekor
Uchinchi qatorga qarang: buyurtma allaqachon jo'natilgan
edi, lekin bekor() uni bemalol bekor qildi.
Bu haqiqiy xato - kuryer yo'lda, mijoz esa buyurtma bekor qilinganini ko'radi.
Sabab: bekor() faqat bitta holatni tekshiradi.
Muallif jonatilgan ni qo'shishni unutgan.
| Muammo | Izoh |
|---|---|
| Har metodda o'z tekshiruvi | Bittasi unutiladi |
| Holat - oddiy satr | 'tolangan' va 'tolangn' farqi yo'q |
| Ruxsat etilgan o'tishlar yozilmagan | Faqat muallif boshida |
| Yangi holat = hamma metodni ochish | 5 holat x 4 amal = 20 tekshiruv |
Yechim: har holat - alohida sinf #
interface Holat
{
public function nomi(): string;
public function tola(Buyurtma2 $b): string;
public function jonat(Buyurtma2 $b): string;
public function bekor(Buyurtma2 $b): string;
}
// Umumiy "mumkin emas" javobi - har holat faqat mumkin bo'lganini yozadi
abstract class AsosHolat implements Holat
{
public function tola(Buyurtma2 $b): string
{
return $this->rad('tolash');
}
public function jonat(Buyurtma2 $b): string
{
return $this->rad('jonatish');
}
public function bekor(Buyurtma2 $b): string
{
return $this->rad('bekor qilish');
}
protected function rad(string $amal): string
{
return sprintf('RAD: "%s" holatida %s mumkin emas',
$this->nomi(), $amal);
}
}
final class Yangi extends AsosHolat
{
public function nomi(): string { return 'yangi'; }
public function tola(Buyurtma2 $b): string
{
$b->holatQoy(new Tolangan());
return 'tolandi';
}
public function bekor(Buyurtma2 $b): string
{
$b->holatQoy(new Bekor());
return 'bekor qilindi';
}
}
final class Tolangan extends AsosHolat
{
public function nomi(): string { return 'tolangan'; }
public function jonat(Buyurtma2 $b): string
{
$b->holatQoy(new Jonatilgan());
return 'jonatildi';
}
public function bekor(Buyurtma2 $b): string
{
$b->holatQoy(new Bekor());
return 'bekor qilindi (pul qaytariladi)';
}
}
// Jo'natilgandan keyin - hech narsa mumkin emas
final class Jonatilgan extends AsosHolat
{
public function nomi(): string { return 'jonatilgan'; }
}
final class Bekor extends AsosHolat
{
public function nomi(): string { return 'bekor'; }
}
final class Buyurtma2
{
private Holat $holat;
/** @var list<string> */
private array $tarix = [];
public function __construct()
{
$this->holat = new Yangi();
}
public function holatQoy(Holat $yangi): void
{
$this->tarix[] = $this->holat->nomi() . ' -> ' . $yangi->nomi();
$this->holat = $yangi;
}
public function tola(): string { return $this->holat->tola($this); }
public function jonat(): string { return $this->holat->jonat($this); }
public function bekor(): string { return $this->holat->bekor($this); }
public function holat(): string { return $this->holat->nomi(); }
/** @return list<string> */
public function tarix(): array { return $this->tarix; }
}
$b = new Buyurtma2();
foreach (['tola', 'jonat', 'bekor', 'tola'] as $amal) {
printf('%-6s %-38s [%s]%s',
$amal . ':', $b->$amal(), $b->holat(), PHP_EOL);
}
echo '--- tarix', PHP_EOL;
foreach ($b->tarix() as $o) {
echo ' ', $o, PHP_EOL;
}
tola: tolandi [tolangan]
jonat: jonatildi [jonatilgan]
bekor: RAD: "jonatilgan" holatida bekor qilish mumkin emas [jonatilgan]
tola: RAD: "jonatilgan" holatida tolash mumkin emas [jonatilgan]
--- tarix
yangi -> tolangan
tolangan -> jonatilgan
Bekor qilish endi rad etildi - va bu tasodifiy emas.
Jonatilgan sinfida bekor() metodi umuman yozilmagan,
shuning uchun asosiy sinfning "mumkin emas" javobi ishlaydi.
| Eski kod | Yangi kod |
|---|---|
| Har metod "kim emasligini" tekshiradi | Har holat "nima mumkinligini" yozadi |
| Unutilgan tekshiruv - xato | Yozilmagan metod - avtomatik rad |
| Holatlar ro'yxati hech qayerda yo'q | Har holat - fayl |
| O'tishlar tarqoq | O'tish holat ichida |
Bu xavfsiz standart tamoyili: unutilgan holatda tizim ruxsat berish emas, rad etish tomon og'adi.
Xavfsizlik bilan bog'liq kodda bu tamoyil majburiy.
Tarix ham avtomatik yig'ildi:
$b = new Buyurtma2();
$b->tola();
$b->jonat();
foreach ($b->tarix() as $i => $o) {
printf(' %d. %s%s', $i + 1, $o, PHP_EOL);
}
$c = new Buyurtma2();
$c->bekor();
echo 'ikkinchi buyurtma: ', $c->holat(), PHP_EOL;
echo 'tola urinishi: ', $c->tola(), PHP_EOL;
1. yangi -> tolangan
2. tolangan -> jonatilgan
ikkinchi buyurtma: bekor
tola urinishi: RAD: "bekor" holatida tolash mumkin emas
Yengilroq variant: enum va o'tishlar jadvali #
Holatlarda xulq kam bo'lsa, alohida sinflar ortiqcha:
enum Bosqich: string
{
case Yangi = 'yangi';
case Tolangan = 'tolangan';
case Jonatilgan = 'jonatilgan';
case Bekor = 'bekor';
/** @return list<self> */
public function keyingilari(): array
{
return match ($this) {
Bosqich::Yangi => [Bosqich::Tolangan, Bosqich::Bekor],
Bosqich::Tolangan => [Bosqich::Jonatilgan, Bosqich::Bekor],
Bosqich::Jonatilgan, Bosqich::Bekor => [],
};
}
public function otishMumkinmi(self $keyingi): bool
{
return in_array($keyingi, $this->keyingilari(), true);
}
public function yakuniymi(): bool
{
return $this->keyingilari() === [];
}
}
echo 'O\'tishlar jadvali:', PHP_EOL;
foreach (Bosqich::cases() as $b) {
$keyingi = array_map(
static fn(Bosqich $x): string => $x->value, $b->keyingilari());
printf(' %-12s -> %s%s', $b->value,
$keyingi === [] ? '(yakuniy)' : implode(', ', $keyingi), PHP_EOL);
}
echo 'Tekshiruvlar:', PHP_EOL;
var_dump(Bosqich::Yangi->otishMumkinmi(Bosqich::Tolangan));
var_dump(Bosqich::Jonatilgan->otishMumkinmi(Bosqich::Bekor));
var_dump(Bosqich::Jonatilgan->yakuniymi());
O'tishlar jadvali:
yangi -> tolangan, bekor
tolangan -> jonatilgan, bekor
jonatilgan -> (yakuniy)
bekor -> (yakuniy)
Tekshiruvlar:
bool(true)
bool(false)
bool(true)
Endi uni ishlatuvchi sinf:
final class Buyurtma3
{
private Bosqich $bosqich = Bosqich::Yangi;
public function otkaz(Bosqich $yangi): string
{
if (!$this->bosqich->otishMumkinmi($yangi)) {
return sprintf('RAD: %s -> %s',
$this->bosqich->value, $yangi->value);
}
$eski = $this->bosqich;
$this->bosqich = $yangi;
return sprintf('OK: %s -> %s', $eski->value, $yangi->value);
}
public function bosqich(): Bosqich { return $this->bosqich; }
}
$b = new Buyurtma3();
foreach ([Bosqich::Tolangan, Bosqich::Jonatilgan,
Bosqich::Bekor, Bosqich::Yangi] as $keyingi) {
echo $b->otkaz($keyingi), PHP_EOL;
}
echo 'yakuniy holat: ', $b->bosqich()->value, PHP_EOL;
OK: yangi -> tolangan
OK: tolangan -> jonatilgan
RAD: jonatilgan -> bekor
RAD: jonatilgan -> yangi
yakuniy holat: jonatilgan
| Belgi | enum jadvali | Holat sinflari |
|---|---|---|
| Holatda xulq bormi | Yo'q yoki oz | Ko'p |
| Har holatda o'z ma'lumoti | Yo'q | Bor |
| Kod miqdori | Kam (1 fayl) | Ko'p (n fayl) |
| Bazaga saqlash | Oson (->value) | Qo'lda o'girish kerak |
| Ro'yxatni ko'rish | cases() | Qo'lda |
| Yangi holat qo'shish | 1-2 joy | 1 yangi fayl |
Amaliyotdagi taqsimot taxminan shunday:
- 80% holatlarda
enum+ o'tishlar jadvali yetarli - Qolganida holat sinflari - masalan har holatda turli hisob-kitob, turli xabar shabloni, turli huquqlar bo'lsa
Yozgan kodingiz bazaga saqlanishi kerak bo'lsa, enum
ayniqsa qulay: ->value to'g'ridan-to'g'ri ustunga yoziladi.
State va Strategy farqi #
Ikkalasining tuzilmasi deyarli bir xil - interfeys va uni bajaruvchi sinflar. Farq kim almashtiradi degan savolda:
$farq = [
['Kim almashtiradi', 'Mijoz kod', 'Holatning ozi'],
['Variantlar bir-birini biladimi', 'Yoq', 'Ha'],
['Nechta variant faol', 'Bittasi', 'Bittasi'],
['Ketma-ketlik muhimmi', 'Yoq', 'Ha - otishlar'],
['Nimani ifodalaydi', 'Qanday qilish', 'Qayerda turish'],
];
printf("%-30s %-12s %s%s", '', 'Strategy', 'State', PHP_EOL);
echo str_repeat('-', 60), PHP_EOL;
foreach ($farq as $q) {
printf("%-30s %-12s %s%s", ...[...$q, PHP_EOL]);
}
Strategy State
------------------------------------------------------------
Kim almashtiradi Mijoz kod Holatning ozi
Variantlar bir-birini biladimi Yoq Ha
Nechta variant faol Bittasi Bittasi
Ketma-ketlik muhimmi Yoq Ha - otishlar
Nimani ifodalaydi Qanday qilish Qayerda turish
holatQoy(new Tolangan())Yuqoridagi kodda Yangi::tola() ichida shunday qator bor:
$b->holatQoy(new Tolangan());
Ya'ni holat o'zi keyingi holatni tanlaydi va kontekstni o'zgartiradi.
Strategiyada bunday hech qachon bo'lmaydi: Pochta
strategiyasi hech qachon "endi Kuryer bo'l" demaydi.
Bu bog'lanishning yon ta'siri bor - holat sinflari bir-birini biladi, ya'ni ular birga o'zgaradi. Shuning uchun ularni bitta modulda saqlash mantiqiy.
Naqshning narxi #
$holatlar = ['Yangi', 'Tolangan', 'Jonatilgan', 'Yetkazilgan',
'Qaytarilgan', 'Bekor'];
$amallar = ['tola', 'jonat', 'yetkaz', 'qaytar', 'bekor'];
printf('%d holat x %d amal = %d ta metod%s',
count($holatlar), count($amallar),
count($holatlar) * count($amallar), PHP_EOL);
// lekin ko'pchiligi yozilmaydi - asosiy sinf rad etadi
$haqiqiy = ['Yangi' => 2, 'Tolangan' => 3, 'Jonatilgan' => 1,
'Yetkazilgan' => 1, 'Qaytarilgan' => 0, 'Bekor' => 0];
printf('haqiqatda yoziladi: %d ta metod%s',
array_sum($haqiqiy), PHP_EOL);
printf('qolgani avtomatik rad etiladi: %d%s',
count($holatlar) * count($amallar) - array_sum($haqiqiy), PHP_EOL);
6 holat x 5 amal = 30 ta metod
haqiqatda yoziladi: 7 ta metod
qolgani avtomatik rad etiladi: 23
6 ta holat - 6 ta fayl, ustiga interfeys va asosiy sinf.
Sakkiz fayl bitta enum o'rniga.
Yana bir narx:
| Narx | Izoh |
|---|---|
| Holat obyekti bazaga saqlanmaydi | ->nomi() ni satr sifatida saqlab, o'qishda qayta yaratish kerak |
| Kontekst holatga uzatiladi | $b argumenti hamma metodda takrorlanadi |
| Holatlar bir-birini biladi | Bog'lanish kuchli |
| Holatda ma'lumot saqlansa | Almashganda u yo'qoladi |
Oxirgisi nozik: Tolangan holatida "qachon to'landi" saqlansa
va keyin Jonatilgan ga o'tilsa - u ma'lumot yo'qoladi.
Shuning uchun ma'lumot kontekstda turishi kerak, holatda
emas.
- Satr holatli buyurtma sinfini yozing.
- Unda ataylab bitta tekshiruvni unuting va noto'g'ri o'tishni bajaring.
- Har holatni alohida sinfga ajrating.
- Asosiy sinfda barcha amallarni rad eting.
- Faqat ruxsat etilgan metodlarni vorislarda yozing.
- Jo'natilgan buyurtmani bekor qilishga urinib ko'ring.
- O'tishlar tarixini avtomatik yig'ing.
- Xuddi shu mashinani
enumva o'tishlar jadvali bilan yozing. - Ikkala variantni kod miqdori bo'yicha solishtiring.
- State va Strategy farqini
holatQoy()misolida tushuntiring.
Xulosa #
- State - obyekt xulqi holatiga qarab o'zgarganda ishlatiladi.
- Satr holat bilan tekshiruvlar har metodda takrorlanadi va bittasi unutiladi.
- Har holat alohida sinf bo'lsa, u nima mumkinligini yozadi.
- Yozilmagan metod - avtomatik rad: xavfsiz standart.
- Holat diagrammasi to'g'ridan-to'g'ri sinflarga aylanadi.
- O'tishlar tarixini kontekst avtomatik yig'ishi mumkin.
- Xulq kam bo'lsa -
enum+ o'tishlar jadvali yetarli va soddaroq. enumbazaga saqlash uchun ham qulay (->value).- State da holatning o'zi keyingi holatni tanlaydi - Strategy da hech qachon.
- Ma'lumot holatda emas, kontekstda saqlanishi kerak.
Keyingi bo'limda so'rovni zanjir bo'ylab uzatadigan Chain of Responsibility va to'plamlarni aylanishni yashiradigan Iterator 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.