6-bo‘lim

Singleton va uning muammolari

Eng oson yoziladigan va eng ko'p pushaymon bo'linadigan naqsh.

🕑 14 daqiqa o‘qish 📄 968 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Klassik ko'rinish
  2. Endi muammolar
  3. 1. Yashirin bog'liqlik
  4. 2. Testlar bir-biriga ta'sir qiladi
  5. Yaxshiroq yo'l: bir marta yaratib, uzatish
  6. PHP da Singleton kamroq zarur
  7. Singleton haqiqatan mos keladigan holatlar
  8. Statik metodlar ham singleton
  9. Xulosa

Singleton - sinfning butun dastur davomida faqat bitta nusxasi bo'lishini kafolatlaydi.

Bu eng mashhur naqsh, va shu bilan birga eng ko'p tanqid qilinadigani. Ikkalasini ham ko'ramiz.

Klassik ko'rinish #

PHP
final class Sozlamalar
{
    private static ?self $nusxa = null;

    /** @var array<string, string> */
    private array $qiymatlar = [];

    // tashqaridan new qilib bo'lmaydi
    private function __construct()
    {
        $this->qiymatlar = ['til' => 'uz', 'mavzu' => 'yorug'];
    }

    public static function ol(): self
    {
        // birinchi chaqiruvda yaratiladi, keyin qayta ishlatiladi
        self::$nusxa ??= new self();
        return self::$nusxa;
    }

    public function qiymat(string $kalit): ?string
    {
        return $this->qiymatlar[$kalit] ?? null;
    }

    public function qoy(string $kalit, string $qiymat): void
    {
        $this->qiymatlar[$kalit] = $qiymat;
    }

    // nusxa ko'chirishning oldini olamiz
    private function __clone() {}

    public function __wakeup(): void
    {
        throw new LogicException('Singleton ni deserializatsiya qilib bolmaydi');
    }
}
PHP
$a = Sozlamalar::ol();
$b = Sozlamalar::ol();

echo 'bir xil obyektmi: ', $a === $b ? 'ha' : 'yoq', PHP_EOL;
echo 'obyekt raqami: ', spl_object_id($a), ' va ', spl_object_id($b), PHP_EOL;
echo 'til: ', $a->qiymat('til'), PHP_EOL;

// $a orqali o'zgartirsak, $b da ham ko'rinadi
$a->qoy('til', 'ru');
echo 'b dagi til: ', $b->qiymat('til'), PHP_EOL;
Natija
bir xil obyektmi: ha
obyekt raqami: 1 va 1
til: uz
b dagi til: ru

Yaratishning barcha yo'llari yopilgan:

PHP
try {
    $x = new Sozlamalar();
} catch (Error $e) {
    echo 'new: ', $e->getMessage(), PHP_EOL;
}

try {
    $x = clone Sozlamalar::ol();
} catch (Error $e) {
    echo 'clone: ', $e->getMessage(), PHP_EOL;
}

try {
    $x = unserialize(serialize(Sozlamalar::ol()));
} catch (LogicException $e) {
    echo 'unserialize: ', $e->getMessage(), PHP_EOL;
}
Natija
new: Call to private Sozlamalar::__construct() from global scope
clone: Call to private Sozlamalar::__clone() from global scope
unserialize: Singleton ni deserializatsiya qilib bolmaydi
??= operatori

self::$nusxa ??= new self(); - bu null bo'lsa o'zlashtirish operatori (PHP 7.4+).

To'liq yozilishi:

KOD
if (self::$nusxa === null) {
    self::$nusxa = new self();
}

Bu kechiktirilgan yaratish (lazy initialization): obyekt birinchi so'ralguncha yaratilmaydi. Agar sinf hech qachon ishlatilmasa, konstruktor ham chaqirilmaydi.

Endi muammolar #

Singleton ikkita narsani birlashtiradi, va ikkalasi ham alohida-alohida muammo:

Nima qiladiNima uchun yomon
Nusxa bitta bo'lishini kafolatlaydiBu odatda kerak emas
Unga hamma joydan kirish beradiBu global o'zgaruvchi

