ในฐานะนักพัฒนา PHP ระดับ Senior การออกแบบโค้ดที่สะอาด (Clean Code) และสามารถบำรุงรักษาได้ง่าย (Maintainable) ถือเป็นหัวใจสำคัญของการพัฒนาระบบขนาดใหญ่ หนึ่งในหลักการพื้นฐานที่สุดของ Object-Oriented Design ที่เราต้องยึดถือคือ Single Responsibility Principle (SRP) หรือ “หลักการความรับผิดชอบเดียว” ซึ่งเป็นส่วนหนึ่งของ SOLID Principles
แนวคิดหลัก: SRP ระบุว่า คลาส (Class) หรือโมดูลใด ๆ ควรมีเพียงเหตุผลในการเปลี่ยนแปลง (Reason to Change) เพียงอย่างเดียวเท่านั้น กล่าวคือ หากคลาส A มีหน้าที่จัดการการคำนวณธุรกิจ (Business Logic), การเชื่อมต่อฐานข้อมูล (Database Access), และการส่งอีเมลแจ้งเตือน (Notification) ทั้งหมดรวมอยู่ในที่เดียวกัน เมื่อเราต้องการเปลี่ยนวิธีการส่งอีเมล เราจะต้องไปแก้ไขไฟล์ที่มีโค้ดเกี่ยวกับการบันทึกข้อมูลด้วย ซึ่งเป็นการเพิ่มความเสี่ยงและทำให้คลาสมีความซับซ้อนเกินจำเป็น
เหตุผลที่เราต้องใช้ SRP
- ลดการพึ่งพา (Decoupling): เมื่อแต่ละคลาสรับผิดชอบเพียงเรื่องเดียว การเปลี่ยนแปลงในส่วนหนึ่งจะไม่ส่งผลกระทบต่อส่วนอื่น ๆ โดยไม่ตั้งใจ
- เพิ่มความสามารถในการทดสอบ (Testability): เราสามารถเขียน Unit Test สำหรับคลาสย่อยแต่ละตัวได้อย่างอิสระ ทำให้การหาจุดบกพร่องทำได้ง่ายและแม่นยำ
- เพิ่มความเข้าใจ (Readability): โค้ดที่ถูกแบ่งหน้าที่อย่างชัดเจนจะทำให้เพื่อนร่วมทีมคนอื่น ๆ เข้าใจโครงสร้างของระบบได้รวดเร็วยิ่งขึ้น
ตัวอย่างการใช้งานและรูปแบบโค้ด (Code Examples)
เราจะมาดูตัวอย่างคลาสที่ละเมิดหลักการ SRP ก่อน จากนั้นจึงทำการ Refactor ให้เป็นไปตามหลักการเพื่อแสดงให้เห็นถึงความแตกต่างในการออกแบบ
บริบทของโค้ดตัวอย่าง: ระบบจัดการคำสั่งซื้อ (Order Processing) ที่ต้องทำ 3 อย่าง คือ 1. คำนวณราคารวม, 2. บันทึกข้อมูลลงฐานข้อมูล, และ 3. ส่งอีเมลยืนยัน
<?php
// =========================================================
// ❌ BAD PRACTICE: คลาสที่ละเมิด SRP (OrderManager)
// มีหน้าที่หลายอย่างเกินไป ทั้ง Business Logic, DB Access, และ Notification
// =========================================================
class OrderManagerBad {
private $dbConnection; // สมมติว่ามีการเชื่อมต่อฐานข้อมูลอยู่แล้ว
public function __construct($connection) {
$this->dbConnection = $connection;
}
/**
* จัดการคำสั่งซื้อทั้งหมด (รวมถึงการบันทึกและการแจ้งเตือน)
*/
public function processOrder(array $orderData, float $taxRate): bool {
// 1. Business Logic: คำนวณราคารวม
$totalPrice = array_sum($orderData['items']) * (1 + $taxRate);
if ($totalPrice <= 0) {
return false; // ล้มเหลวในการคำนวณ
}
// 2. Data Persistence: บันทึกข้อมูลลงฐานข้อมูล
$sql = "INSERT INTO orders (user_id, total_price, status) VALUES (?, ?, 'COMPLETED')";
$stmt = $this->dbConnection->prepare($sql);
$successDb = $stmt->execute([$orderData['user_id'], $totalPrice]);
if (!$successDb) {
return false; // ล้มเหลวในการบันทึกข้อมูล
}
// 3. Notification: ส่งอีเมลยืนยันคำสั่งซื้อ
$subject = "Order Confirmation";
$body = "Dear User, Your order of $" . number_format($totalPrice, 2) . " has been confirmed.";
$emailSent = $this->sendEmailNotification("[email protected]", $subject, $body);
if (!$emailSent) {
// ถ้าส่งอีเมลล้มเหลว อาจจะต้องมีการ Rollback ข้อมูลใน DB ด้วย!
echo "Warning: Order saved but email failed to send.";
}
return true;
}
/**
* ฟังก์ชันจำลองการเชื่อมต่อฐานข้อมูล (เพื่อความสมบูรณ์ของตัวอย่าง)
*/
private function sendEmailNotification(string $to, string $subject, string $body): bool {
// โค้ดจริงจะใช้ PHPMailer หรือบริการอื่น ๆ
echo "--- [LOG] Attempting to send email to {$to} ---\n";
return true; // สมมติว่าสำเร็จเสมอ
}
}
/* --------------------------------------------------------- */
/* 🚀 GOOD PRACTICE: การ Refactor ตามหลัก SRP (Dependency Injection) */
/* --------------------------------------------------------- */
// 1. Interface/Contract สำหรับการกำหนดหน้าที่ (Decoupling)
interface PaymentGateway {
public function processPayment(float $amount, string $token): bool;
}
// 2. Class ที่รับผิดชอบเฉพาะเรื่องการคำนวณธุรกิจ
class OrderCalculator {
/**
* คำนวณราคารวมของสินค้าทั้งหมด
*/
public function calculateTotal(array $items, float $taxRate): float {
$subtotal = array_sum($items);
return $subtotal * (1 + $taxRate);
}
}
// 3. Class ที่รับผิดชอบเฉพาะเรื่องการบันทึกข้อมูล (Persistence)
class OrderRepository {
private $dbConnection;
public function __construct($connection) {
$this->dbConnection = $connection;
}
/**
* บันทึกคำสั่งซื้อลงฐานข้อมูลเท่านั้น
*/
public function save(int $userId, float $totalPrice): bool {
// ใช้ Prepared Statements เสมอเพื่อป้องกัน SQL Injection
$sql = "INSERT INTO orders (user_id, total_price, status) VALUES (?, ?, 'COMPLETED')";
$stmt = $this->dbConnection->prepare($sql);
return $stmt->execute([$userId, $totalPrice]);
}
}
// 4. Class ที่รับผิดชอบเฉพาะเรื่องการแจ้งเตือน (Notification/External Service)
class EmailService {
/**
* ส่งอีเมลยืนยันคำสั่งซื้อเท่านั้น
*/
public function sendConfirmation(string $email, float $total): bool {
echo "--- [LOG] Successfully sent confirmation email to {$email} for $" . number_format($total, 2) . " ---\n";
// ในความเป็นจริงควรมีการจัดการ Exception ที่นี่
return true;
}
}
// 5. Orchestrator: คลาสที่รวบรวมการทำงาน (ใช้ Dependency Injection)
class OrderService {
private $calculator;
private $repository;
private $emailService;
public function __construct(OrderCalculator $calc, OrderRepository $repo, EmailService $emailSvc) {
// รับ Dependencies ผ่าน Constructor ทำให้ทดสอบง่ายมาก (Testability)
$this->calculator = $calc;
$this->repository = $repo;
$this->emailService = $emailSvc;
}
/**
* กระบวนการหลัก: ใช้คลาสย่อยที่รับผิดชอบเฉพาะด้านเท่านั้น
*/
public function processOrder(int $userId, array $items, float $taxRate, string $userEmail): bool {
// 1. Calculate (Responsibility: Calculation)
$totalPrice = $this->calculator->calculateTotal($items, $taxRate);
if ($totalPrice <= 0) {
return false;
}
try {
// 2. Save (Responsibility: Persistence/Database)
if (!$this->repository->save($userId, $totalPrice)) {
throw new Exception("Failed to save order to database.");
}
// 3. Notify (Responsibility: External Communication)
$this->emailService->sendConfirmation($userEmail, $totalPrice);
return true;
} catch (Exception $e) {
// การจัดการข้อผิดพลาดรวมศูนย์ที่นี่เท่านั้น
error_log("Order processing failed: " . $e->getMessage());
return false;
}
}
}
// =========================================================
// 🧪 การใช้งานจริง (Simulation)
// =========================================================
// จำลองการเชื่อมต่อ DB และ Dependency Injection
$dbConnectionMock = new class {
public function prepare($sql) { return new stdClass(); } // Mock statement
};
// สร้าง Instance ของคลาสที่แยกหน้าที่แล้ว
$calculator = new OrderCalculator();
$repository = new OrderRepository($dbConnectionMock);
$emailService = new EmailService();
// ส่งมอบ Dependencies ให้กับ Service หลัก (OrderService)
$orderService = new OrderService($calculator, $repository, $emailService);
// ข้อมูลจำลอง
$user_id = 101;
$items = [100.0, 50.0]; // สินค้าสองชิ้น
$tax_rate = 0.07; // VAT 7%
$user_email = "[email protected]";
echo "========================================\n";
echo "🚀 Running Order Processing (SRP Compliant)\n";
echo "========================================\n";
if ($orderService->processOrder($user_id, $items, $tax_rate, $user_email)) {
echo "\n✅ SUCCESS: Order processed successfully and all services notified.\n";
} else {
echo "\n❌ FAILURE: Failed to process order due to system error.\n";
}
?>
ข้อควรระวัง Security และ Best Practices
- คำนึงถึงความปลอดภัย (Security): การแยกหน้าที่ช่วยให้เราสามารถกำหนดจุดตรวจสอบความถูกต้องของข้อมูล (Validation) ได้อย่างเฉพาะเจาะจง เช่น ในคลาส `OrderCalculator` เราควรตรวจสอบว่าราคาที่รับเข้ามาเป็นตัวเลขบวกเสมอ และในชั้น Repository ต้องใช้ Prepared Statements เสมอเพื่อป้องกัน SQL Injection
- Performance: การออกแบบที่ดีต้องมาพร้อมกับการปรับปรุงประสิทธิภาพ ควรพิจารณาการใช้รูปแบบ Design Pattern เช่น Dependency Injection (DI) เพื่อให้สามารถ Mock หรือ Stub Dependencies ได้ง่ายสำหรับการทดสอบ และควรมีการนำ Caching Layer เข้ามาจัดการข้อมูลที่ถูกเรียกซ้ำ ๆ
- Error Handling: เมื่อหน้าที่ถูกแยกออกไปแล้ว การจัดการข้อผิดพลาดก็ต้องทำในระดับ Orchestrator (เช่น `OrderService`) โดยการใช้
try...catchและโยน Custom Exception ที่เฉพาะเจาะจง เช่นPaymentFailedExceptionหรือInvalidInputExceptionเพื่อให้ผู้เรียกใช้งานสามารถตอบสนองต่อความล้มเหลวได้อย่างเหมาะสม
สรุปและการนำไปประยุกต์ใช้งาน
การปฏิบัติตามหลัก Single Responsibility Principle ไม่ใช่แค่การจัดระเบียบโค้ด แต่เป็นการลงทุนในความยั่งยืนของระบบ (System Sustainability) เมื่อโปรเจกต์ขยายตัวและมีนักพัฒนาเข้ามาหลายคน การที่แต่ละคลาสรับผิดชอบเพียงเรื่องเดียวจะช่วยลด “Cognitive Load” ของทีมงานได้อย่างมหาศาล
- สรุปภาพรวม: แทนที่จะสร้าง Super-Class ที่ทำทุกอย่าง เราควรใช้แนวคิด Composition over Inheritance โดยการให้ Class หลัก (เช่น `OrderService`) ทำหน้าที่เป็นผู้ประสานงาน (Orchestrator) และเรียกใช้บริการย่อย ๆ (Services) ที่ถูกออกแบบมาตาม SRP เช่น การคำนวณ, การบันทึกข้อมูล, และการสื่อสารภายนอก
กรณีที่ควรนำไปประยุกต์ใช้งาน
- ระบบที่มี Domain Logic ซับซ้อน (เช่น ระบบบัญชี, ระบบ E-commerce)
- เมื่อต้องมีการเชื่อมต่อกับบริการภายนอกหลายตัว (Payment Gateways, SMS Services, Email APIs)
- ทุกครั้งที่โค้ดเริ่มมีขนาดใหญ่เกินกว่า 100 บรรทัด และคุณรู้สึกว่า “ถ้าเปลี่ยนตรงนี้ อาจจะพังตรงนั้น” นั่นคือสัญญาณเตือนให้คุณต้องแยกหน้าที่ออกไปตามหลัก SRP ทันที
อ่านเพิ่มเติม