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

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

English

Structural pattern nói về composition — cách bạn ghép các object thành cấu trúc lớn hơn mà vẫn giữ được sự linh hoạt. Nếu bạn đã từng wrap một library để nó fit với interface của mình, hay xây cây component mà tất cả đều respond cùng một method, thì bạn đã dùng mấy ý tưởng này rồi. Đây là 9 pattern hệ thống hóa chúng.

1. Adapter/ Wrapper

Adapter Pattern là pattern giữ vai trò trung gian giữa hai lớp, chuyển đổi giao diện của một hay nhiều lớp có sẵn thành một giao diện khác, thích hợp cho lớp đang viết. Điều này cho phép các lớp có các giao diện khác nhau có thể dễ dàng giao tiếp tốt với nhau thông qua giao diện trung gian, không cần thay đổi code của lớp có sẵn cũng như lớp đang viết. Adapter Pattern còn gọi là Wrapper Pattern do cung cấp một giao diện “bọc ngoài” tương thích cho một hệ thống có sẵn, có dữ liệu và hành vi phù hợp nhưng có giao diện không tương thích với lớp đang viết

Adapter thì ai cũng biết rồi. Power adapter (cục chuyển đổi điện áp), laptop adapter (cục sạc), memory card adapter — tất cả đều làm một việc: nối hai thứ không tương thích lại với nhau. Adapter Pattern trong code cũng y hệt vậy.

1.1. Tại sao cần sử dụng Adapter Pattern

Mình từng rất chán khi phải viết đi viết lại cùng một đoạn code từ dự án này sang dự án khác. Đến lúc quyết định tự viết library để tái sử dụng thì lại gặp tình huống: interface phù hợp với dự án cũ nhưng sang dự án mới lại dùng không được. Lại hì hục ngồi sửa. Adapter Pattern sinh ra để giải quyết đúng chuyện này. Dùng khi:

Để hiểu Adapter Pattern, bạn cần nắm ba khái niệm:

1.2. Phân loại adapter

Hai khái niệm này cho ta hai cách cài đặt adapter: Object AdapterClass Adapter.

1.3. Class Adapter

Trong mô hình này, lớp Adapter kế thừa từ lớp có giao diện không tương thích (Adaptee), đồng thời implement giao diện mà người dùng mong muốn (Target). Khi gọi các phương thức của Target, Adapter sẽ gọi các phương thức nó kế thừa từ Adaptee bên trong.

1.3.1. Cấu trúc

1.3.2. Ví dụ

interface Target {
    request(): string;
}
class Adaptee {
    specificRequest() : string {
        return ".eetpadA eht fo roivaheb laicepS";
    }
}
class Adapter extends Adaptee implements Target {
    constructor() {
        super();
    }
    request(){
        return "Adapter: (TRANSLATED) "+this.specificRequest().split("").reverse().join("");
    }
}
function clientCode(target: Target) {
    console.log(target.request());
}
let adaptee = new Adaptee();
console.log("Client: The Adaptee class has a weird interface. See, I don't understand it");
console.log("Adaptee: "+adaptee.specificRequest());

console.log("Client: But I can work with it via the Adapter");
let adapter = new Adapter();
clientCode(adapter);
Client: The Adaptee class has a weird interface. See, I don't understand it
Adaptee: .eetpadA eht fo roivaheb laicepS
Client: But I can work with it via the Adapter
Adapter: (TRANSLATED) Special behavior of the Adaptee.

1.4. Object Adapter

Cách này dùng composition thay vì kế thừa. Adapter giữ một tham chiếu đến đối tượng Adaptee và implement giao diện Target. Khi gọi phương thức Target, Adapter ủy quyền cho Adaptee thông qua tham chiếu đó. Cách này tránh được vấn đề đa kế thừa — mà Java, C# không hỗ trợ. Mình khuyến khích dùng cách này.

1.4.1. Cấu trúc Object Adapter

1.4.2. Ví dụ

