Service Locator thùng chứa đa năng?

1. Service Locator là gì?

Trước khi đi vào chi tiết câu hỏi Service Locator là gì? chúng ta cùng xem xét vấn đề sau trong lập trình. Một class A phụ thuộc vào một service hoặc một component là những lớp cụ thể (concrete class) trong lúc chạy ứng dụng.

Dependency runtime

Hình 1: Class A phụ thuộc chặt chẽ với Service A, Service B

Sự phụ thuộc của các class vào các service này sẽ có một số vấn đề cần phải giải quyết:

  • Nếu thay thế hoặc cập nhật các service phụ thuộc, chúng ta cần thay đổi mã nguồn của class A.
  • Các lớp cụ thể của một phụ thuộc có thể dùng được trong thời gian chạy hay không?
  • Các class như vậy không tách biệt và rất khó cho unit test bởi chúng phụ thuộc trực tiếp, như vậy các phụ thuộc không thể thay thế bởi các stub hoặc mock trong test.
  • Các class cần phải viết những đoạn mã cho việc tạo, tìm kiếm và quản lý các phụ thuộc.

Giải pháp cho vấn đề này chính là áp dụng Service Locator pattern, nó tạo ra một class chứa các tham chiếu đến các service và nó đóng gói các xử lý nghiệp vụ để xác định các service. Hình dưới đây mô tả cách thức Service Locator hoạt động.

Service Locator Pattern

Service Locator không quan tâm đến việc các service hoạt động như thế nào, nó chỉ quan tâm đến việc các service này được đăng ký và cách định vị các service này. Thông thường Service Locator được kết hợp với Factory Pattern hoặc Dependency Injection Pattern để có thể tạo ra các instance của service.

Service Locator có nhiều điểm tương đồng với Factory Pattern và Register Pattern nếu để ý kỹ thì chúng vận hành khác nhau:

  • Factory Pattern tạo và trả về đối tượng này, tuy nhiên nó không giữ đối tượng được tạo này để sử dụng về sau, hay nói một cách khác Factory pattern chỉ tạo và trả về đối tượng.
  • Register Pattern lưu giữ các tham chiếu đến đối tượng (chỉ đơn giản là lưu tên đối tượng) để có thể sử dụng đối tượng nhưng nó không tạo ra đối tượng.
  • Service Locator lưu giữ tên đối tượng và tạo ra đối tượng nếu được định nghĩa tức là nó làm cả hai nhiệm vụ của Factory pattern và Register pattern.

2. Ví dụ về Service Locator

Chúng ta bắt đầu với Service Locator bằng một ví dụ về những chiếc xe ô tô (car) và người lái xe (driver). Người lái xe cần lái xe ô tô, nó được mô phỏng lại bởi đoạn code sau:

class Car {
    public function run() {
        echo 'Brùm brùm...';
    }
}

class Driver {
    private $car;
    public function __construct() {
        $this->car = new Car();
    }
    public function drive() {
        $this->car->run();
    }
}

class Driver có phương thức khởi tạo sẽ gán một đối tượng xe ô tô mới vào thuộc tính $car và phương thức driver() sẽ thực hiện phương thức run() của class Car. Các class đã được định nghĩa, tiếp theo chúng ta tạo ra người lái xe và cho người này lái xe:

$driver = new Driver();
$dirver->drive(); // Brùm brùm...

Ở ví dụ trên chúng ta thấy class Driver phụ thuộc chặt chẽ (trong lập trình phần mềm thường sử dụng thuật ngữ tightly coupled) vào class Car. Trong thực tế, một người lái xe có thể lái nhiều chiếc xe khác nhau, do vậy nếu thay class Car bằng một class khác, chúng ta cần phải code lại class Driver, thật bất tiện phải không? Kinh nghiệm này đã được đúc kết trong nguyên lý đóng mở nằm trong Nguyên lý SOLID về thiết kế hướng đối tượng.

