ป้ายกำกับ: PaymentService

PHP: การหลีกเลี่ยง Global Variables และ Global FunctionsPHP: การหลีกเลี่ยง Global Variables และ Global Functions

ในฐานะนักพัฒนา PHP ระดับ Senior สิ่งที่ต้องให้ความสำคัญสูงสุดไม่ใช่แค่การทำให้โค้ดทำงานได้ (Functionality) แต่คือการทำให้โค้ดนั้นสามารถบำรุงรักษาได้ง่าย (Maintainability), ทดสอบได้ง่าย (Testability), และขยายขนาดได้ (Scalability) การใช้ Global Variables หรือ Global Functions เป็นหนึ่งในรูปแบบการเขียนโค้ดที่เสี่ยงที่สุดในการทำลายหลักการเหล่านี้

Global Scope คือพื้นที่ของตัวแปรหรือฟังก์ชันที่สามารถเข้าถึงได้จากทุกส่วนของโปรแกรม ไม่ว่าจะเป็นไฟล์ไหนก็ตาม เมื่อเราพึ่งพา Global State มากเกินไป โค้ดของเราจะกลายเป็นสิ่งที่เรียกว่า “Spaghetti Code” เพราะการเปลี่ยนแปลงค่าในตัวแปรใดตัวแปรหนึ่ง ณ จุด A อาจส่งผลกระทบต่อตรรกะการทำงานที่จุด Z โดยที่เราไม่รู้ล่วงหน้า นี่คือปัญหาของการพึ่งพาภายนอก (External Dependency) ที่ไม่มีการควบคุม

หลักการสำคัญในการหลีกเลี่ยงปัญหานี้คือการใช้แนวคิด Encapsulation และ Dependency Injection (DI) เราควรส่งผ่านข้อมูลหรือบริการที่จำเป็น (Dependencies) เข้าไปในฟังก์ชันหรือคลาสอย่างชัดเจน แทนที่จะปล่อยให้มันลอยอยู่ใน Global Scope การทำเช่นนี้จะทำให้โค้ดของเราเป็นแบบ Modular, Predictable, และสามารถ Unit Test ได้ง่ายมาก


ตัวอย่างการใช้งานและรูปแบบโค้ด (Code Examples)

ตัวอย่างนี้แสดงให้เห็นถึงปัญหาของการใช้ Global Variable ในการจัดการค่า Configuration และวิธีการแก้ไขโดยการห่อหุ้มตรรกะเหล่านั้นไว้ใน Class Service ที่รับ Dependencies เข้ามาผ่าน Constructor

สถานการณ์เดิม (Bad Practice): เรามีฟังก์ชันหลายตัวที่ต้องเข้าถึงค่าตั้งต้น เช่น API Key หรือ Database Connection ซึ่งถูกกำหนดเป็น Global Variable ทำให้ถ้าเราต้องการเปลี่ยนการเชื่อมต่อหรือเพิ่มค่าใหม่ ต้องไปแก้ไขทุกไฟล์ที่เรียกใช้ Global ตัวแปรนั้น

สถานการณ์ที่ดีขึ้น (Good Practice): เราสร้างคลาส PaymentService ที่รับ Object ของ Configuration และ Logger เข้ามาใน Constructor เมื่อใดก็ตามที่โค้ดส่วนอื่นต้องการใช้งานบริการนี้ ก็จะต้องส่งผ่าน Dependencies เหล่านี้ให้ครบถ้วน ทำให้การไหลของข้อมูลชัดเจนและสามารถ Mocking สำหรับการทดสอบได้ง่าย

<?php

// ====================================================
// ❌ BAD PRACTICE: การใช้ Global Variables และ Functions
// ====================================================

// กำหนดค่า Config แบบ Global (อันตราย!)
$global_api_key = "abc123xyz";
$global_logger = function($msg) { echo "[LOG] $msg\n"; }; // Global Function

function processPaymentGlobal(string $userId, float $amount): bool {
    // ต้องพึ่งพาค่า global ทั้งหมด ทำให้โค้ดนี้ไม่สามารถทดสอบได้โดยไม่มีการตั้งค่าภายนอก
    if (empty($global_api_key)) {
        echo "Error: API Key is missing.\n";
        return false;
    }

    $message = "Processing payment for user $userId, amount $amount using key $global_api_key...";
    $global_logger($message); // เรียกใช้ Global Function
    // ... โค้ดการเรียก API จริงๆ
    echo "Payment processed successfully (Global Scope).\n";
    return true;
}

echo "--- [BAD PRACTICE RUN] ---\n";
processPaymentGlobal("user_001", 99.99);


// ====================================================
// ✅ GOOD PRACTICE: การใช้ Class และ Dependency Injection (DI)
// ====================================================

/**
 * Interface สำหรับ Logger เพื่อให้สามารถ Mock ได้ง่ายในการทดสอบ
 */
interface LoggerInterface {
    public function log(string $message): void;
}

/**
 * Concrete Implementation ของ Logger
 */