class Target {
  request() {
    return "Target: The default target's behavior.";
  }
}
class Adaptee {
  specificRequest() {
    return ".eetpadA eht fo roivaheb laicepS";
  }
}
class Adapter implements Target {
    constructor(private obj: Adaptee) { }
    request(){
        return "Adapter: (TRANSLATED) "+this.obj.specificRequest().split("").reverse().join("");
    }
}
function clientCode(target: Target) {
    console.log(target.request());
}
let adaptee = new Adaptee();
console.log("Client: The Adaptee class has a weird interface. See, I don't understand it");
console.log("Adaptee: "+adaptee.specificRequest());

console.log("Client: But I can work with it via the Adapter");
let adapter = new Adapter(adaptee);
clientCode(adapter);
Client: The Adaptee class has a weird interface. See, I don't understand it
Adaptee: .eetpadA eht fo roivaheb laicepS
Client: But I can work with it via the Adapter
Adapter: (TRANSLATED) Special behavior of the Adaptee.

1.5. Lời kết

Mình thấy Adapter Pattern cực kỳ hữu ích khi làm ứng dụng lớn có dùng nhiều API bên ngoài. Nó là lớp đệm — khi nhà cung cấp API thay đổi gì đó, bạn chỉ sửa adapter chứ không phải đào bới cả codebase. Đúng, bạn phải tạo thêm class và interface. Nhưng với hệ thống lớn, chi phí ban đầu đó hoàn toàn xứng đáng.

2. Bridge Pattern

Bridge pattern được sử dụng khi chúng ta muốn tách một lớp lớn với các tính năng phức tạp thành một lớp chính (Abstraction) và nhiều giao diện của các tính năng khác nhau (Implementation) để phát triển độc lập với nhau. Chúng được nối với nhau bởi việc lớp chính trỏ đến giao diện của các tính năng gọi là bridge (cầu nối). Nhờ đó việc chỉnh sửa Abstraction sẽ không tác động đến Implementation và ngược lại.

2.1. Vấn đề

Giả sử bạn có lớp Shape với hai lớp con: CircleSquare. Giờ bạn muốn thêm màu sắc. Vì đã có hai lớp con, bạn sẽ cần bốn tổ hợp: BlueCircle, RedSquare, v.v.

Số lượng class tăng theo cấp số nhân. Thêm Triangle? Hai class mới (mỗi màu một cái). Thêm một màu? Ba class mới (mỗi hình một cái). Mỗi chiều mở rộng thêm sẽ nhân lên tổng số class.

Đây chính là lúc cần đến Bridge Pattern.

2.2. Khi nào nên dùng Bridge pattern

2.3. Cấu trúc

2.4. Các bước thực hiện

2.5. Ví dụ

class Abstraction {
    constructor(protected iA : ImplementationA) { }
    operation() : string {
        return `Abstraction - Base operation with: \n${this.iA.operation_implementation()}`;
    }
}
interface ImplementationA {
    operation_implementation() : string;
}
class ConcreteAImplementationA implements ImplementationA {
    operation_implementation() : string {
        return `Concrete A of Implementation A`;
    }
}
class ConcreteBImplementationA implements ImplementationA {
    operation_implementation() : string {
        return `Concrete B of ImplementationA`;
    }
}
function client_code(abstraction: Abstraction) {
    console.log(abstraction.operation());
}
let concreteAimplementationA = new ConcreteAImplementationA();
let abstraction = new Abstraction(concreteAimplementationA);
client_code(abstraction);
Abstraction - Base operation with:
Concrete A of Implementation A
class ExtendedAbstraction extends Abstraction {
    constructor(protected iA: ImplementationA) {
        super(iA);
    }
    operation() : string {
        return `ExtendedAbstraction - Extended operation with: \n${this.iA.operation_implementation()}`;
    }
}

let concreteBimplementationA = new ConcreteBImplementationA();
let extendedAbstraction = new ExtendedAbstraction(concreteBimplementationA);
client_code(extendedAbstraction);
ExtendedAbstraction - Extended operation with:
Concrete B of ImplementationA

3. Composite

Composite pattern cho phép bạn làm việc với các đối tượng tương tự nhau với cấu trúc dạng cây

3.1. Khi nào nên dùng Composite pattern

