Dependency Injection là gì? 3 phương pháp DI thường gặp

Dependency Injection là 1 khái niệm quan trọng mà bất cứ lập trình viên nào cũng phải nắm được, Nó gần như xuất hiện ở tất cả các framework hiện đại của tất cả ngôn lập trình. Vậy Dependency Injection (DI) là gì? và DI trong laravel mạnh mẽ cỡ nào thì hôm nay hãy cùng mình tìm hiểu về Dependency Injection trong laravel nhé…!

Ở các bài viết trước mình cũng có đề cập tới các kiến thức cũng rất quan trọng trong laravel mà bất cứ lập trình viên laravel nào cũng nên biết để code của mình thêm ngon nghẻ hơn :v, nếu các bạn chưa đọc có thể xem qua nhé.

Dependency Injection là gì?

Dependency Injection là gì

Trên wikipedia có viết về DI như thế này:

In software engineering, dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally. Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs. The pattern ensures that an object or function which wants to use a given service should not have to know how to construct those services.

Theo Wikipedia

Rồi tôi đang đọc cái mọe gì thế này? Mình đọc xong cũng như trâu xem bảng tin, mếu hiểu =)). Hãy tạm quên những thứ này đi, đọc khó hiểu vãi lúa =)).

Túm lại thì Dependency Injection (DI) là 1 design pattern được sử dụng rất phổ biến. Hiểu 1 cách đơn giản thì nếu 1 Class A hoạt động phụ thuộc vào class B thì thay vì khởi tạo trực tiếp instance của class B bên trong class A thì ta sẽ truyền class B vào bên trong class A thông qua Constructor (hàm khởi tạo), hoặc hàm setter hoặc interface. Việc này sẽ làm giảm bớt sự phụ thuộc của các class với nhau, từ đó dễ bảo trì và nâng cấp chương trình hơn.

Quá trình truyền vào như vậy được gọi là inject, instance của class B mà class A cần để hoạt động được gọi là dependency. Chúng ta cùng xem ví dụ sau:

Giả sử ta có 1 class có tên là Computer và class này có các properties là monitor và keyboard, Khi này tạm gọi Class Computer phụ thuộc vào 2 class Keyboard và Monitor vì nếu 2 class đó thay đổi thì class Computer cũng thay đổi theo. Giờ thì sẽ code theo cả 2 cách sử dụng DI và non DI.

Cách 1: Code theo kiểu không sử dụng Dependency Injection: Theo logic mà chúng ta vẫn thường làm khi học ở trường thì bên trong class Computer ta sẽ viết như sau:

Class Computer {
    protected $monitor;
    protected $keyboard;
    public function __construct(){
        $this->monitor = new Monitor();
        $this->keyboard = new Keyboard();
    }
}

Và ở ngoài Service chúng ta muốn sử dụng thằng Computer này ta sẽ tạo ra object của nó bằng cú pháp:

$computer = new Computer();

Dễ thấy rằng như cách code này thì bên trong construction của class Computer ta sẽ khởi tạo tất cả properties bằng lệnh lệnh tạo đối tượng mới new.

Vấn đề bắt đầu nảy sinh nếu sau này ta muốn thay class Keyboard bằng class khác là class Voice => khi này ngoài việc phải sửa logic ở service thì ta phải vào bên trong class Computer và thay đổi tên class từ Keyboard sang Voice. Còn chưa nói tới việc nếu class Keyboard lỗi có thể khiến cho class Computer lỗi theo luôn. Việc này có tiềm ẩn rủi ro khá lớn vì 2 class Computer và Keyboard đã phụ thuộc chặt chẽ vào nhau. Thử hình dung ta có thêm 5-10 class phụ thuộc vào thằng Keyboard này nữa thì việc sửa cả 5-10 class đúng là ác mộng.

Về nguyên tắc thì cần sửa thằng nào thì chỉ sửa thằng đấy thôi, sửa thằng Keyboard mà lại phải sửa cả class Computer là rất bất cập => cách này không ổn.

Hồi mà mình còn đi học mình còn code theo kiểu này nữa cơ :v.

Class Computer {
    protected Monitor $monitor = new Monitor() ;
    protected Keyboard $keyboard = new Keyboard();
    public function __construct(){
    }
}

Cách này còn tồi tệ hơn cả cách trước vì nó mặc định khởi tạo khi load class luôn dù có dùng hay không :v. nhưng thôi cả lớp code như thế chứ riêng mình mếu đâu :v

 

Cách 2: Code theo kiểu có sử dụng Dependency Injection

Thay vì việc khởi tạo luôn properties $monitor và $keyboard bên trong construction của class Computer thì bây giờ chúng ta sẽ loại bỏ hết giá trị của properties ra khỏi ngoài class bằng cách truyền giá trị từ bên ngoài vào thông qua hàm khởi tạo:

Class Computer {
    protected $monitor;
    protected $keyboard;
    public function __construct(Monitor $monitor, Keyboard $keyboard){
        $this->monitor = $monitor;
        $this->keyboard = $keyboard;
    }
}

Và ở ngoài Service chúng ta muốn sử dụng thằng Computer này ta sẽ tạo ra object của nó bằng cú pháp:

$monitor = new Monitor();
$keyboard = new Keyboard();
$computer = new Computer($monitor, $keyboard);

Khi này nếu bạn có muốn thay đổi Keyboard thành Voice thì chúng ta cũng không cần phải đụng vào class Computer nữa mà chỉ cần sửa logic ở ngoài service mà thôi:

