メインコンテンツへ移動

เนื้อหาภาษาไทย / タイ語のコンテンツ

システムガイド 内容確認済み

SOLID Principles: หลักการออกแบบ OOP ที่ดี

บทความนี้แนะนำหลักการ SOLID ซึ่งเป็นชุดหลักการออกแบบเชิงวัตถุที่สำคัญสำหรับการสร้างซอฟต์แวร์ที่มีคุณภาพสูง, ยืดหยุ่น, และบำรุงรักษาง่าย โดยได้อธิบายหลักการแต่ละ...

SOLID Principles: หลักการออกแบบ OOP ที่ดี

หลังจากที่เราได้เรียนรู้เรื่อง Dependency Injection และ Service Container ไปแล้ว จะเห็นได้ว่าการออกแบบระบบที่ดีนั้นมีความสำคัญอย่างมาก หลักการ SOLID คือชุดของหลักการออกแบบที่ช่วยให้เราสร้างซอฟต์แวร์ที่ยืดหยุ่น, บำรุงรักษาง่าย, และเข้าใจได้ง่ายขึ้น SOLID เป็นตัวย่อที่มาจากหลักการ 5 ข้อดังนี้:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

เราจะมาเจาะลึกแต่ละหลักการเหล่านี้กัน

Single Responsibility Principle (SRP): Class ควรมีหน้าที่เดียว

หลักการ SRP บอกว่า class หนึ่ง class ควรมีเหตุผลเดียวในการเปลี่ยนแปลง (reason to change) หรือพูดง่ายๆ คือ class นั้นควรรับผิดชอบเพียงอย่างเดียวเท่านั้น หาก class ของเราทำหลายอย่างเกินไป เมื่อความต้องการของระบบเปลี่ยนแปลง การแก้ไข class นั้นอาจส่งผลกระทบต่อส่วนอื่นๆ ของระบบได้

ตัวอย่างที่ไม่ดี:

class User {
    public function __construct(private string $name, private string $email) {}

    public function getName(): string {
        return $this->name;
    }

    public function getEmail(): string {
        return $this->email;
    }

    public function saveToDatabase(): void {
        // โค้ดสำหรับบันทึกข้อมูลผู้ใช้ลงฐานข้อมูล
        echo "Saving user to database...\n";
    }

    public function sendWelcomeEmail(): void {
        // โค้ดสำหรับส่งอีเมลต้อนรับ
        echo "Sending welcome email...\n";
    }
}

$user = new User("John Doe", "john.doe@example.com");
$user->saveToDatabase();
$user->sendWelcomeEmail();

ในตัวอย่างนี้ class `User` มีหน้าที่ทั้งจัดการข้อมูลผู้ใช้, บันทึกลงฐานข้อมูล, และส่งอีเมล หากเราต้องการเปลี่ยนวิธีการส่งอีเมล เราต้องแก้ไข class `User` ซึ่งอาจส่งผลกระทบต่อส่วนอื่นๆ ที่เกี่ยวข้องกับการจัดการข้อมูลผู้ใช้

ตัวอย่างที่ดี:

class User {
    public function __construct(private string $name, private string $email) {}

    public function getName(): string {
        return $this->name;
    }

    public function getEmail(): string {
        return $this->email;
    }
}

class UserRepository {
    public function save(User $user): void {
        // โค้ดสำหรับบันทึกข้อมูลผู้ใช้ลงฐานข้อมูล
        echo "Saving user to database...\n";
    }
}

class EmailService {
    public function sendWelcomeEmail(User $user): void {
        // โค้ดสำหรับส่งอีเมลต้อนรับ
        echo "Sending welcome email...\n";
    }
}

$user = new User("John Doe", "john.doe@example.com");
$userRepository = new UserRepository();
$emailService = new EmailService();

$userRepository->save($user);
$emailService->sendWelcomeEmail($user);

ในตัวอย่างนี้ เราได้แยกความรับผิดชอบออกเป็น 3 class: `User`, `UserRepository`, และ `EmailService` แต่ละ class มีหน้าที่ของตัวเองอย่างชัดเจน ทำให้การแก้ไขหรือเปลี่ยนแปลงระบบทำได้ง่ายขึ้นและลดผลกระทบต่อส่วนอื่นๆ

