17-bo‘lim

State

Holatni satr emas, obyekt qilish - va noto'g'ri o'tishlarni imkonsiz qilish.

🕑 12 daqiqa o‘qish 📄 795 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo: holat - satr, tekshiruvlar tarqoq
  2. Yechim: har holat - alohida sinf
  3. Yengilroq variant: enum va o'tishlar jadvali
  4. State va Strategy farqi
  5. Naqshning narxi
  6. Xulosa

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 #

PHP
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;
Natija
tolandi -> tolangan
jonatildi -> jonatilgan
bekor qilindi -> bekor
Yo'lda ketayotgan buyurtma bekor qilindi

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.

MuammoIzoh
Har metodda o'z tekshiruviBittasi unutiladi
Holat - oddiy satr'tolangan' va 'tolangn' farqi yo'q
Ruxsat etilgan o'tishlar yozilmaganFaqat muallif boshida
Yangi holat = hamma metodni ochish5 holat x 4 amal = 20 tekshiruv

Yechim: har holat - alohida sinf #

PHP
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; }
}
PHP
$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;
}
Natija
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.

Nima o'zgardi
Eski kodYangi kod
Har metod "kim emasligini" tekshiradiHar holat "nima mumkinligini" yozadi
Unutilgan tekshiruv - xatoYozilmagan metod - avtomatik rad
Holatlar ro'yxati hech qayerda yo'qHar holat - fayl
O'tishlar tarqoqO'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:

PHP
$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;
Natija
  1. yangi -> tolangan
  2. tolangan -> jonatilgan
ikkinchi buyurtma: bekor
tola urinishi: RAD: "bekor" holatida tolash mumkin emas
Buyurtma holatlari va ruxsat etilgan o'tishlar Yangi boshlang'ich Tolangan Jonatilgan yakuniy Bekor yakuniy tola() jonat() bekor() bekor() × bekor() - RAD Har o'q - holat sinfidagi bitta metod. Metod yozilmagan bo'lsa, o'q ham yo'q. Diagramma bilan kod bir-biriga mos - bu naqshning asosiy foydasi.
Holat diagrammasi to'g'ridan-to'g'ri sinflarga aylanadi

Yengilroq variant: enum va o'tishlar jadvali #

Holatlarda xulq kam bo'lsa, alohida sinflar ortiqcha:

PHP
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() === [];
    }
}
PHP
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());
Natija
O'tishlar jadvali:
  yangi        -> tolangan, bekor
  tolangan     -> jonatilgan, bekor
  jonatilgan   -> (yakuniy)
  bekor        -> (yakuniy)
Tekshiruvlar:
bool(true)
bool(false)
bool(true)

Endi uni ishlatuvchi sinf:

PHP
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;
Natija
OK:  yangi -> tolangan
OK:  tolangan -> jonatilgan
RAD: jonatilgan -> bekor
RAD: jonatilgan -> yangi
yakuniy holat: jonatilgan
Qaysi variantni tanlash
Belgienum jadvaliHolat sinflari
Holatda xulq bormiYo'q yoki ozKo'p
Har holatda o'z ma'lumotiYo'qBor
Kod miqdoriKam (1 fayl)Ko'p (n fayl)
Bazaga saqlashOson (->value)Qo'lda o'girish kerak
Ro'yxatni ko'rishcases()Qo'lda
Yangi holat qo'shish1-2 joy1 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:

PHP
$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]);
}
Natija
                               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
Eng aniq belgi - holatQoy(new Tolangan())

Yuqoridagi kodda Yangi::tola() ichida shunday qator bor:

KOD
$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 #

PHP
$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);
Natija
6 holat x 5 amal = 30 ta metod
haqiqatda yoziladi: 7 ta metod
qolgani avtomatik rad etiladi: 23
Lekin sinflar soni baribir o'sadi

6 ta holat - 6 ta fayl, ustiga interfeys va asosiy sinf. Sakkiz fayl bitta enum o'rniga.

Yana bir narx:

NarxIzoh
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 biladiBog'lanish kuchli
Holatda ma'lumot saqlansaAlmashganda 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.

Amaliy topshiriq
  1. Satr holatli buyurtma sinfini yozing.
  2. Unda ataylab bitta tekshiruvni unuting va noto'g'ri o'tishni bajaring.
  3. Har holatni alohida sinfga ajrating.
  4. Asosiy sinfda barcha amallarni rad eting.
  5. Faqat ruxsat etilgan metodlarni vorislarda yozing.
  6. Jo'natilgan buyurtmani bekor qilishga urinib ko'ring.
  7. O'tishlar tarixini avtomatik yig'ing.
  8. Xuddi shu mashinani enum va o'tishlar jadvali bilan yozing.
  9. Ikkala variantni kod miqdori bo'yicha solishtiring.
  10. 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.
  • enum bazaga 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.

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.