Kodokon kodokon.com

OOP สมัยใหม่: คลาส คุณสมบัติแบบเลื่อนขั้น และการมองเห็น

เขียนคลาส PHP 8 ให้เป็นสำนวนที่ถูกต้อง: การเลื่อนขั้นคุณสมบัติ, readonly, named constructor และ value object ที่เปลี่ยนแปลงไม่ได้

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

คุณเขียนคลาสมานานแล้ว แต่ PHP 8 ได้ทำให้นิสัยเดิมหลายอย่างล้าสมัยไป สิ่งที่เห็นได้ชัดที่สุดคือ การเลื่อนขั้นคุณสมบัติ (property promotion) ในคอนสตรักเตอร์ การเติมคำสงวนบอกการมองเห็น (private, protected, public) ไว้หน้าพารามิเตอร์ของคอนสตรักเตอร์ จะสร้างคุณสมบัติและกำหนดค่าให้มันในขั้นตอนเดียว บล็อกซ้ำซ้อนสามส่วนของสไตล์เดิม คือการประกาศ พารามิเตอร์ และการกำหนดค่า จะยุบรวมเหลือเพียงส่วนเดียว

PHP
<?php

declare(strict_types=1);

class LegacyProduct
{
    private string $name;
    private int $priceCents;

    public function __construct(
        string $name,
        int $priceCents,
    ) {
        $this->name = $name;
        $this->priceCents = $priceCents;
    }
}

final class Product
{
    public function __construct(
        private string $name,
        private int $priceCents,
    ) {
    }

    public function label(): string
    {
        return sprintf(
            '%s (%.2f EUR)',
            $this->name,
            $this->priceCents / 100,
        );
    }
}

$product = new Product('Keyboard', 4990);
echo $product->label();
ก่อน / หลัง: การเลื่อนขั้นช่วยกำจัดโค้ดกำหนดค่าซ้ำ ๆ ทั้งหมด

การมองเห็นยังคงเป็นเครื่องมือแรกของคุณสำหรับการห่อหุ้ม (encapsulation): ใช้ private เป็นค่าเริ่มต้น, protected เฉพาะเมื่อคลาสลูกต้องการเข้าถึง, และ public เป็นทางเลือกสุดท้าย ตั้งแต่ PHP 8.1 เป็นต้นมา readonly เพิ่มการรับประกันที่แข็งแกร่งกว่า นั่นคือคุณสมบัติจะถูกเขียนได้เพียง ครั้งเดียว และจากภายในขอบเขตของคลาสที่ประกาศมันเท่านั้น เมื่อผสานกับการเลื่อนขั้น มันจะสร้าง value object ที่เปลี่ยนแปลงไม่ได้ (immutable) ในเพียงไม่กี่บรรทัด การ "เปลี่ยนแปลง" ใด ๆ จะคืนอินสแตนซ์ใหม่ ซึ่งช่วยขจัดผลข้างเคียงจากการใช้ร่วมกัน

PHP
<?php

declare(strict_types=1);

final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {
    }

    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException(
                'Currency mismatch'
            );
        }

        return new self(
            $this->amount + $other->amount,
            $this->currency,
        );
    }
}

$total = (new Money(500, 'EUR'))
    ->add(new Money(250, 'EUR'));

echo $total->amount;
value object ที่เปลี่ยนแปลงไม่ได้: add() คืนอินสแตนซ์ใหม่

ในด้านเมท็อด มีสองแพตเทิร์นที่นักพัฒนาผู้มีประสบการณ์ใช้กันเป็นหลัก Named constructor: คอนสตรักเตอร์แบบ private ที่จับคู่กับ static factory ทำให้เจตนาแต่ละอย่างชัดเจน และห้ามการสร้างอินสแตนซ์ที่ไม่สอดคล้องกัน Fluent interface: การคืน $this (เปลี่ยนแปลงได้) หรือ new self(...) (เปลี่ยนแปลงไม่ได้) ช่วยให้คุณเชื่อมการเรียกต่อกันได้ ข้อแลกเปลี่ยนเป็นที่รู้กันดี เวอร์ชันที่เปลี่ยนแปลงไม่ได้นั้นปลอดภัยกว่า แต่จะสร้างอ็อบเจกต์หนึ่งตัวต่อการเรียกหนึ่งครั้ง ซึ่งแทบไม่มีนัยสำคัญในเกือบทุกที่ แต่จะวัดผลได้ในลูปที่ทำงานถี่ (hot loop)

PHP
<?php

declare(strict_types=1);

final class User
{
    private function __construct(
        public readonly string $email,
        public readonly string $role,
    ) {
    }

    public static function admin(string $email): self
    {
        return new self($email, 'admin');
    }

    public static function member(string $email): self
    {
        return new self($email, 'member');
    }
}

$user = User::admin('lea@example.com');
echo $user->role;
คอนสตรักเตอร์แบบ private + static factory: สร้างได้เฉพาะสถานะที่ถูกต้องเท่านั้น

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. การเลื่อนขั้นคุณสมบัติทำอะไรในคอนสตรักเตอร์ของ PHP 8?
    • มันทำให้ทุกคุณสมบัติเป็น public โดยอัตโนมัติ
    • มันประกาศคุณสมบัติและกำหนดค่าจากพารามิเตอร์ในขั้นตอนเดียว
    • มันสร้างตัวเข้าถึง (getter) ให้ทุกคุณสมบัติ
    • มันแปลงคุณสมบัติให้เป็นค่าคงที่ของคลาส
  2. คุณสมบัติ readonly เขียนได้เมื่อใด?
    • ไม่ได้เลย: มันต้องรับค่าเริ่มต้นตอนประกาศ
    • ได้เพียงครั้งเดียว จากภายในขอบเขตของคลาสที่ประกาศมัน
    • เมื่อใดก็ได้ ตราบเท่าที่การเขียนมาจากตัวคลาสเอง
  3. คุณควรเลือกการมองเห็นแบบใดสำหรับคุณสมบัติที่ควรใช้ได้เฉพาะคลาสและคลาสลูกของมัน?
    • public
    • protected
    • private