Open/Closed Principle (OCP): เปิดรับการขยาย แต่ปิดรับการแก้ไข

หลักการ OCP บอกว่า class ของเราควรเปิดรับการขยาย (extension) แต่ปิดรับการแก้ไข (modification) หมายความว่าเราควรสามารถเพิ่มความสามารถใหม่ให้กับ class ได้โดยไม่ต้องแก้ไขโค้ดเดิม

ตัวอย่างที่ไม่ดี:

class Order {
    private array $items;
    private string $paymentType;

    public function __construct(array $items, string $paymentType) {
        $this->items = $items;
        $this->paymentType = $paymentType;
    }

    public function processPayment(): void {
        if ($this->paymentType === "credit_card") {
            // โค้ดสำหรับประมวลผลการชำระเงินด้วยบัตรเครดิต
            echo "Processing credit card payment...\n";
        } elseif ($this->paymentType === "paypal") {
            // โค้ดสำหรับประมวลผลการชำระเงินด้วย PayPal
            echo "Processing PayPal payment...\n";
        } else {
            throw new Exception("Unsupported payment type");
        }
    }
}

$order = new Order(["item1", "item2"], "credit_card");
$order->processPayment();

ในตัวอย่างนี้ หากเราต้องการเพิ่มวิธีการชำระเงินใหม่ เช่น PromptPay เราต้องแก้ไข class `Order` ซึ่งขัดกับหลักการ OCP

ตัวอย่างที่ดี:

interface PaymentProcessor {
    public function processPayment(array $items): void;
}

class CreditCardPaymentProcessor implements PaymentProcessor {
    public function processPayment(array $items): void {
        // โค้ดสำหรับประมวลผลการชำระเงินด้วยบัตรเครดิต
        echo "Processing credit card payment...\n";
    }
}

class PayPalPaymentProcessor implements PaymentProcessor {
    public function processPayment(array $items): void {
        // โค้ดสำหรับประมวลผลการชำระเงินด้วย PayPal
        echo "Processing PayPal payment...\n";
    }
}

class Order {
    private array $items;
    private PaymentProcessor $paymentProcessor;

    public function __construct(array $items, PaymentProcessor $paymentProcessor) {
        $this->items = $items;
        $this->paymentProcessor = $paymentProcessor;
    }

    public function processPayment(): void {
        $this->paymentProcessor->processPayment($this->items);
    }
}

$creditCardProcessor = new CreditCardPaymentProcessor();
$order = new Order(["item1", "item2"], $creditCardProcessor);
$order->processPayment();

ในตัวอย่างนี้ เราได้สร้าง interface `PaymentProcessor` และ implement class สำหรับแต่ละวิธีการชำระเงิน เมื่อเราต้องการเพิ่มวิธีการชำระเงินใหม่ เราเพียงแค่สร้าง class ใหม่ที่ implement `PaymentProcessor` โดยไม่ต้องแก้ไข class `Order`

Liskov Substitution Principle (LSP): Class ลูกต้องแทนที่ Class แม่ได้

หลักการ LSP บอกว่า class ลูก (subtype) ควรสามารถแทนที่ class แม่ (base type) ได้โดยที่โค้ดที่ใช้ class แม่ไม่เกิดปัญหาใดๆ หาก class ลูกไม่สามารถทำหน้าที่เหมือน class แม่ได้ แสดงว่าการสืบทอดนั้นไม่ถูกต้อง

หลักการนี้เน้นเรื่องความสอดคล้องของพฤติกรรมระหว่าง class แม่และ class ลูก

Interface Segregation Principle (ISP): Interface ควรเฉพาะเจาะจง

หลักการ ISP บอกว่า client ไม่ควรถูกบังคับให้ implement methods ที่ตัวเองไม่ได้ใช้ หาก interface ใหญ่เกินไป ควรแบ่งออกเป็น interface เล็กๆ หลาย interface ที่มีความเฉพาะเจาะจงมากขึ้น

ตัวอย่างที่ไม่ดี:

interface Worker {
    public function work(): void;
    public function eat(): void;
}

class Human implements Worker {
    public function work(): void {
        // ทำงาน
        echo "Working...\n";
    }
    public function eat(): void {
        // กิน
        echo "Eating...\n";
    }
}