3.2. Cấu trúc

Ba loại class:

3.3. Cách cài đặt

3.4. Ví dụ

class Employee {
    private subordinates: Array<Employee>;
    constructor(
        private name: string,
        private dept: string,
        private salary: number
    ) {
        this.subordinates = new Array<Employee>();
    }
    add(e: Employee): void {
        this.subordinates.push(e);
    }
    remove(e: Employee): void {
        this.subordinates.splice(this.subordinates.indexOf(e), 1);
    }
    getChildren(): Array<Employee> {
        return this.subordinates;
    }
    toString(): string {
        return (`Employee :[ Name : ${this.name}, dept : ${this.dept}, salary : ${this.salary} ]`);
    }
}
let CEO: Employee = new Employee("John", "CEO", 30000);
let headSales : Employee = new Employee("Robert", "Head Sales", 20000);
let headMarketing : Employee = new Employee("Michel", "Head Marketing", 20000);

let clerk1 : Employee = new Employee("Laura", "Marketing", 10000);
let clerk2 : Employee = new Employee("Bob", "Marketing", 10000);

let salesExecutive1 : Employee = new Employee("Richard", "Sales", 10000);
let salesExecutive2 : Employee = new Employee("Rob", "Sales", 10000);

CEO.add(headSales);
CEO.add(headMarketing);

headSales.add(salesExecutive1);
headSales.add(salesExecutive2);

headMarketing.add(clerk1);
headMarketing.add(clerk2);

//print all employees of the organization
console.log(CEO);
for (let headEmployee of CEO.getChildren()) {
    console.log(headEmployee);
    for (let employee of headEmployee.getChildren())
        console.log(employee);
}
Employee :[ Name : John, dept : CEO, salary : 30000 ]
Employee :[ Name : Robert, dept : Head Sales, salary : 20000 ]
Employee :[ Name : Richard, dept : Sales, salary : 10000 ]
Employee :[ Name : Rob, dept : Sales, salary : 10000 ]
Employee :[ Name : Michel, dept : Head Marketing, salary : 20000 ]
Employee :[ Name : Laura, dept : Marketing, salary : 10000 ]
Employee :[ Name : Bob, dept : Marketing, salary : 10000 ]

4. Decorator

Decorator thường được dùng khi ta muốn thêm chức năng cho một đối tượng đã tồn tại trước đó, mà không muốn ảnh hưởng đến các đối tượng khác

4.1. Vấn đề

Đôi khi mình cần mở rộng một phương thức trong đối tượng. Cách thông thường là kế thừa. Nhưng có những lúc kế thừa làm code phức tạp hơn mức cần thiết. Decorator Pattern cho bạn cách mở rộng linh hoạt hơn.

4.2. Khác biệt giữa mở rộng phương thức theo cách linh động với mở rộng theo cách tĩnh

4.3. Ví dụ

Tưởng tượng bạn làm ở cửa hàng Pizza. Cửa hàng làm pizza cà chuapizza phô mai. Khách hàng có thể thêm topping: gà hoặc tiêu. Vậy bạn có các combo: pizza gà cà chua, pizza cà chua hạt tiêu, pizza gà phô mai, pizza phô mai hồ tiêu, pizza cà chua gà hồ tiêuphô mai gà hồ tiêu. Dùng mở rộng tĩnh thì phải tạo một class cho mỗi combo: TomatoChickenPizza, TomatoPepperPizza… Số lượng class bùng nổ. Phải dùng cách động.

4.4. Cấu trúc

Các thành phần:

4.5. Thực hành

interface IPizza {
    doPizza() : string;
}
class TomatoPizza implements IPizza {
    doPizza(): string {
        return "I am a Tomato Pizza";
    }
}
class ChickenPizza implements IPizza {
    doPizza(): string {
        return "I am a Chicken Pizza";
    }
}
abstract class PizzaDecorator implements IPizza {
    constructor(protected mPizza: IPizza) { }

    getPizza(): IPizza {
        return this.mPizza;
    }

    setPizza(mPizza: IPizza): void {
        this.mPizza = mPizza;
    }