$monitor = new Monitor();
//$keyboard = new Keyboard(); => bỏ thằng này đi và thay bằng Voice
$voice = new Voice();
$computer = new Computer($monitor, $voice);

Tới đây chắc có bạn sẽ thắc mắc là rõ ràng bên trong Constructor đang để kiểu dữ liệu là Keyboard và bên dưới lại truyền vào kiểu dữ liệu là Voice? Thằng viết bài này bịp à :v

=> Trong thực tế không có ai code như trên cả, mình viết để cho các bạn hiểu tư duy của cách Inject thôi. Trong thực tế thì mọi người sẽ tận dụng tính đa hình của OOP, Khai báo kiểu dữ liệu truyền vào là Interface để có thể làm cho code linh hoạt hơn, sau này cũng dễ maintain hơn rất nhiều. Cũng chính việc kết hợp giữa DI và tính đa hình trong OOP này đã làm nên cả 1 hệ tư tưởng, nó mạnh mẽ tới nỗi từ khi phương pháp này ra đời thì các tác giả của những framework lớn đã phải ngay lập tức thêm kĩ thuật này cho đứa con của mình.

Class Computer sẽ được viết lại như sau:

Class Computer {
    protected $monitor;
    protected $keyboard;
    public function __construct(IDisplay $monitor, IInput $keyboard){
        $this->monitor = $monitor;
        $this->keyboard = $keyboard;
    }
}
//hãy bỏ qua vấn đề đặt tên biến nhé, mình để nguyên vậy để các bạn dễ focus
//các bạn chú ý là kiểu dữ liệu truyền vào bên trong constructor bây giờ không
//còn là tên class đơn thuần nữa mà nó là Interface => đây cũng là 1 biểu hiện
//của tính đa hình trong OOP

Và miễn là Keyboard và Voice cùng implement IInput thì việc thay đổi đã trở lên dễ dàng hơn rất nhiều rồi. Cách thức này chính là làm giảm sự phụ thuộc của 2 Class Computer và Keyboard đi. Khi thay đổi class Keyboard thì không phải sửa gì vào class Computer cả.

Giờ thì các bạn đã hiểu DI là gì chưa nào? Cơ bản chỉ là thay đổi cách code để giảm sự phụ thuộc của 2 class thôi.

Các loại dependency injection:

3 kieu dependency injection

Về cơ bản thì có 3 loại DI phổ biến:

1. Constructor injection: Là cách chúng ta đã làm bên trên, các dependence được truyền vào trong class bằng constructor (hàm khởi tạo của class).

2. Interface injection: Thằng cha này tương đối mơ hồ, nó gần giống như Setter injection chỉ là ta khai báo thêm 1 method bắt buộc ở interface và bắt các class implement nó phải viết lại thôi. Người ta thường sử dụng cách inject này khi không biết các class implement nó cần inject cái gì.

Interface IInputImplement{
    public void setInput(IInput $input);
}
Class Computer implement IInputImplement {
    protected $keyboard;
    public void function setInput(IInput $keyboard){
        $this->keyboard = $keyboard;
    }
}

3. Setter injection: Các dependence được truyền vào trong class bằng hàm setter.

Class Computer {
    protected $monitor;
    protected $keyboard;
    public void function setKeyboard(IInput $keyboard){
        $this->keyboard = $keyboard;
    }
    public void function setMonitor(IDisplay $monitor){
        $this->monitor = $monitor;
    }
}
// bên ngoài truyền vào thông qua hàm setter này thôi
$monitor = new Monitor();
$keyboard = new Keyboard();
$computer = new Computer();
$computer->setKeyboard($keyboard);
$computer->setMonitor($monitor);

Tới đây chắc hẳn sẽ có bạn than rằng ôi thế giả dụ như thằng Monitor có vài thằng phụ thuộc nữa như Color, Size… xong thằng Keyboard cũng vậy thì trong service khởi tạo new Color, new Size sml ư =)). Cũng đúng nhưng các mấy thánh sáng tạo ra DI này quả là thiên tài, các ông thần ấy đã nghĩ trước cho chúng ta rồi. Họ sẽ tạo ra 1 nơi để quản lý những thằng phụ thuộc này, inject hộ mình luôn, bạn cần cái gì thì lấy cái đó ra dùng thôi, không cần khởi tạo loằng ngoằng ở service nữa. Các bạn có thể xem thêm về Service Container tại đây.

Tổng kết

Trong phạm vi bài này mình chỉ giới thiệu sơ qua để các bạn hiểu bản chất của thằng DI. Đây là 1 khái niệm cực kì quan trọng mà các bạn bắt buộc phải nắm vững. Dựa trên khái niệm, này Laravel cũng cung cấp cho chúng ta 1 công cụ mạnh kinh khủng khiếp có tên là Service container. Đó là nơi quản lý các class Dependency và hỗ trợ Inject mà hầu như chúng ta không phải làm cái bất gì cả. Service container được sử dụng ở hầu hết ngõ ngách của laravel, nó chính là vũ khí bí mật giúp laravel đẻ sau nhưng nhanh chóng vượt lên dẫn top về sự phổ biến trong các php framework.

Bài sau mình sẽ viết về 1 design pattern rất phổ biến trong các laravel project đó là Repository Pattern. 1 pattern giúp tổ chức code 1 cách khoa học hơn, dễ maintain và nhìn chuyên nghiệp hơn :v


Bài viết liên quan

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *