Giới thiệu Dependency Injection trong Angular
- 25-12-2023
- Toanngo92
- 0 Comments
Mục lục
Dependency Injection là gì?
Dependency Injection là một cách để tổ chức và quản lý các phần của một chương trình. Nó giúp chương trình linh hoạt hơn bằng cách cho phép chèn các thành phần cần thiết vào trong chương trình một cách dễ dàng. Bạn có thể nghĩ về nó như cách bạn sắp xếp và sử dụng các khối LEGO. Khi bạn cần một khối cụ thể, bạn lấy nó từ hộp và gắn vào nơi bạn cần trong cấu trúc của mô hình LEGO.
Trên Frontend, việc này cũng quan trọng và có ích. Ví dụ, Angular, một trong những Framework Frontend, sử dụng Dependency Injection rất nhiều. Nó có một cách riêng để chèn các phần tử vào chương trình, giúp tổ chức và quản lý chúng một cách dễ dàng hơn. Điều này giúp cho việc xây dựng ứng dụng một cách có tổ chức và linh hoạt hơn.
DI là gì?
Dependency Injection (DI) là cách để giúp các phần của chương trình “kết nối” với nhau một cách thông minh. Đây giống như việc khi bạn cần một công cụ nào đó, bạn không cần tự làm mà có thể nhờ người khác cung cấp cho bạn.
Ví dụ, hãy tưởng tượng bạn đang xây dựng một ứng dụng mua sắm trực tuyến. Bạn có các bộ phận khác nhau: một bộ phận quản lý sản phẩm, một bộ phận giỏ hàng và một bộ phận hiển thị sản phẩm. Thay vì mỗi bộ phận phải tự lo mọi việc, DI cho phép chúng “hỏi” và nhận các dịch vụ cần thiết từ nhau.
Ví dụ như khi phần hiển thị sản phẩm cần tính tổng giá trị giỏ hàng, nó không cần phải tự mình tính, mà có thể hỏi bộ phận giỏ hàng để lấy thông tin. Dependency Injection giúp cho các bộ phận này không phụ thuộc trực tiếp vào nhau mà có thể tương tác thông qua việc cung cấp cho nhau các dịch vụ cần thiết.
Hãy xem đoạn mã bên dưới
class ProductModel {
sku: string;
name: string;
price: number;
}
interface CartItem {
product: ProductModel;
quantity: number;
}
class CartService {
selectedProducts: CartItem[] = [];
calculateTotal(): number {
return this.selectedProducts.reduce(
(total, item) => item.product.price * item.quantity + total,
0
);
}
addToCart(): void {
// logic here
}
}
class ProductComponent {
cartService: CartService;
}
Khởi tạo bên trong ProductComponent
Trong một số trường hợp, để tạo một phiên bản của một công cụ trong một phần của chương trình từ phần khác, chúng ta có thể dùng lệnh “new”. Điều này giống như việc bạn tự làm một công cụ mới khi cần.
Ví dụ, trong một phần của chương trình, để có một dịch vụ giỏ hàng, chúng ta có thể viết mã để tạo một phiên bản mới của dịch vụ đó, giống như khi bạn tạo một chiếc đồ mới từ đầu. Điều này có thể như thế này:
class ProductComponent {
cartService: CartService;
constructor() {
this.cartService = new CartService(); // Tạo một phiên bản mới của CartService
}
}
Tuy nhiên, điều này có thể khiến hai phần của chương trình trở nên quá liên kết chặt chẽ với nhau (tight coupling). Khi bạn muốn thay đổi hoặc cải tiến dịch vụ giỏ hàng, bạn sẽ phải sửa code trong cả hai phần, và đôi khi còn phải kiểm tra lại cả hai để đảm bảo chúng vẫn hoạt động đúng.
Injection (Request để lấy về instance)
Injection, trong trường hợp này, giống như việc chúng ta đặt một yêu cầu đặc biệt để lấy một phiên bản cụ thể từ một nguồn nào đó.
Ví dụ, khi chúng ta cần một dịch vụ giỏ hàng trong một phần của chương trình, chúng ta không tạo một phiên bản mới mà chúng ta “yêu cầu” một phiên bản từ nguồn cung cấp (container) nào đó. Điều này có thể như sau:
class ProductComponent {
cartService: CartService;
constructor(cartService: CartService) {
this.cartService = cartService; // Nhận dịch vụ giỏ hàng từ nguồn cung cấp
}
}
Điều này có thể giúp chúng ta tạo ra một nguồn cung cấp tự động (IoC container) có khả năng cung cấp các phiên bản cần thiết cho các phần khác nhau của chương trình.
Thông qua cú pháp này, class ProductComponent không cần biết cách tạo dịch vụ giỏ hàng mà nó chỉ cần “yêu cầu” từ nguồn cung cấp. Khi muốn thay đổi hoặc cải tiến dịch vụ giỏ hàng, chúng ta chỉ cần thay đổi trong nguồn cung cấp mà không cần sửa code trong class ProductComponent. Điều này giúp chúng ta duy trì tính linh hoạt và dễ dàng thay đổi.
Mẫu thiết kế này gọi là Dependency Injection, cụ thể là constructor injection, giúp chúng ta quản lý và cung cấp các phiên bản cần thiết một cách thông minh.
DI trong Angular
Trong Angular, Dependency Injection (DI) bao gồm ba phần chính:
- Injector: Đây là một cái hộp chứa các công cụ để chúng ta lấy hoặc tạo các bản thể của các cái chúng ta cần.
- Provider: Như một bản vẽ chỉ dẫn cho Injector về cách tạo một bản thể của một cái gì đó.
- Dependency: Đại diện cho cái chúng ta cần tạo, có thể là một hàm, một giá trị cụ thể hoặc một object.
Chúng ta có thể cung cấp injectors với providers ở nhiều cấp độ khác nhau trong ứng dụng Angular:
- Trong
@Injectable()
decorator cho một service. - Trong
@NgModule()
decorator (trong mảngproviders
) cho một NgModule. - Trong
@Component()
decorator (trong mảngproviders
) cho một component hoặc directive.
Ví dụ, để cung cấp CartService
:
@Injectable({
providedIn: "root",
})
export class CartService {
// properties and methods
}
@Component({
selector: "app-product",
templateUrl: "./product.component.html",
styleUrls: ["./product.component.css"],
})
export class ProductComponent implements OnInit {
constructor(private cartService: CartService) {}
ngOnInit() {
console.log(this.cartService.calculateTotal());
}
}
Khi chúng ta sử dụng @Injectable
decorator, chúng ta đang cung cấp metadata cho Angular, cho biết cách để tạo một phiên bản của CartService khi được yêu cầu, như trong trường hợp của ProductComponent. Thông tin providedIn: 'root'
chỉ ra rằng chúng ta muốn service này sẽ tồn tại như một bản thể duy nhất (singleton) trong toàn bộ ứng dụng.
Override Provider trong Angular
Bây giờ, thay vì lưu trữ thông tin giỏ hàng ở phía client như trước, hệ thống yêu cầu chúng ta sử dụng một nguồn thông tin bên ngoài như một API để thêm vào giỏ hàng và tính tổng tiền. Điều này yêu cầu chúng ta thay đổi cách thức tính toán trong class CartService
, không thay đổi giao diện công khai của nó, chỉ là cách triển khai như sau:
@Injectable()
export class CartExtService {
calculateTotal(): number {
// Gọi đến nguồn dữ liệu bên ngoài
// Trả về dữ liệu từ nguồn bên ngoài
return Math.random() * 100;
}
addToCart(): void {
// logic ở đây
}
}
Điều tuyệt vời là chúng ta có thể thay đổi triển khai của CartService
mà không cần phải sửa đổi code của ProductComponent
. Có hai cách để làm điều này:
- Override thông qua
@NgModule
:
@NgModule({
// các thông tin metadata khác
providers: [
{
provide: CartService,
useClass: CartExtService,
},
],
})
export class AppModule {}
2. Hoặc Override trực tiếp trong @Injectable
của service:
@Injectable({
providedIn: "root",
useClass: CartExtService,
})
export class CartService {
// logic ở đây
}
Như vậy, thông qua cách làm này, chúng ta có thể thay đổi triển khai của CartService
để sử dụng dịch vụ mới mà không làm ảnh hưởng đến code của ProductComponent
, giúp chúng ta linh hoạt và dễ dàng thay đổi triển khai khi cần thiết.