    abstract doPizza(): string;
}
class CheeseDecorator extends PizzaDecorator {
    constructor(pizza: IPizza) {
        super(pizza);
    }

    doPizza(): string {
        let type: string = this.mPizza.doPizza();
        return type + this.addCheese();
    }

    // This is additional functionality
    // It adds cheese to an existing pizza
    addCheese(): string {
        return " + Cheese";
    }
}
class PepperDecorator extends PizzaDecorator {
    constructor(pizza: IPizza) {
        super(pizza);
    }

    doPizza(): string {
        let type: string = this.mPizza.doPizza();
        return type + this.addPepper();
    }

    // This is additional functionality
    // It adds cheese to an existing pizza
    addPepper(): string {
        return " + Pepper";
    }
}
let tomato : IPizza = new TomatoPizza();
let chicken : IPizza = new ChickenPizza();

console.log(tomato.doPizza());
console.log(chicken.doPizza());

// Use Decorator pattern to extend existing pizza dynamically
// Add pepper to tomato-pizza
let pepperDecorator : PepperDecorator = new PepperDecorator(tomato);
console.log(pepperDecorator.doPizza());

// Add cheese to tomato-pizza
let cheeseDecorator : CheeseDecorator = new CheeseDecorator(tomato);
console.log(cheeseDecorator.doPizza());

// Add cheese and pepper to tomato-pizza
// We combine functionalities together easily.
let cheeseDecorator2 : CheeseDecorator = new CheeseDecorator(pepperDecorator);
console.log(cheeseDecorator2.doPizza());
I am a Tomato Pizza
I am a Chicken Pizza
I am a Tomato Pizza + Pepper
I am a Tomato Pizza + Cheese
I am a Tomato Pizza + Pepper + Cheese

5. Facade

Bao bọc một hệ thống con phức tạp với một giao diện đơn giản

5.1. Ưu điểm của Facade Pattern?

5.2. Vấn đề

Giả sử bạn có chuỗi hành động thực hiện theo thứ tự, và chuỗi này cần dùng ở nhiều nơi trong ứng dụng. Bạn copy-paste nó khắp nơi. Nhanh gọn. Rồi bạn nhận ra cần thay đổi chuỗi xử lý đó. Giờ phải lục lại tất cả các chỗ, sửa từng cái một, và cầu nguyện không bỏ sót chỗ nào. Tốn thời gian. Mất kiểm soát. Dễ sinh bug.

5.3. Giải pháp

Tạo một Facade với phương thức bao bọc chuỗi code lặp đi lặp lại. Mọi nơi chỉ cần gọi Facade. Cần thay đổi logic? Sửa một chỗ. Xong.

5.4. Cấu trúc

Các subsystem bên trong Facade cũng có thể sử dụng Facade.

5.5. Ví dụ

Xét quy trình checkout khi người dùng mua hàng online trên trang web của bạn:

// Xử lý checkout
let productId = "123456";
let product = Product.find(productId);
if(product.length > 0) {
    // Thêm vào giỏ hàng
    let cart = new Cart();
    cart.addItem(product);
    // Tính phí ship hàng
    let shipping = new ShippingCharge(product);
    shipping.calculateCharge();
    // Tính mã giảm giá
    let discount = new Discount(product);
    discount.applyDiscount();
    // Tạo mã đơn hàng
    let order = new Order();
    order.generateOrder();
    ...
}
class OrderFacade {
        private product: any;
        constructor(productId: string) {
            this.product = Product.find(productId);
        }
        generateOrder() {
            // Xử lý toàn bộ logic
            if(this.checkQuantity()) {
                this.addToCart();
                this.calulateShipping();
                this.applyDiscount();
                this.placeOrder();
            }

        }
        private addToCart () {
            let cart : Cart = new Cart();
            cart.addItem(this.product);
        }
        private checkQuantity() {
            return this.product.length != 0 ? true : false;
        }
        private function calulateShipping() {
            let shipping : ShippingCharge = new ShippingCharge(this.product);
            shipping.calculateCharge();
        }
        private applyDiscount() {
            let discount : Discount = new Discount();
            discount.applyDiscount(this.product);
        }
        private placeOrder() {
            let order : Order = new Order();
            order.generateOrder();
        }
    }
let productId = "123456";
let order = new OrderFacade(productId);
order.generateOrder();

Toàn bộ logic phức tạp giờ nằm gọn trong generateOrder. Cần thay đổi? Sửa Facade. Cần dùng ở chỗ khác? Gọi generateOrder(). Không cần bê cả đống code đi nữa.

5.6. Kết

Mình hay nghĩ Facade Pattern giống như có thư ký. Bạn nói cần làm gì, cô ấy lo phần còn lại. Facade object cũng vậy — nó điều phối nhiều nhiệm vụ phía sau một lời gọi duy nhất.

6. Proxy

Proxy cung cấp một class ảo đứng trước class thực sự mà chúng ta muốn làm việc để xử lí, tăng thêm tính bảo mật và cải thiện performance cho hệ thống khi xử lí dữ liệu liên quan đến class mà chúng ta làm việc

6.1. Khi nào nên sử dụng Proxy Pattern?

6.2. Các loại ứng dụng của Proxy Pattern

6.3. Cấu trúc

Ba thành phần:

6.4. Ví dụ

interface IItem {
    delaytoResponseContent(): Promise<number>;
    content: number;
}

class Item implements IItem {
    // Giá trị của content nằm trên server
    protected _content: number = Math.round(Math.random() * 10);
    get content() {
        return this._content;
    }
    set content(n: number) {
        this._content = n;
    }

    // Trả về giá trị của content mất nhiều chi phí
    async delaytoResponseContent() : Promise<number> {
        await this.sleep(1000);
        return this.content;
    }

    // Tạo chi phí kết nối
    sleep(ms: number) {
        return new Promise((resolve) =>
            setTimeout(resolve, ms)
        )
    }
}
interface Subject {
    data: Array<IItem>;
    Request(index: number): any;
}

class RealSubject {
    constructor(private _data: Array<IItem>) { }
    get data() : Array<IItem> {
        return this._data;
    }

    Request(index: number) : Promise<number> {
        return this.data[index].delaytoResponseContent();
    }
}

class SubjectProxy implements Subject {
    // Khai báo cho đúng cấu trúc nhưng không sử dụng
    data!: Array<IItem>;
    // Duy trì một kết nối đến RealSubject để xử lí dữ liệu
    private realSubject!: RealSubject;

    // Kết nối đến host thông qua RealSubject
    constructor(data: Array<IItem>) {
        this.realSubject = new RealSubject(data);
    }

    // - Ghi đè lại phương thức Request
    // - Thay vì lấy về giá trị content mất nhiều chi phí
    //   thì ta chỉ lấy về index/id của giá trị đó
    // - Khi nào người dùng cần giá trị content thì mới
    //   sử dụng phương thức delaytoResponseContent để
    //   lấy giá trị content về
    Request(index: number) : IItem {
        return this.realSubject.data[index];
    }
}
// Tạo một host giả có chứa content và index Item
let data : Array<IItem> = [
    new Item(),
    new Item(),
    new Item(),
    new Item(),
    new Item(),
    new Item(),
];
let realSubject = new RealSubject(data);
(async () => {
    let wanted : number = 3;
    for(let i = 0; i < 5; i++) {
        await realSubject.Request(i).then(
            res => {
                if(i!=wanted)
                    console.log("Result: "+res)
                else
                    console.log("Wanted result: "+res)
            }
        );
    }
})();
Result: 4
Result: 5
Result: 5
Wanted result: 4
Result: 9
let proxy = new SubjectProxy(data);
(async () => {
    let wanted : number = 3;
    let results : Array<IItem> = new Array<IItem>();
    for(let i = 0; i < 5; i++) {
        results.push(proxy.Request(i));
        if(i==wanted)
            results[i].delaytoResponseContent()
                .then(res => console.log("Wanted result via proxy: "+res));
    }
    for(let i = 0; i < 5; i++)
        if(i!=wanted)
            results[i].delaytoResponseContent()
                .then(res => console.log("Result via proxy: "+res))
})();
Wanted result via proxy: 1
Result via proxy: 0
Result via proxy: 7
Result via proxy: 2
Result via proxy: 1

6.5. So sánh với pattern cùng loại (Structural Pattern)

Proxy trông khá giống AdapterDecorator, nên cần phân biệt:

7. Flyweight

Được dùng như một object chia sẻ, có thể được sử dụng đồng thời ở nhiều ngữ cảnh khác nhau, hoạt động như một đối tượng độc lập tại mỗi ngữ cảnh. Mỗi đối tượng cụ thể sẽ tham chiếu đến cùng một instance được chia sẻ ở trong pool của Flyweight object

7.1. Vấn đề

Mình thấy ví dụ này khá hay. Bạn xây dựng game bắn súng sinh tồn kiểu PUBG. Hoàn thành, push code, build, gửi bạn chơi thử. Game crash sau vài phút. Sau mấy tiếng đào error log, phát hiện ra game hết RAM vì hệ thống particle. Mỗi viên đạn, tên lửa, mảnh đạn là một object riêng chứa đầy dữ liệu. Lúc chiến đấu căng, particle mới spawn liên tục cho đến khi RAM tràn. Game die.

7.2. Giải quyết

Nhìn kỹ lớp Particle, bạn thấy trường colorsprite ngốn bộ nhớ nhiều nhất. Tệ hơn, hai trường này lưu dữ liệu gần giống hệt nhau trên tất cả các object — đó là Intrinsic state. Tất cả đạn có cùng color và sprite. Chỉ khác coord, vector, speed — đó là Extrinsic state.

Cách giải: ngừng lưu extrinsic state bên trong object. Chuyển chúng sang tham số phương thức. Object giờ chỉ giữ intrinsic state. Tái sử dụng các object đã “gọt” này kết hợp với extrinsic state từng ngữ cảnh — tiết kiệm bộ nhớ mà vẫn đủ dữ liệu. Object sau khi loại bỏ extrinsic state gọi là flyweight.

Lưu ý: Vì flyweight được chia sẻ giữa nhiều ngữ cảnh, tuyệt đối không sửa đổi thuộc tính của nó sau khi khởi tạo. Set một lần, xong rồi thôi.

Quay lại game. Giả sử có ba loại particle: đạn, tên lửa, mảnh đạn. Spawn rất nhiều, cần tối ưu. Sau khi tách extrinsic state khỏi các class (biến chúng thành flyweight), extrinsic state đi đâu? Nó vẫn riêng của từng object nên phải lưu đâu đó. Thường thì đưa vào Container — nơi quản lý cả extrinsic state lẫn flyweight. Container ở đây là lớp Game. Extrinsic state vào Container qua đối tượng Context, bên trong chứa:

Để quản lý flyweight tốt hơn, dùng FlyweightFactory thay vì mảng thường.

7.3. Khi nào nên sử dụng Flyweight Pattern?

Chỉ khi chương trình phải chứa số lượng lớn object gần giống nhau và bạn cần tối ưu bộ nhớ. Nếu không, thêm complexity không đáng.

7.4. Cấu trúc

Các thành phần:

7.5. Cài đặt Flyweight Pattern

7.6. Thực hành

class Flyweight {
    constructor(
        public model: string,
        public processor: string
    ) { }
    operation(memory: number, tag: string) {
        return `Computer: Model = ${this.model}, Proccessor = ${this.processor}, Memory = ${memory}, Tag = ${tag}`;
    }
}
class FlyWeightFactory {
    constructor() { }
    // Quản lí các flyweight
    public cache: any = {};
    getFlyweight(
        model: string,
        processor: string
    ) {
        if (!this.cache[model + processor]) {
            this.cache[model + processor] =
                new Flyweight(model, processor);
        }
        return this.cache[model + processor];
    }
    count() {
        var count = 0;
        for (var f in this.cache) count++;
        return count;
    }
}
class Context {
    constructor(
        public flyweight: Flyweight,
        public memory: number, // Unique state
        public tag: string // Unique state
    ) { }
    operation() {
        return this.flyweight.operation(this.memory, this.tag);
    }
}
class Client {
    constructor() {}
    public flyweightFactory : FlyWeightFactory = new FlyWeightFactory();
    public contexts : Array<Context> = new Array<Context>();
    public countContext : number = 0;
    add(
        model: string,
        processor: string,
        memory: number,
        tag: string
    ) {
        this.contexts.push(
            new Context(
                this.flyweightFactory.getFlyweight(
                    model, processor
                ),
                memory, tag
            )
        );
        this.countContext++;
    }
    getContext(
        model: string,
        processor: string,
        memory: number,
        tag: string
    ) {
        return this.flyweightFactory.getFlyweight(
            model, processor
        ).operation(memory, tag);
    }
    getCountContext() {
        return this.countContext;
    }
}
var computers = new Client();

computers.add("Studio XPS", "Intel", 5, "Y755P");
computers.add("Studio XPS", "Intel", 6, "X997T");
computers.add("Studio XPS", "Intel", 2, "U8U80");
computers.add("Studio XPS", "Intel", 2, "NT777");
computers.add("Studio XPS", "Intel", 2, "0J88A");
computers.add("Envy", "Intel", 4, "CNU883701");
computers.add("Envy", "Intel", 2, "TXU003283");

console.log("Computer counting: " + computers.getCountContext());
console.log("Flyweight counting: " + computers.flyweightFactory.count());
Computer counting: 7
Flyweight counting: 2
console.log("Before changes: ");
for (let [k, v] of computers.contexts.entries()) {
  console.log(`(${k + 1}) => ` + v.operation());
}

computers.flyweightFactory.getFlyweight("Studio XPS", "Intel").processor =
  "AMD";

console.log("After changes: ");
for (let [k, v] of computers.contexts.entries()) {
  console.log(`(${k + 1}) => ` + v.operation());
}
Before changes:
(1) => Computer: Model = Studio XPS, Proccessor = Intel, Memory = 5, Tag = Y755P
(2) => Computer: Model = Studio XPS, Proccessor = Intel, Memory = 6, Tag = X997T
(3) => Computer: Model = Studio XPS, Proccessor = Intel, Memory = 2, Tag = U8U80
(4) => Computer: Model = Studio XPS, Proccessor = Intel, Memory = 2, Tag = NT777
(5) => Computer: Model = Studio XPS, Proccessor = Intel, Memory = 2, Tag = 0J88A
(6) => Computer: Model = Envy, Proccessor = Intel, Memory = 4, Tag = CNU883701
(7) => Computer: Model = Envy, Proccessor = Intel, Memory = 2, Tag = TXU003283
After changes:
(1) => Computer: Model = Studio XPS, Proccessor = AMD, Memory = 5, Tag = Y755P
(2) => Computer: Model = Studio XPS, Proccessor = AMD, Memory = 6, Tag = X997T
(3) => Computer: Model = Studio XPS, Proccessor = AMD, Memory = 2, Tag = U8U80
(4) => Computer: Model = Studio XPS, Proccessor = AMD, Memory = 2, Tag = NT777
(5) => Computer: Model = Studio XPS, Proccessor = AMD, Memory = 2, Tag = 0J88A
(6) => Computer: Model = Envy, Proccessor = Intel, Memory = 4, Tag = CNU883701
(7) => Computer: Model = Envy, Proccessor = Intel, Memory = 2, Tag = TXU003283

8. Delegate

Loại bỏ các chức năng phức tạp từ class chính bằng cách đưa chúng vào class khác xử lí

8.1. Vấn đề

Ví dụ thực tế: vận chuyển hàng bằng xe khách, tàu hỏa, hoặc máy bay. Hàng nhẹ cần gấp thì đi máy bay. Hàng cồng kềnh cần nhanh thì đi xe khách. Hàng chuyển chậm được thì đi tàu. Bạn có ba class: RailShipper, BusShipper, PlaneShipper, đều có phương thức delivery. Thông thường bạn viết logic điều kiện để chọn đúng loại. Thêm nhiều loại vận chuyển nữa thì class phình to và rối.

8.2. Giải quyết