Để giải quyết vấn đề này, chúng ta tạo ra một interface cho xe ô tô và ngay lập tức các nhà sản xuất cho ra hai loại ô tô là BMV và Mercedes. Class Driver cũng thay đổi lại phương thức khởi tạo nó nhận giá trị truyền vào là đối tượng của các class được implement từ interface Car.

interface Car {
    public function run();
}

class BMW implements Car {
    public function run()
    {
        echo 'Bi em ví...!';
    }
}

class Mercedes implements Car {
    public function run()
    {
        echo 'Mẹc xà đí...!';
    }
}

class Driver {
    private $car;
    public function __construct(Car $car)
    {
        $this->car = $car;
    }
    public function drive()
    {
        $this->car->run();
    }
}

Khi đó Class Driver không còn phụ thuộc vào một Class cụ thể nào, trong đoạn code sau nói lên điều đó:

$mec = new Mercedes();
$bmv = new BMV();

// Lái xe 1 thích lái Mercedes
$driver1 = new Driver($mec);
$dirver1->drive(); // Mẹc xà đí...!

// Lái xe 2 thích lái BMV
$driver2 = new Driver($bmv);
$dirver2->drive(); // Bi em ví...!

Cách thức trên đã giải quyết được vấn đề về phụ thuộc tuy nhiên khi số lượng xe ô tô nhiều lên chúng ta sẽ cần một số quản lý như xe ô tô này hiện có đang chạy được không và vấn đề về sử dụng tài nguyên như chỉ tạo ra xe khi cần lái và các vấn đề về tạo xe mới. Service Locator giải quyết được bài toán này, nó tạo ra một gara ô tô và khi cần lái loại xe nào nó sẽ lấy ra cho chúng ta.

Trong phần tiếp theo chúng ta sẽ tạo ra cái gara đó:

interface ServiceLocatorInterface {
    public function set($name, $service);
    public function get($name);
    public function has($name);
    public function remove($name);
    public function clear();
}

class ServiceLocator implements ServiceLocatorInterface {
    protected $services = array();
  
    public function set($name, $service) {
        if (!is_object($service)) {
             throw new InvalidArgumentException( "Only objects can be registered with the locator.");
        }
        if (!in_array($service, $this->services, true)) {
            $this->services[$name] = $service;
        }
        return $this;
    }
    
    public function get($name) {
        if (!isset($this->services[$name])) {
            throw new RuntimeException( "The service $name has not been registered with the locator.");
        }
        return $this->services[$name];
    }

    public function has($name) {
        return isset($this->services[$name]);
    }
    
    public function remove($name) {
        if (isset($this->services[$name])) {
            unset($this->services[$name]);
        }
        return $this;
    }

    public function clear() {
        $this->services = array();
        return $this;
    }
}

Thay đổi lại class Driver để sử dụng cái gara này

class Driver {
    private $car;
    public function __construct(ServiceLocatorInterface $locator, $carname) {
        $this->car = $locator->get($carname);
    }
    public function drive() {
        $this->car->run();
    }
}

Đoạn code tiếp theo chúng ta sẽ sử dụng những chiếc gara được áp dụng Service Locator.

// Tạo ra cái gara
$locator = new ServiceLocator;
$locator->set('mec', new Mercerdes());
$locator->set('bmv', new BMV());

// Tạo ra ông lái xe và lấy xe Mercedes từ gara
$driver = new Driver($locator, 'mec');
$driver->drive(); // Mẹc xà đí...!

3. Lời kết

Service Locator là một pattern giải quyết vấn đề về sự phụ thuộc trong lập trình với việc áp dụng nguyên lý Inversion of Control. Nó cũng là một thuật ngữ hay được nhắc đến cùng với nguyên lý Dependency Inversion trong SOLID, IoC Container, DI container. Service Locator chính là một cách thực hiện của IoC Container. Trong các bài viết tiếp theo bạn sẽ tìm hiểu sâu về các khái niệm liên quan này, đây cũng là những chủ đề đang rất nóng trong lập trình phần mềm.

Mối liên hệ giữa IoC, DI, DI Container, IoC Container

3 thoughts on “Service Locator thùng chứa đa năng?

Add Comment