メインコンテンツへ移動

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

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

การทำ Dependency Injection Container

บทความนี้แนะนำ Dependency Injection Container (DIC) ซึ่งเป็นเครื่องมือที่ช่วยจัดการ dependencies ของคลาสในโปรเจกต์ PHP อย่างมีประสิทธิภาพ เพื่อแก้ปัญหาความยุ่งย...

การทำ Dependency Injection Container

ในบทก่อนหน้า เราได้เรียนรู้เกี่ยวกับการจัดการ Secret Keys ด้วย Environment Variables ซึ่งเป็นวิธีที่ช่วยให้เราแยกค่าคอนฟิกออกจากโค้ดหลักและจัดการได้ง่ายขึ้น ในบทนี้ เราจะมาต่อยอดแนวคิดของการจัดการ Dependencies โดยใช้สิ่งที่เรียกว่า Dependency Injection Container (DIC) ซึ่งเป็นเครื่องมือที่ช่วยให้เราจัดการ Dependencies ของคลาสต่างๆ ในโปรเจกต์ PHP ของเราได้อย่างมีประสิทธิภาพและเป็นระเบียบมากขึ้น

Container Design: ทำไมต้องมี Container?

ลองจินตนาการว่าคุณกำลังสร้างแอปพลิเคชันที่มีหลายคลาส แต่ละคลาสต้องการ Dependencies ที่แตกต่างกัน เช่น คลาส UserController อาจต้องการ UserRepository และ Logger ในการทำงาน หากเราสร้าง Dependencies เหล่านี้โดยตรงในคลาส UserController จะเกิดอะไรขึ้น?

class UserController {
    private $userRepository;
    private $logger;

    public function __construct() {
        $this->userRepository = new UserRepository();
        $this->logger = new Logger();
    }

    // ...
}

ปัญหาคือ:

  • ความยุ่งยากในการทดสอบ: การสร้าง Dependencies ภายในคลาสทำให้ยากต่อการ Mock หรือ Stub Dependencies เหล่านี้ในการทดสอบ Unit Testing
  • ความซับซ้อนในการเปลี่ยนแปลง: หากเราต้องการเปลี่ยน Logger เป็น NewLogger เราต้องแก้ไขโค้ดใน UserController โดยตรง
  • การพึ่งพา (Coupling) ที่สูง: UserController ขึ้นอยู่กับ implementation ของ UserRepository และ Logger อย่างมาก

Dependency Injection Container (DIC) เข้ามาช่วยแก้ปัญหาเหล่านี้ โดย DIC จะทำหน้าที่เป็นศูนย์กลางในการจัดการ Dependencies ทั้งหมดของแอปพลิเคชันของเรา แทนที่จะให้แต่ละคลาสสร้าง Dependencies เอง เราจะให้ DIC เป็นผู้สร้างและส่ง Dependencies เหล่านั้นให้คลาสต่างๆ

Trade-offs: การใช้ DIC ทำให้โค้ดมีความซับซ้อนขึ้นเล็กน้อยในช่วงแรก แต่ในระยะยาวจะช่วยให้โค้ดมีความยืดหยุ่น, ทดสอบได้ง่าย, และบำรุงรักษาได้ง่ายขึ้น

Binding Interfaces: กำหนด Dependency อย่างไร?

ใน DIC เรามักจะใช้ Interface เป็น Abstraction Layer เพื่อให้โค้ดของเราไม่ผูกติดกับ Implementation ใด Implementation หนึ่งโดยเฉพาะ ตัวอย่างเช่น:

interface UserRepositoryInterface {
    public function findById(int $id): ?User;
}

class DatabaseUserRepository implements UserRepositoryInterface {
    public function findById(int $id): ?User {
        // ดึงข้อมูลผู้ใช้จากฐานข้อมูล
    }
}

เราสามารถ Binding Interface UserRepositoryInterface กับ Implementation DatabaseUserRepository ใน DIC ได้ดังนี้ (ตัวอย่างนี้ใช้ Psr\Container interface ซึ่งเป็นมาตรฐานที่หลาย DIC libraries รองรับ):

use Psr\Container\ContainerInterface;
use DI\ContainerBuilder; // ตัวอย่างจาก library DI

$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
    UserRepositoryInterface::class => \DI\create(DatabaseUserRepository::class),
]);
$container = $containerBuilder->build();

โค้ดนี้บอกว่า เมื่อใดก็ตามที่มีการร้องขอ UserRepositoryInterface ให้สร้าง Instance ของ DatabaseUserRepository แล้วส่งกลับไป

เหตุผลเชิงออกแบบ: การ Binding Interface ช่วยให้เราสามารถสลับ Implementation ของ Dependency ได้ง่าย หากเราต้องการเปลี่ยนจาก DatabaseUserRepository เป็น CacheUserRepository เราเพียงแค่แก้ไข Binding ใน DIC เท่านั้น โค้ดส่วนอื่น ๆ ที่ใช้ UserRepositoryInterface ไม่จำเป็นต้องเปลี่ยนแปลง

Resolving Dependencies: ใช้งาน Container อย่างไร?