Tách logic chọn phương thức vận chuyển ra lớp ShipperHandler. Lớp này chọn shipper phù hợp và gọi delivery cho bạn.

8.3. Cấu trúc

Ba thành phần:

8.4. Thực hành

interface Shipper {
    delivery(): string;
}

class RailShipper implements Shipper {
    delivery() {
        return `Package is delivering by train`;
    }
}

class BusShipper implements Shipper {
    delivery() {
        return `Package is delivering by bus`;
    }
}

class PlaneShipper implements Shipper {
    delivery() {
        return `Package is delivering by plane`;
    }
}

class ShipperHandler implements Shipper {
    private shipper!: Shipper;
    constructor(type: string) {
        this.shipper = eval(`new ${type}Shipper();`);
    }
    delivery() {
        return this.shipper.delivery()
    }
}

let shipper: Shipper = new ShipperHandler('Bus');
console.log(shipper.delivery());
Package is delivering by bus

8.5. Lời kết

Delegate Pattern có điểm giống kế thừa trong OOP nhưng linh hoạt hơn. Cũng có nét tương đồng với Proxy Pattern, tuy mỗi pattern hữu ích trong tình huống khác nhau. Mình nghĩ điều quan trọng nhất với design pattern là: dùng khi phù hợp. Đừng ép pattern vào bài toán chỉ vì bạn biết nó tồn tại.

9. Entity-Attribute-Value (EAV)

Entity-Attribute-Value Pattern viết tắt là EAV Pattern, là 1 mô hình dữ liệu, làm việc với các thực thể (entity) có số lượng các thuộc tính (attribute) có thể mở rộng

EAV là kỹ thuật thiết kế CSDL cho hệ thống cần tùy biến cao. Nếu bạn từng làm việc với Magento thì đã thấy nó rồi — đó là xương sống của hệ thống product attribute trong Magento. Ở đây mình trình bày phiên bản đơn giản: Product Attributes (Level 2 trong hệ thống quản lý sản phẩm của Magento).

9.1. Cấu trúc

Ba bảng:

Mối quan hệ giữa các bảng:

Ví dụ về mối quan hệ giữa Entity-Attribute-Value:

value_identity_idvalueattribute_id
11”S”1
21”Trắng”2
31303
411004
52”S”1
62”Đen”2
72203
822004

9.2. Ví dụ

Bạn đang xây dựng trang bán hàng (quần áo chẳng hạn). Database đơn giản thôi:

Đây là cách thiết kế đơn giản nhất, dễ tiếp cận. Nhưng cũng dễ vỡ khi hệ thống lớn lên (Hệ thống quản lý sản phẩm Level 1).

Khi công ty mở rộng — bán điện thoại, máy tính, đồ gia dụng — cần thêm nhiều trường: color, size, weight, chip, ram… Với bảng phẳng, bạn thêm hàng chục cột nhưng mỗi query chỉ dùng vài cột. Lãng phí. Đây là lúc EAV phát huy tác dụng (Hệ thống quản lý sản phẩm Level 2).

Với EAV, cấu trúc bảng trở thành:

Giờ bạn có thể thêm thuộc tính từ trang quản trị mà không cần đụng đến schema database.

  1. Thêm record vào bảng attributes cho thuộc tính color:
id=1, name="color"
  1. Thêm giá trị thuộc tính và product_id tương ứng vào bảng attribute_value:
id = 1, attribute_id = 1, value = "Đỏ", product_id = 1
id = 2, attribute_id = 1, value = "Xanh Lam", product_id = 2
id = 3, attribute_id = 1, value = "Vàng", product_id = 7
id = 4, attribute_id = 1, value = "Trắng", product_id = 5

Linh hoạt hơn nhiều. Nếu muốn nâng cao kỹ năng thiết kế hệ thống — đặc biệt e-commerce — mình khuyên nên nghiên cứu các mã nguồn mở như Magento. Cách họ thiết kế database dạy mình rất nhiều về tư duy thiết kế ở quy mô lớn.


Share this post on:

Bài trước
Tổng quan Design Pattern — Khởi đầu series