Ikkinchisi asosiy muammo.

1. Yashirin bog'liqlik #

PHP
final class HisobotXizmati
{
    public function tayyorla(): string
    {
        // bog'liqlik konstruktorda ko'rinmaydi
        $til = Sozlamalar::ol()->qiymat('til');
        return "hisobot tili: {$til}";
    }
}

$x = new HisobotXizmati();
echo $x->tayyorla(), PHP_EOL;
Natija
hisobot tili: uz
Konstruktorga qarab bog'liqlikni bilib bo'lmaydi

new HisobotXizmati() - konstruktor bo'sh. Sinf hech narsaga bog'liq emasdek ko'rinadi.

Aslida u Sozlamalar ga bog'liq. Buni bilish uchun butun sinf kodini o'qish kerak.

Katta loyihada bu quyidagiga olib keladi:

OqibatIzoh
Sinfni alohida ishlatib bo'lmaydiSingleton hamisha kerak
Bog'liqliklar grafigi ko'rinmaydiKim kimga tayanadi - noma'lum
Refaktoring xavfliNima buzilishini oldindan bilib bo'lmaydi

2-bo'limdagi D tamoyili aynan shuni taqiqlaydi.

2. Testlar bir-biriga ta'sir qiladi #

PHP
// birinchi "test" tilni o'zgartiradi
function testRuscha(): string
{
    Sozlamalar::ol()->qoy('til', 'ru');
    return 'testRuscha: ' . Sozlamalar::ol()->qiymat('til');
}

// ikkinchi "test" o'zbekchani kutadi - o'zi hech narsa o'zgartirmaydi
function testOzbekcha(): string
{
    return 'testOzbekcha: ' . Sozlamalar::ol()->qiymat('til');
}

echo testOzbekcha(), PHP_EOL;   // hali toza
echo testRuscha(), PHP_EOL;     // holatni o'zgartirdi
echo testOzbekcha(), PHP_EOL;   // endi ifloslangan
Natija
testOzbekcha: uz
testRuscha: ru
testOzbekcha: ru
Mana asosiy muammo

Uchinchi "test" hech narsa o'zgartirmagan, lekin ru oldi - chunki ikkinchi test global holatni ifloslantirdi.

Real loyihada bu quyidagicha ko'rinadi:

BelgiSabab
Test alohida o'tadi, birga o'tmaydiOldingi test holatni o'zgartirgan
Testlar tartibi ahamiyatliGlobal holat ketma-ket to'planadi
"Menda ishlaydi"Boshqa mashinada tartib boshqacha

Yagona yechim - har testdan oldin singleton ni tozalash. Buning uchun esa private static maydonga kirish kerak - ya'ni reflection yoki maxsus qaytaTikla() metodi.

Ikkalasi ham naqshning o'z kafolatini buzadi.

Singleton bog'liqlikni yashiradi Singleton bilan Injeksiya bilan Hisobot Buyurtma Xabarnoma Sozlamalar::ol() Bog'liqlik konstruktorda KO'RINMAYDI Holat testlar orasida saqlanadi Hisobot Buyurtma Xabarnoma konstruktor Sozlamalar Bog'liqlik konstruktorda KO'RINADI Har test o'z nusxasini oladi Nusxa baribir bitta O'ng tomonda ham Sozlamalar bitta marta yaratiladi - farqi shuki, buni yig'uvchi kod hal qiladi.
Muammo "bitta nusxa" da emas, "hamma joydan kirish" da

Yaxshiroq yo'l: bir marta yaratib, uzatish #

PHP
final readonly class Konfiguratsiya
{
    /** @param array<string, string> $qiymatlar */
    public function __construct(private array $qiymatlar) {}

    public function qiymat(string $kalit): ?string
    {
        return $this->qiymatlar[$kalit] ?? null;
    }
}

final readonly class Hisobot
{
    // bog'liqlik ochiq ko'rinadi
    public function __construct(private Konfiguratsiya $konf) {}

    public function tayyorla(): string
    {
        return 'hisobot tili: ' . $this->konf->qiymat('til');
    }
}