class Robot implements Worker {
    public function work(): void {
        // ทำงาน
        echo "Working...\n";
    }
    public function eat(): void {
        // หุ่นยนต์ไม่ต้องกิน!
        throw new Exception("Robot cannot eat!");
    }
}

ในตัวอย่างนี้ class `Robot` ถูกบังคับให้ implement method `eat()` ซึ่งไม่จำเป็นสำหรับ robot

ตัวอย่างที่ดี:

interface Workable {
    public function work(): void;
}

interface Eatable {
    public function eat(): void;
}

class Human implements Workable, Eatable {
    public function work(): void {
        // ทำงาน
        echo "Working...\n";
    }
    public function eat(): void {
        // กิน
        echo "Eating...\n";
    }
}

class Robot implements Workable {
    public function work(): void {
        // ทำงาน
        echo "Working...\n";
    }
}

ในตัวอย่างนี้ เราได้แบ่ง interface `Worker` ออกเป็น `Workable` และ `Eatable` ทำให้ class `Robot` ไม่ต้อง implement method ที่ไม่จำเป็น

Dependency Inversion Principle (DIP): ขึ้นอยู่กับ Abstraction ไม่ใช่ Concrete

หลักการ DIP บอกว่า:

  1. High-level modules ไม่ควรขึ้นอยู่กับ low-level modules ทั้งสองควรขึ้นอยู่กับ abstractions
  2. Abstractions ไม่ควรขึ้นอยู่กับ details Details ควรขึ้นอยู่กับ abstractions

พูดง่ายๆ คือ class ของเราควรขึ้นอยู่กับ interface หรือ abstract class มากกว่า class ที่เป็น concrete implementation เพื่อลดการพึ่งพากันและเพิ่มความยืดหยุ่น

ตัวอย่างที่ไม่ดี:

class LightBulb {
    public function turnOn(): void {
        echo "LightBulb: Bulb turned on...\n";
    }

    public function turnOff(): void {
        echo "LightBulb: Bulb turned off...\n";
    }
}

class Switch {
    private LightBulb $bulb;

    public function __construct() {
        $this->bulb = new LightBulb(); // High-level module ขึ้นอยู่กับ low-level module โดยตรง
    }

    public function operate(): void {
        $this->bulb->turnOn();
    }
}

$switch = new Switch();
$switch->operate();

ในตัวอย่างนี้ class `Switch` (high-level module) ขึ้นอยู่กับ class `LightBulb` (low-level module) โดยตรง หากเราต้องการเปลี่ยนเป็นหลอดไฟชนิดอื่น เราต้องแก้ไข class `Switch`

ตัวอย่างที่ดี:

interface Switchable {
    public function turnOn(): void;
    public function turnOff(): void;
}

class LightBulb implements Switchable {
    public function turnOn(): void {
        echo "LightBulb: Bulb turned on...\n";
    }

    public function turnOff(): void {
        echo "LightBulb: Bulb turned off...\n";
    }
}

class Switch {
    private Switchable $device;

    public function __construct(Switchable $device) {
        $this->device = $device; // High-level module ขึ้นอยู่กับ abstraction
    }

    public function operate(): void {
        $this->device->turnOn();
    }
}

$bulb = new LightBulb();
$switch = new Switch($bulb);
$switch->operate();

ในตัวอย่างนี้ class `Switch` ขึ้นอยู่กับ interface `Switchable` ทำให้เราสามารถเปลี่ยนเป็นอุปกรณ์ชนิดอื่นที่ implement `Switchable` ได้โดยไม่ต้องแก้ไข class `Switch`

การนำหลักการ SOLID มาใช้ในการออกแบบ OOP จะช่วยให้เราสร้างซอฟต์แวร์ที่มีคุณภาพสูง, ยืดหยุ่น, บำรุงรักษาง่าย, และสามารถปรับเปลี่ยนได้ตามความต้องการของระบบที่เปลี่ยนแปลงไป

Office.in.th Custom Service

この構成をもとにシステムを開発・カスタマイズしたいですか?

Office.in.th は、エンタープライズ Web アプリケーション、API 連携、官民の業務管理システムの設計・開発を承り、長期的なサポートも提供します。

ソフトウェア開発サービスを見る