Đến nội dung chính
sang.id.vn
Quay lại

Creational Design Pattern — Giải thích dễ hiểu với TypeScript

English

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

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.

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");
    }
}
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"
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.

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 đó.

interface Product {
    doStuff() : void;
}
class ConcreteProduct1 implements Product {
    doStuff() {
        return "Result of the ConcreteProduct1";
    }
}
class ConcreteProduct2 implements Product {
    doStuff() {
        return "Result of the ConcreteProduct2";
    }
}
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()}`;
    }
}
class ConcreteCreator1 extends Creator {
    public createProduct() {
        return new ConcreteProduct1();
    }
}
class ConcreteCreator2 extends Creator {
    public createProduct() {
        return new ConcreteProduct2();
    }
}
function clientCode(creator: Creator) {
    console.log(creator.someOperation());
}
console.log("App: Launched with the ConcreteCreator2\n");
clientCode(new ConcreteCreator2());
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

interface AbstractProductA {
    functionA(): string;
}
interface AbstractProductB {
    functionB(): string;
}
interface AbstractFactory {
    createProductA(): AbstractProductA;
    createProductB(): AbstractProductB;
}
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.";
    }
}
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();
    }
}
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());
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:

4.2. Phân biệt Abstract Factory và Factory Method

4.2.1. Giống nhau

4.2.2. Khác nhau

Factory Method
Abstract Factory

4.3. Vậy khi nào nên dùng Abstract Factory, khi nào nên dùng 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.

5.1. Ưu điểm

5.2. Hạn chế

Mức độ sử dụng: Thường xuyên

5.3. Thành phần chính

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ì đã.

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}`;
    }
}
abstract class Builder {
    abstract BuildPartA(content: string): Builder;
    abstract BuildPartB(content: string): Builder;
    abstract BuildPartC(content: string): Builder;
    abstract GetResult(): Product;
}
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);
    }
}
class Director {
    private product! : Product;
    constructor(private builder : Builder) {
        this.product = this.builder.GetResult();
    }
    showProduct() {
        return this.product.show();
    }
}
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());
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

6.3. Nhược điểm

6.4. Sử dụng Object Pool khi

6.5. Cấu trúc

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:

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.

class Taxi {
    constructor(private name: string) {}
    getName() : String {
        return this.name;
    }
    setName(name: string) {
        this.name = name;
    }
    toString() : String {
        return `Taxi [name = ${this.name}]`
    }
}
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));
    }
}
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));
    }
}
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();
}
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

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

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}`
        );
    }
}
let proto = new Customer("n/a", "n/a", "pending");
let prototype = new CustomerPrototype(proto);

let customer = prototype.clone();
customer.say();
name: n/a n/a, status: pending
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.

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
console.log(
  harryPotter.constructor === ngan.constructor,
  snake.constructor === ngan.constructor
); // Kết quả: true true
console.log(harryPotter.bash === ngan.bash, snake.bash === ngan.bash); // Kết quả: false false
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
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

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 đề:

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:

class Xe {
    public _lop!: Lop;
    set lop(lop: Lop) {
        this._lop = lop;
    }
}
let xe : Xe = new Xe();
xe.lop = new LopB();

Share this post on:

Bài trước
Behavioral Design Pattern — Giải thích dễ hiểu với TypeScript
Bài tiếp
Tổng quan Design Pattern — Khởi đầu series