// dasturning kirish nuqtasida BIR MARTA yaratiladi
$konf = new Konfiguratsiya(['til' => 'uz', 'mavzu' => 'yorug']);

echo (new Hisobot($konf))->tayyorla(), PHP_EOL;

// testda esa boshqa nusxa - hech narsa ifloslanmaydi
$testKonf = new Konfiguratsiya(['til' => 'ru']);
echo (new Hisobot($testKonf))->tayyorla(), PHP_EOL;
echo (new Hisobot($konf))->tayyorla(), PHP_EOL;
Natija
hisobot tili: uz
hisobot tili: ru
hisobot tili: uz
Nusxa baribir bitta

Diqqat: $konf butun dasturda bitta marta yaratildi. Ya'ni "bitta nusxa" maqsadiga erishildi.

Farq shundaki, buni sinf o'zi majburlamadi - dasturni yig'uvchi kod hal qildi.

SingletonInjeksiya
Nusxa soniSinf majburlaydiYig'uvchi kod hal qiladi
Bog'liqlik ko'rinadimiYo'qHa, konstruktorda
Testda almashtirishQiyinOson
Ikkinchi nusxa kerak bo'lsaNaqshni buzish kerakShunchaki new

Oxirgi qator amalda tez-tez uchraydi: "bitta baza ulanishi" degan qoida bir kun "asosiy va o'qish uchun replika" ga aylanadi.

PHP da Singleton kamroq zarur #

PHP ning ishlash modeli boshqa tillardan farq qiladi:

PHP
// Har so'rov o'z jarayonida boshlanadi va tugaydi
echo 'jarayon: har HTTP so\'rovi uchun yangi', PHP_EOL;
echo 'xotira: so\'rov tugagach butunlay tozalanadi', PHP_EOL;
echo 'oqimlar: odatiy PHP-FPM da bitta so\'rov = bitta oqim', PHP_EOL;

$boshlanish = memory_get_usage();
$katta = range(1, 100000);
$oxiri = memory_get_usage();

printf('massiv xotirasi: ~%d KB%s',
    intdiv($oxiri - $boshlanish, 1024), PHP_EOL);
unset($katta);
printf('unset dan keyin: ~%d KB%s',
    intdiv(memory_get_usage() - $boshlanish, 1024), PHP_EOL);
Natija
jarayon: har HTTP so'rovi uchun yangi
xotira: so'rov tugagach butunlay tozalanadi
oqimlar: odatiy PHP-FPM da bitta so'rov = bitta oqim
massiv xotirasi: ~4096 KB
unset dan keyin: ~0 KB
Java va PHP dagi farq

Java yoki C# da singleton butun server ishlash muddati davomida yashaydi - soatlab, kunlab. Shuning uchun u yerda oqimlar xavfsizligi (synchronized, volatile) jiddiy masala.

Odatiy PHP da esa obyekt bitta so'rov davomida yashaydi va so'rov tugagach yo'qoladi. Oqimlar muammosi ham yo'q.

Bu shuni bildiradi:

Java dagi sababPHP dagi holat
Qimmat resursni bir marta yaratishHar so'rovda baribir yangi
Oqimlar orasida bo'lishishOqim bitta
Xotirani tejashSo'rov qisqa

Ya'ni PHP da Singleton ning texnik sabablari deyarli yo'q. Qolgani - qulaylik, va u qimmatga tushadi.

(Swoole yoki RoadRunner kabi doimiy ishlaydigan muhitlar bundan mustasno - u yerda holat so'rovlar orasida saqlanadi va bu yangi xatolar manbai bo'ladi.)

Singleton haqiqatan mos keladigan holatlar #

Naqsh butunlay yomon emas. U mos keladigan holatlar bor:

HolatNima uchun mos
Jurnal yozuvchi (logger)Holatsiz, almashtirish kerak emas
Tasodifiy sonlar generatori urug'iGlobal bo'lishi mantiqiy
Konstantalar registriO'zgarmaydi
php://stdout kabi tashqi resursHaqiqatan bitta
PHP
final class Jurnal
{
    private static ?self $nusxa = null;
    /** @var list<string> */
    private array $qatorlar = [];