เมื่อเรา Binding Dependencies ใน DIC เรียบร้อยแล้ว เราสามารถ Resolving Dependencies เหล่านั้นได้ในคลาสต่างๆ ตัวอย่างเช่น:

class UserController {
    private $userRepository;

    public function __construct(UserRepositoryInterface $userRepository) {
        $this->userRepository = $userRepository;
    }

    public function getUser(int $id) {
        $user = $this->userRepository->findById($id);
        // ...
    }
}

สังเกตว่า UserController รับ UserRepositoryInterface เป็น Dependency ผ่าน Constructor นี่คือหลักการของ Constructor Injection

ในการสร้าง Instance ของ UserController เราจะใช้ DIC:

$userController = $container->get(UserController::class);

DIC จะทำการ Resolving Dependencies ของ UserController โดยอัตโนมัติ โดยจะสร้าง Instance ของ DatabaseUserRepository (ตามที่เราได้ Binding ไว้) แล้วส่งให้กับ Constructor ของ UserController

แนวปฏิบัติที่ดี:

  • Constructor Injection: ใช้ Constructor Injection เพื่อให้ Dependencies ชัดเจนและง่ายต่อการทดสอบ
  • Interface-based Programming: ใช้ Interface เพื่อ Abstraction และ Coupling ที่ต่ำ
  • Single Responsibility Principle: แต่ละคลาสควรมีหน้าที่เดียว และ Dependencies ควรสะท้อนถึงหน้าที่นั้น

ตัวอย่างการใช้งาน Dependency Injection Container อย่างง่าย

เพื่อให้เห็นภาพชัดเจนยิ่งขึ้น เราจะสร้าง DIC อย่างง่ายด้วยตนเอง (เพื่อความเข้าใจ ไม่แนะนำให้ใช้ใน Production)

class SimpleContainer {
    private $bindings = [];

    public function bind(string $abstract, $concrete) {
        $this->bindings[$abstract] = $concrete;
    }

    public function resolve(string $abstract) {
        if (isset($this->bindings[$abstract])) {
            $concrete = $this->bindings[$abstract];

            if (is_callable($concrete)) {
                return $concrete($this); // Pass the container itself for resolving nested dependencies
            }

            return $concrete; // Assume it's an instance or class name
        }

        throw new \Exception("No binding found for {$abstract}");
    }

    public function get(string $id) {
        try {
            return $this->resolve($id);
        } catch (\Exception $e) {
            // Attempt to auto-resolve if no binding found
            try {
                $reflection = new \ReflectionClass($id);
                if (!$reflection->isInstantiable()) {
                    throw new \Exception("Class {$id} is not instantiable");
                }

                $constructor = $reflection->getConstructor();
                if (is_null($constructor)) {
                    return new $id(); // No constructor, just create the instance
                }

                $parameters = $constructor->getParameters();
                $dependencies = [];
                foreach ($parameters as $parameter) {
                    $dependencyType = $parameter->getType();
                    if ($dependencyType === null) {
                        throw new \Exception("Unable to resolve dependency for {$parameter->getName()} in {$id}'s constructor.  Type hint is missing.");
                    }

                    $dependencyClassName = $dependencyType->getName();
                    $dependencies[] = $this->get($dependencyClassName); // Recursively resolve dependencies
                }

                return $reflection->newInstanceArgs($dependencies);

            } catch (\ReflectionException $reflectionException) {
                throw new \Exception("Unable to auto-resolve {$id}: " . $reflectionException->getMessage());
            }
        }
    }
}

// ตัวอย่างการใช้งาน
$container = new SimpleContainer();

// Binding
$container->bind(UserRepositoryInterface::class, new DatabaseUserRepository());

// Resolving
$userController = new UserController($container->get(UserRepositoryInterface::class));

// Or, with auto-resolution (if enabled and no binding exists):
// $userController = $container->get(UserController::class); //  Requires type-hinted constructor parameters in UserController

โค้ดนี้แสดงให้เห็นหลักการพื้นฐานของ DIC: การ Binding Dependencies และการ Resolving Dependencies แม้ว่าจะเป็น Implementation อย่างง่าย แต่ก็ช่วยให้เราเข้าใจแนวคิดของ DIC ได้ดียิ่งขึ้น

สรุป

Dependency Injection Container เป็นเครื่องมือที่มีประโยชน์อย่างมากในการจัดการ Dependencies ของแอปพลิเคชัน PHP ช่วยให้โค้ดมีความยืดหยุ่น, ทดสอบได้ง่าย, และบำรุงรักษาได้ง่ายขึ้น แม้ว่าการใช้งาน DIC อาจต้องใช้ความพยายามในการเรียนรู้ในช่วงแรก แต่ผลตอบแทนที่ได้ในระยะยาวคุ้มค่าอย่างแน่นอน ในบทถัดไป เราจะพูดถึง Event Dispatcher ซึ่งเป็นอีกหนึ่งเครื่องมือที่ช่วยให้เราสร้างแอปพลิเคชันที่ยืดหยุ่นและขยายได้ง่ายยิ่งขึ้น

Office.in.th Custom Service

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

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

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