class FileLogger implements LoggerInterface {
    private string $logFile;
    public function __construct(string $logFile) {
        $this->logFile = $logFile;
    }
    public function log(string $message): void {
        file_put_contents($this->logFile, "[" . date('Y-m-d H:i:s') . "] " . $message . "\n", FILE_APPEND);
        echo "[Logger] Logged message to file.\n"; // แสดงผลใน Console แทนการเขียนไฟล์จริงเพื่อความง่ายในการสาธิต
    }
}

/**
 * Service Class ที่จัดการ Payment โดยรับ Dependencies เข้ามาผ่าน Constructor
 */
class PaymentService {
    private string $apiKey;
    private LoggerInterface $logger;

    // Dependency Injection: รับ Config และ Logger เป็น Argument
    public function __construct(string $apiKey, LoggerInterface $logger) {
        $this->apiKey = $apiKey;
        $this->logger = $logger;
    }

    public function processPayment(string $userId, float $amount): bool {
        // โค้ดนี้พึ่งพาเฉพาะ Properties ที่ถูกส่งเข้ามาเท่านั้น ไม่ต้องพึ่ง Global Scope
        if (empty($this->apiKey)) {
            $this->logger->log("ERROR: API Key is not configured.");
            return false;
        }

        $message = "Processing payment for user $userId, amount $amount using key $this->apiKey...";
        $this->logger->log($message); 
        // ... โค้ดการเรียก API จริงๆ โดยใช้ $this->apiKey
        echo "Payment processed successfully (Service Class).\n";
        return true;
    }
}

// --- การใช้งานในส่วน Main Logic ---
try {
    // 1. เตรียม Dependencies ก่อน
    $configApiKey = "secure_api_key_v2"; // ค่า Config ควรมาจาก Environment Variables หรือ Config Reader
    $loggerDependency = new FileLogger("app.log"); // สร้าง Logger Instance

    // 2. ส่งผ่าน Dependencies ให้ Service Class
    $paymentService = new PaymentService($configApiKey, $loggerDependency);

    echo "\n--- [GOOD PRACTICE RUN] ---\n";
    $paymentService->processPayment("user_002", 199.50);

} catch (Exception $e) {
    // ควรมีการดักจับ Exception ที่นี่เสมอ
    echo "An unexpected error occurred: " . $e->getMessage() . "\n";
}

?>

ข้อควรระวัง Security และ Best Practices

  • คำนึงถึงความปลอดภัย (Security): การใช้ Global State ทำให้เกิดปัญหา Race Condition ได้ง่าย หากหลาย Request เข้ามาพร้อมกันและพยายามแก้ไขตัวแปร Global เดียวกัน อาจทำให้ข้อมูลผิดพลาดได้เสมอ ควรแทนที่ด้วยการจัดการสถานะผ่าน Object หรือ Database Transaction ที่มีการล็อค (Locking)
  • Performance: แม้ว่า Global Scope จะไม่ได้ช้ากว่า Local Scope อย่างมีนัยสำคัญ แต่ในเชิงสถาปัตยกรรม การพึ่งพา Global State ทำให้โค้ดทำงานได้ยากต่อการทำ Optimization และ Unit Testing ซึ่งเป็นต้นทุนที่สูงกว่ามาก ควรใช้ Type Hinting (เช่น LoggerInterface ในตัวอย่าง) เพื่อให้ PHP Engine สามารถตรวจสอบและ Optimize โค้ดได้อย่างเต็มที่
  • Error Handling: เมื่อโค้ดพึ่งพา Global Variables มากเกินไป การหาจุดที่ทำให้ค่าเหล่านั้นผิดพลาดจึงยากมาก ควรใช้ Dependency Injection Container (DIC) เช่น Symfony Service Container หรือ Laravel IoC เพื่อจัดการการสร้างและส่งผ่าน Dependencies อย่างเป็นระบบ และต้องมีการห่อหุ้มการเรียกใช้บริการเหล่านี้ด้วย try...catch เสมอ

สรุปและการนำไปประยุกต์ใช้งาน

การหลีกเลี่ยง Global Variables และ Functions ไม่ใช่แค่เรื่องของ “สไตล์” แต่เป็นรากฐานของการเขียนโปรแกรมเชิงวัตถุ (OOP) ที่แข็งแกร่งและสามารถขยายขนาดได้จริง เมื่อคุณเริ่มโครงการใหม่ๆ ให้ยึดหลักการ Dependency Injection เป็นแนวทางปฏิบัติหลักเสมอ

ในระดับ Production Environment ควรใช้ Frameworks สมัยใหม่ เช่น Laravel หรือ Symfony ซึ่งมีระบบ Service Container และ Autowiring ที่ช่วยให้เราไม่ต้องจัดการกับการส่งผ่าน Dependencies ด้วยมือ (Manual DI) แต่ก็ยังคงรักษาหลักการ Encapsulation ไว้ได้ ทำให้โค้ดของเราสะอาด, ทดสอบง่าย, และพร้อมรับมือกับความซับซ้อนของแอปพลิเคชันขนาดใหญ่ได้อย่างมั่นใจ


อ่านเพิ่มเติม