Mỗi lần viết new Something() rải rác khắp codebase, bạn đang đưa ra quyết định về việc tạo object — dù có nhận ra hay không. Creational pattern giúp biến những quyết định đó thành rõ ràng và có chủ đích. Đây là 8 pattern bao quát cách các object được sinh ra.
1. Singleton
Tạo ra duy nhất 1 instance cho toàn hệ thống. Không ai được phép tạo cái thứ hai. Mình hay dùng pattern này khi cần chạy 1 chương trình con độc lập để kiểm tra và xử lý xuyên suốt hệ thống.
public class Singleton {
private constructor() {} // Private constructor để các class khác không thể tạo instance mới
// Cách này là cách đơn giản nhất nhưng nhược điểm là instance tạo ra nhưng có thể không được sử dụng
// private static instance: Singleton = new Singleton();
// public static getInstance() : Singleton {
// return Singleton.instance;
// }
// Cách viết này chỉ khởi tạo instance khi có class gọi đến
private static instance: Singleton;
public static getInstance() : Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
// Method
public helloWorld() {
console.log("Hello World");
}
}
Singleton.getInstance().helloWorld(); // Kết quả: "Hello World"
Lưu ý
JavaScript chỉ có đơn luồng, nên bạn không cần lo chuyện 2 class gọi instance cùng lúc rồi tạo ra 2 bản. Bớt được một mối lo so với Java hay C#.
2. Factory
Quản lý và trả về object theo yêu cầu, giúp việc khởi tạo linh hoạt hơn. Mình thường dùng khi có nhiều class tương tự nhau (thường implement cùng 1 interface) và muốn 1 đầu vào gọn gàng để lấy đúng cái cần.
2.1. Chức năng
- Tạo ra 1 cách mới để khởi tạo Object
- Giấu logic khởi tạo (rất tiện khi viết thư viện cho người khác dùng)
- Giảm sự phụ thuộc: class nào extend CarFactory thì khi thêm xe mới chỉ sửa từ CarFactory trở lên
Hình dung thế này: xưởng xe muốn cho khách xem mẫu. Nếu không dùng Factory, xưởng phải kéo hết mỗi hãng 1 chiếc ra, nhưng khách chỉ cần xem 1 loại. Tốn tài nguyên vô ích. Dùng Factory thì chỉ lấy đúng cái khách hỏi.
- Tạo ra các mẫu xe:
interface Car {
viewCar(): void;
}
class Honda implements Car {
viewCar() {
console.log("Honda-chan");
}
}
class Nexus implements Car {
viewCar() {
console.log("Nexus-chan");
}
}
class Toyota implements Car {
viewCar() {
console.log("Toyota-chan");
}
}
- Cách bình thường: Chuẩn bị toàn bộ xe mỗi loại 1 chiếc
class CarFactory {
public honda : Honda = new Honda();
public nexus : Nexus = new Nexus();
public toyota : Toyota = new Toyota();
}
var viewCar = new CarFactory();
car.honda.viewCar(); // Kết quả "Honda-chan"
- Factory pattern: Chỉ chuẩn bị loại xe mà khách yêu cầu
class CarFactory {
public car!: Car;
constructor(carType : string) {
switch(carType) {
case "HONDA": {
this.car = new Honda();
break;
}
case "NEXUS": {
this.car = new Nexus();
break;
}
case "TOYOTA": {
this.car = new Toyota();
}
}
}
}
var car = new CarFactory("TOYOTA");
car.car.viewCar();
3. Factory Method
Factory Method giải quyết bài toán: tạo object mà không để code gọi biết class cụ thể nào được dùng. Mình thấy pattern này cực kỳ hữu ích khi xây hệ thống cần mở rộng.
- Mục đích chính là giấu đi class nào đang được khởi tạo.
- Thay vì gọi
newtrực tiếp, bạn định nghĩa 1 method chịu trách nhiệm tạo object. Các lớp con override method đó để đổi kiểu object. Gọn và linh hoạt.
3.1. Cấu trúc
Interface Product được trỏ đến nhiều nhất trên hình, nên mình bắt đầu từ đây. Mình hay chia hình ra 2 phần: phía trên là “hợp đồng” giữa 2 sếp, phía dưới là nhân viên 2 bên giao tiếp qua các điều khoản trong hợp đồng đó.
- Định nghĩa Product
interface Product {
doStuff() : void;
}
- Tạo ra mẫu sản phẩm từ định nghĩa Product
class ConcreteProduct1 implements Product {
doStuff() {
return "Result of the ConcreteProduct1";
}
}
class ConcreteProduct2 implements Product {
doStuff() {
return "Result of the ConcreteProduct2";
}
}
- Định nghĩa Creator
abstract class Creator {
public abstract createProduct() : Product;
someOperation() {
// Call the factory method to create a Product object.
let product : Product = this.createProduct();
// Now, use the product.
return `Creator: The same creator's code has just worked with ${product.doStuff()}`;
}
}
- Tạo ra instance của Creator
class ConcreteCreator1 extends Creator {
public createProduct() {
return new ConcreteProduct1();
}
}
class ConcreteCreator2 extends Creator {
public createProduct() {
return new ConcreteProduct2();
}
}
- Client sử dụng
function clientCode(creator: Creator) {
console.log(creator.someOperation());
}
console.log("App: Launched with the ConcreteCreator2\n");
clientCode(new ConcreteCreator2());
- Output
App: Launched with the ConcreteCreator2
The same creator's code has just worked with Result of the ConcreteProduct2
4. Abstract Factory
Abstract Factory gom một đống class phức tạp có liên quan lại, xử lý bên trong và chỉ trả ra cái client cần. Mình hay gọi đây là pattern “gia đình sản phẩm” — khi bạn có nhiều sản phẩm thuộc cùng một nhóm và muốn tạo chúng qua 1 factory thống nhất.
Mức độ sử dụng: Khá thường xuyên
4.1. Cấu trúc
- Trước tiên, định nghĩa sản phẩm muốn tạo
interface AbstractProductA {
functionA(): string;
}
interface AbstractProductB {
functionB(): string;
}
- Tạo định nghĩa nhà máy sản xuất A và B
interface AbstractFactory {
createProductA(): AbstractProductA;
createProductB(): AbstractProductB;
}
- Mình muốn 2 nhà máy sản xuất 2 loại A và B nhưng khác phiên bản. Nhà máy 1 ra version 1, nhà máy 2 ra version 2. Nhưng trước khi xây nhà máy, phải có mẫu sản phẩm đã — nhà máy cần biết nó sẽ sản xuất cái gì.
class ConcreteProductA1 implements AbstractProductA {
functionA(): string {
return "The result of the product A1.";
}
}
class ConcreteProductA2 implements AbstractProductA {
functionA(): string {
return "The result of the product A2.";
}
}
class ConcreteProductB1 implements AbstractProductB {
functionB(): string {
return "The result of the product B1.";
}
}
class ConcreteProductB2 implements AbstractProductB {
functionB(): string {
return "The result of the product B2.";
}
}
- Giờ xây nhà máy
class ConcreteFactory1 implements AbstractFactory {
createProductA(): AbstractProductA {
return new ConcreteProductA1();
}
createProductB(): AbstractProductB {
return new ConcreteProductB1();
}
}
class ConcreteFactory2 implements AbstractFactory {
createProductA(): AbstractProductA {
return new ConcreteProductA2();
}
createProductB(): AbstractProductB {
return new ConcreteProductB2();
}
}
- Client sử dụng
function clientCode(factory: AbstractFactory) {
let product_a = factory.createProductA();
let product_b = factory.createProductB();
console.log(product_b.functionB());
}
console.log("Client: Testing client code with the first factory type");
clientCode(new ConcreteFactory1());
console.log("Client: Testing the same client code with the second factory type");
clientCode(new ConcreteFactory2());
- Output
Client: Testing client code with the first factory type
The result of the product B1.
Client: Testing the same client code with the second factory type
The result of the product B2.
Rút ra:
- Muốn tạo sản phẩm gì thì phải định nghĩa nó trước.
- Muốn xây nhà máy thì phải có mẫu sản phẩm trước, rồi mới đến nhà máy.
- Bản chất là tạo nhiều lớp, kết hợp chúng để giảm phụ thuộc và dễ bảo trì. Thêm sản phẩm mới = thêm code, không phải sửa code cũ. Đó là cốt lõi.
4.2. Phân biệt Abstract Factory và Factory Method
4.2.1. Giống nhau
- Đều là Factory Pattern
- Đều giảm sự phụ thuộc giữa chương trình và cài đặt cụ thể
- Đều đóng gói quá trình tạo object để code độc lập với kiểu cụ thể
4.2.2. Khác nhau
Factory Method
- Dùng class để tạo product.
- Dựa vào kế thừa. Bạn extend 1 class, override factory method, method đó tạo object.
- Ý tưởng: dùng lớp con để sinh ra object mong muốn. Người dùng chỉ biết lớp trừu tượng (gia cầm), còn lớp con lo cụ thể (gà, vịt, ngan). Chương trình độc lập với kiểu cụ thể.
- Thêm product mới? Chỉ cần 1 method.
- Người dùng không biết cái gì được tạo ra — gọi hàm là xong.
Abstract Factory
- Dùng object (không phải class) để tạo product.
- Dựa vào composition thay vì kế thừa.
- Tạo 1 kiểu abstract dùng để sinh ra cả nhóm product. Lớp con của abstract đó quyết định cách tạo từng product. Bạn tạo instance của 1 lớp con (instance đó là factory) rồi inject vào chỗ cần. Giống Factory Method, code dùng factory hoàn toàn độc lập với product cụ thể. Bonus: các product liên quan đã được nhóm sẵn. Nhược điểm? Thêm product mới vào nhóm = phải sửa tất cả lớp con (factory). Ai cũng ghét Abstract Factory ở điểm này.
- Tạo được nhiều kiểu product khác nhau.
- Abstract Factory thường dùng nhiều Factory Method bên trong. Các lớp factory con dùng Factory Method để tạo product tương ứng.
4.3. Vậy khi nào nên dùng Abstract Factory, khi nào nên dùng Factory Method?
-
Abstract Factory: Khi cần tạo nhiều loại product cùng lúc và muốn client không cần biết class cụ thể.
-
Factory Method: Khi chỉ cần tạo 1 kiểu product, hoặc muốn code độc lập với class cụ thể, hoặc chưa biết sau này cần lớp con nào. Tạo 1 subclass (factory implement abstract type) và implement Factory Method.
Nguồn: quyển sách Head First — Design Pattern
5. Builder
Builder giúp xây dựng object phức tạp theo từng bước, dùng các building block đơn giản. Mỗi bước độc lập với nhau. Mình hay dùng pattern này khi thấy constructor có quá nhiều tham số — cái kiểu “telescoping constructor” mà tham số cứ dài ra mãi.
- Cho phép tạo object phức tạp qua các câu lệnh đơn giản, set từng thuộc tính một.
- Xóa sổ anti-pattern telescoping constructor. Bạn biết cái kiểu class có cả đống thuộc tính, constructor cứ phình ra mãi.
- Phù hợp khi thuật toán set từng thuộc tính không phụ thuộc vào thuộc tính khác.
5.1. Ưu điểm
- Thêm 1 cách khởi tạo object gọn gàng
- Không cần viết cả tá constructor nữa
5.2. Hạn chế
- Phải tạo builder riêng cho từng class. Thêm code là thêm.
Mức độ sử dụng: Thường xuyên
5.3. Thành phần chính
- Product: Object phức tạp cần xây dựng, nhiều thuộc tính
- Builder: Abstract class hoặc interface khai báo method tạo object
- ConcreteBuilder: Implement Builder, chứa chi tiết cách xây dựng. Giữ các instance đã tạo và có method trả về chúng
- Director: Nơi gọi Builder để dựng object
5.4. Cấu trúc
interface Builder được trỏ đến nhiều nhất, nên mình bắt đầu từ đây. Nhưng trước đó phải khai báo Product — muốn xây builder thì phải biết sản phẩm là gì đã.
- Định nghĩa sản phẩm
class Product {
constructor(
private partA: string,
private partB: string,
private partC: string
) { }
show() : string {
return `This product has 3 parts: ${this.partA}, ${this.partB} and ${this.partC}`;
}
}
- Định nghĩa Builder
abstract class Builder {
abstract BuildPartA(content: string): Builder;
abstract BuildPartB(content: string): Builder;
abstract BuildPartC(content: string): Builder;
abstract GetResult(): Product;
}
- Tạo concrete builder để sản xuất sản phẩm
class ConcreteBuilder1 extends Builder {
private partA! : string;
private partB! : string;
private partC! : string;
BuildPartA(content: string) : Builder {
this.partA = content;
return this; // Sử dụng để khai báo ngắn khi client sử dụng
}
BuildPartB(content: string) : Builder {
this.partB = content;
return this;
}
BuildPartC(content: string) : Builder {
this.partC = content;
return this;
}
GetResult() : Product {
return new Product(this.partA, this.partB, this.partC);
}
}
- Tạo Director sử dụng Builder
class Director {
private product! : Product;
constructor(private builder : Builder) {
this.product = this.builder.GetResult();
}
showProduct() {
return this.product.show();
}
}
- Client sử dụng
let b1 : Builder = new ConcreteBuilder1();
let director : Director = new Director(
b1.BuildPartA('Ngô')
.BuildPartB('Quang')
.BuildPartC('Sang') // Khai báo ngắn
);
console.log(director.showProduct());
- Output
This product has 3 parts: Ngô, Quang and Sang
6. Object Pool
Object Pool quản lý một bộ nhớ đệm chứa sẵn các object. Client thay vì tạo mới thì cứ lấy cái có sẵn trong pool ra dùng. Pool tự tạo object mới nếu chưa có, hoặc giới hạn số lượng tùy bạn cấu hình. Mình gặp pattern này nhiều nhất ở connection pooling cho database.
6.1. Ví dụ
Hình dung như kho văn phòng. Nhân viên mới vào, quản lý ra kho kiếm thiết bị. Có sẵn? Lấy luôn. Không có? Đặt mua mới. Nhân viên nghỉ việc? Thiết bị quay về kho cho người sau dùng.
Nhưng ví dụ này cũng cho thấy vấn đề. Thứ nhất, pool tốn tài nguyên riêng — muốn có kho thì tốn diện tích, tiền, và người quản lý. Thứ hai, đồ trong kho cũ quá thì hết xài được. Cần iPhone cho nhân viên mới mà kho chỉ có Nokia 1080… RIP. Tốn tài nguyên mà không dùng được.
Ý tưởng cốt lõi: nếu instance dùng lại được thì đừng tạo mới, hãy tái sử dụng.
6.2. Ưu điểm
- Tăng hiệu suất ứng dụng
- Hiệu quả khi việc khởi tạo object tốn thời gian
- Quản lý connection với khả năng tái sử dụng và chia sẻ
- Giới hạn được số lượng object tối đa
6.3. Nhược điểm
- Có thể tạo ra rác. Cần dọn dẹp định kỳ.
- Phải cẩn thận với trạng thái object khi nó quay về pool. Nếu object trả về ở trạng thái “bẩn”, client sau sẽ nhận phải thứ không mong đợi — dẫn đến bug, không nhất quán, hoặc rò rỉ thông tin.
6.4. Sử dụng Object Pool khi
- Tạo object tốn kém (query database, kết nối mạng)
- Cần tạo nhiều object trong thời gian ngắn (rủi ro phân mảnh bộ nhớ)
- Liên tục tạo rồi hủy object
- Muốn tái sử dụng có kiểm soát thay vì
newtràn lan - Nhiều client cần cùng tài nguyên ở các thời điểm khác nhau
6.5. Cấu trúc
- Reusable: Các object được tái sử dụng
- Client: Các class dùng object tái sử dụng
- ReusablePool: Bộ quản lý, giữ object và phát cho client khi cần
6.6. Ví dụ Object Pool thông qua ứng dụng Taxi
Hình dung hãng taxi chỉ có N chiếc. Hãng quản lý trạng thái (rảnh hay đang chở), điều xe rảnh đi đón khách, cho khách chờ nếu hết xe, hủy nếu chờ quá lâu.
Mình mô hình hóa thế này:
- Taxi: Đại diện 1 chiếc taxi với thuộc tính và method của nó.
- TaxiPool: Đại diện công ty taxi.
- getTaxi(): Lấy taxi rảnh. Throw exception nếu chờ quá lâu.
- release(): Trả taxi về pool sau khi chở xong.
- available: Danh sách taxi rảnh.
- inUse: Danh sách taxi đang chạy.
- ClientThread: Đại diện khách hàng — gọi xe, đi, trả khách.
Trong code dưới, mình mô phỏng pool 4 taxi với 8 cuộc gọi cùng lúc. Mỗi taxi mất 200ms để đến điểm đón, chở khách 1000-1500ms (ngẫu nhiên), khách chờ tối đa 1200ms trước khi bỏ cuộc.
- Khai báo class Taxi
class Taxi {
constructor(private name: string) {}
getName() : String {
return this.name;
}
setName(name: string) {
this.name = name;
}
toString() : String {
return `Taxi [name = ${this.name}]`
}
}
- Khai báo TaxiPool đại diện object pool
class TaxiPool {
private readonly EXPIRED_TIME_IN_MILISECOND: number = 1200;
private readonly NUMBER_OF_TAXI: number = 4;
private available: Array<Taxi> = new Array<Taxi>();
private inUse: Array<Taxi> = new Array<Taxi>();
private count: number = 0;
private waiting: boolean = false;
async getTaxi(): Promise<Taxi> {
if (this.available.length > 0) {
let taxi: Taxi = this.available.shift() || new Taxi('');
this.inUse.push(taxi);
return Promise.resolve(taxi);
}
if (this.count == this.NUMBER_OF_TAXI) {
this.waitingUntilTaxiAvailable();
return this.getTaxi();
}
return await this.createTaxi();
}
release(taxi: Taxi) {
this.inUse.splice(this.inUse.indexOf(taxi), 1);
this.available.push(taxi);
console.log(taxi.getName() + " is free");
}
async createTaxi(): Promise<Taxi> {
await this.sleep(200);
this.count++;
let taxi: Taxi = new Taxi("Taxi " + this.count);
console.log(taxi.getName() + " is created");
return taxi;
}
async waitingUntilTaxiAvailable(): Promise<void> {
if (this.waiting) {
this.waiting = false;
throw new Error("No taxi available");
}
this.waiting = true;
await this.waitingF(this.EXPIRED_TIME_IN_MILISECOND);
}
async waitingF(numberOfSecond: number) {
try {
await this.sleep(numberOfSecond);
} catch (e) {
throw new Error(e);
}
}
sleep(ms: any): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
- Tạo ClientThread để gọi xe
class ClientThread {
private clientNumber: number = Math.floor(Math.random() * 501) + 1000;
constructor(private taxiPool: TaxiPool) { }
run(): void {
this.takeATaxi();
}
private async takeATaxi(): Promise<void> {
try {
console.log("New client: " + this.clientNumber);
let taxi: Taxi;
taxi = await this.taxiPool.getTaxi().then(res => res);
await this.sleep(Math.floor(Math.random() * 501) + 1000);
this.taxiPool.release(taxi);
console.log("Served the client: " + this.clientNumber);
} catch (e) {
console.log(">>>Rejected the client: " + this.clientNumber);
}
}
sleep(ms: any): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
- Chạy chương trình
const NUM_OF_CLIENT: number = 8;
let taxiPool: TaxiPool = new TaxiPool();
for (let i = 0; i < NUM_OF_CLIENT; i++) {
let client: ClientThread = new ClientThread(taxiPool);
client.run();
}
- Kết quả
New client: 1196
New client: 1346
New client: 1398
New client: 1052
New client: 1132
New client: 1280
New client: 1174
New client: 1247
Taxi 1 is created
Taxi 2 is created
Taxi 3 is created
Taxi 4 is created
Taxi 5 is created
Taxi 6 is created
Taxi 7 is created
Taxi 8 is created
Taxi 8 is free
Served the client: 1247
Taxi 5 is free
Served the client: 1132
Taxi 6 is free
Served the client: 1280
Taxi 3 is free
Served the client: 1398
Taxi 4 is free
Served the client: 1052
Taxi 2 is free
Served the client: 1346
Taxi 7 is free
Served the client: 1174
Taxi 1 is free
Served the client: 1196
Nhận xét
- Cái hay của pooling là tận dụng tài nguyên đã cấp phát. Với 4 taxi, nhiều khi vẫn phục vụ được hơn 4 request cùng lúc vì taxi được tái sử dụng. Không tốn thời gian khởi tạo, không tốn bộ nhớ cho việc tạo rồi hủy liên tục.
- Có thể linh hoạt hơn bằng cách đặt N (số instance tối thiểu lúc rảnh) và M (tối đa khi cần huy động hết). Qua đỉnh rồi thì pool tự thu gọn lại.
Tham khảo: https://gpcoder.com/4456-huong-dan-java-design-pattern-object-pool/
7. Prototype
Prototype tạo object mới bằng cách copy từ object có sẵn. Thay vì xây từ đầu, bạn clone một bản mẫu. Mình dùng pattern này nhiều hơn mình tưởng.
Tần suất sử dụng: Cao
7.1. Cấu trúc
7.2. Các thành phần tham gia
- Prototype - CustomerPrototype: Cung cấp interface để clone chính nó
- Clones - Customer: Các object được tạo ra bằng clone
class CustomerPrototype {
constructor(public proto: any) { }
clone() {
var customer: Customer = new Customer(
this.proto.first,
this.proto.last,
this.proto.status
);
return customer;
};
}
class Customer {
constructor(
public first: string,
public last: string,
public status: string
) { }
say() {
console.log(
`name: ${this.first} ${this.last}, status: ${this.status}`
);
}
}
- Chạy chương trình
let proto = new Customer("n/a", "n/a", "pending");
let prototype = new CustomerPrototype(proto);
let customer = prototype.clone();
customer.say();
- Kết quả
name: n/a n/a, status: pending
- Một cách clone nhanh trong ES6
let prototye : {
name: "John"
}
let custom = {...prototype};
Có một chuyện mình hay quên hồi mới học. Mỗi instance kế thừa thuộc tính và method từ class, nên mỗi instance tốn bộ nhớ riêng cho phần đó. Prototype Pattern có biến thể giải quyết vấn đề này bằng cách cho các instance dùng chung thuộc tính từ class.
- Giả sử viết game, mình muốn class Warrior như sau:
function Warrior(name) {
this.name = name;
this.hp = 100;
this.bash = function (target) {
target.hp -= 10;
};
this.slash = function (target) {
target.hp /= 2;
};
}
let harryPotter = new Warrior("Harry Potter");
let ngan = new Warrior("Ngan");
let snake = new Warrior("Snake");
harryPotter.bash(ngan);
console.log(ngan.hp); // 90
ngan.slash(snake);
console.log(snake.hp); // 50
- 3 chiến binh, cùng chung constructor:
console.log(
harryPotter.constructor === ngan.constructor,
snake.constructor === ngan.constructor
); // Kết quả: true true
- Các chiến binh đánh nhau thoải mái. Nhưng vấn đề là mỗi chiến binh giữ bản copy riêng của các skill function, dù function giống hệt nhau. Lãng phí:
console.log(harryPotter.bash === ngan.bash, snake.bash === ngan.bash); // Kết quả: false false
- Đây là lý do
prototypetồn tại. Chuyển method dùng chung sang đó:
function Warrior(name) {
this.name = name;
this.hp = 100;
}
Warrior.prototype.bash = function (target) {
target.hp -= 10;
};
Warrior.prototype.slash = function (target) {
target.hp /= 2;
};
let harryPotter = new Warrior("Harry Potter");
let ngan = new Warrior("Ngan");
let snake = new Warrior("Snake");
console.log(harryPotter.bash === ngan.bash, snake.bash === ngan.bash); // Kết quả: true, true
- Viết kiểu khác cũng tương tự:
Warrior.prototype = {
bash: function (target) {
target.hp -= 10;
},
slash: function (target) {
target.hp /= 2;
},
};
Nhưng cẩn thận — cách này thay thế toàn bộ prototype object, nên mất luôn reference đến constructor Warrior. Dù sao nó cũng cho thấy rõ cách prototype thực hiện kế thừa và chia sẻ trong JavaScript.
8. Dependency Injection
Thay vì để class tự tạo dependency bên trong, bạn khai báo dependency là interface rồi truyền instance cụ thể từ bên ngoài vào. Mọi thứ linh hoạt hơn, test cũng dễ hơn.
8.1. Ưu điểm
- Viết unit test dễ
- Thay đổi implementation dễ
- Scale tốt khi dự án lớn lên
8.2. Đặt vấn đề
Giả sử bạn có xe máy. Xe có linh kiện, ví dụ lốp. Lốp loại A giá $1000, loại B giá $500. Bạn chọn loại B để tiết kiệm.
class LopA {
public price : number = 1000;
}
class LopB {
public price : number = 500;
}
class Xe {
public lop : LopA = new LopA();
}
Khi tạo object xe, class tự tạo instance lốp bên trong. Phụ thuộc cứng, gây ra 2 vấn đề:
- Không đổi được linh kiện. Có tiền muốn thay lốp A? Không được, đã gắn cứng rồi.
- Muốn thay đổi gì phải sửa class. Vi phạm nguyên tắc Open-Closed trong SOLID.
8.3. Giải quyết
DI giải quyết gọn gàng. Cốt lõi: khai báo dependency là interface, không gắn cứng vào class, không tạo instance bên trong. Tạo instance bên ngoài rồi truyền vào. Phụ thuộc cứng biến mất.
Quay lại ví dụ — thay vì tạo sẵn lốp trong class Xe, chỉ khai báo interface rồi inject từ ngoài:
interface Lop {
price: number;
}
class LopA implements Lop {
public price : number = 1000;
}
class LopB implements Lop {
public price : number = 500;
}
class Xe {
constructor(public lop: Lop) {}
}
// run
let xe : Xe = new Xe(new LopB());
console.log(xe.lop.price); // Kết quả: 500
Muốn đổi lốp B sang A? Đơn giản:
xe.lop = new LopA();
console.log(xe.lop.price); // Kết quả: 1000
Đổi lốp mà không sửa gì bên trong class Xe. Đúng SOLID.
8.4. Các dạng DI
DI có 2 dạng:
- Constructor Injection (ví dụ trên)
- Setter Injection
class Xe {
public _lop!: Lop;
set lop(lop: Lop) {
this._lop = lop;
}
}
let xe : Xe = new Xe();
xe.lop = new LopB();