    private function __construct() {}

    public static function ol(): self
    {
        return self::$nusxa ??= new self();
    }

    public function yoz(string $daraja, string $xabar): void
    {
        $this->qatorlar[] = strtoupper($daraja) . ': ' . $xabar;
    }

    /** @return list<string> */
    public function hammasi(): array
    {
        return $this->qatorlar;
    }
}

Jurnal::ol()->yoz('info', 'dastur boshlandi');
Jurnal::ol()->yoz('warning', 'sekin sorov: 1.2s');
Jurnal::ol()->yoz('info', 'dastur tugadi');

foreach (Jurnal::ol()->hammasi() as $i => $q) {
    printf('%d. %s%s', $i + 1, $q, PHP_EOL);
}
Natija
1. INFO: dastur boshlandi
2. WARNING: sekin sorov: 1.2s
3. INFO: dastur tugadi
Bu yerda ham savol beriladi

Jurnal singleton bo'lishi shart emas. Uni ham konstruktordan uzatish mumkin, va shunda testda soxta jurnal qo'yish osonlashadi.

Amaliy qoida:

Singleton ni oxirgi chora sifatida ishlating. Avval injeksiyani sinab ko'ring; u noqulay bo'lib chiqsagina singleton haqida o'ylang.

Ko'p jamoada bu qoida qattiqroq yangraydi: yangi kodda singleton umuman yozilmaydi.

Statik metodlar ham singleton #

PHP
final class Matematika
{
    public static function ortacha(float ...$sonlar): float
    {
        if ($sonlar === []) {
            throw new InvalidArgumentException('bosh royxat');
        }
        return array_sum($sonlar) / count($sonlar);
    }
}

printf('%.2f%s', Matematika::ortacha(4.0, 5.5, 6.5), PHP_EOL);
Natija
5.33
Bu esa muammosiz

ortacha() - statik metod, lekin u holatsiz: hech narsa saqlamaydi, tashqi narsaga bog'lanmaydi, bir xil kirishga doim bir xil javob beradi.

Singleton ning muammosi statik bo'lishida emas, holatda.

Statik + holatsizXavfsiz
Statik + holatGlobal o'zgaruvchi, xavfli

Shuning uchun str_pad, array_map kabi funksiyalar hech kimga xalaqit bermaydi.

Amaliy topshiriq
  1. Klassik singleton yozing va === bilan bitta ekanini tekshiring.
  2. new, clone, unserialize yo'llarini yoping va har birini sinang.
  3. Singleton holatini o'zgartiruvchi ikkita "test" funksiya yozing.
  4. Ularni turli tartibda chaqirib, natija o'zgarishini ko'ring.
  5. Xuddi shu sinfni konstruktor injeksiyasiga o'tkazing.
  6. Injeksiya bilan ikkita turli konfiguratsiya yarating.
  7. Nusxa baribir bitta bo'lganini tushuntiring.
  8. PHP va Java dagi obyekt umrini solishtiring.
  9. Singleton mos keladigan uchta holat yozing.
  10. Statik metod qachon xavfsiz ekanini tushuntiring.

Xulosa #

  • Singleton - sinfning bitta nusxasi bo'lishini kafolatlaydi.
  • Amalga oshirish: private __construct, static maydon, ol() metodi.
  • ??= bilan kechiktirilgan yaratish olinadi.
  • __clone va __wakeup ham yopilishi kerak.
  • Asosiy muammo "bitta nusxa" da emas - hamma joydan kirish da.
  • Bog'liqlik konstruktorda ko'rinmaydi, shuning uchun kodni o'qish qiyinlashadi.
  • Global holat testlarni bir-biriga bog'laydi va tartibga sezgir qiladi.
  • Yechim: obyektni bir marta yaratib, konstruktordan uzatish.
  • PHP da obyekt bitta so'rov davomida yashaydi - singleton ning texnik sabablari deyarli yo'q.
  • Statik metod holatsiz bo'lsa xavfsiz; muammo statiklikda emas, holatda.

Keyingi bo'limda mavjud obyektdan nusxa olib yangi obyekt yasaydigan Prototype 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.