Tìm hiểu Factory Pattern và cách áp dụng trong JavaScript

Factory Pattern (Mẫu thiết kế nhà máy) định nghĩa một giao diện để tạo ra các đối tượng, nhưng cho phép các lớp con quyết định lớp nào sẽ được khởi tạo. Bằng cách dựa vào các tham số đầu vào để trả về các thực thể (instance) khác nhau, Factory Pattern giúp tách biệt logic khởi tạo đối tượng khỏi logic nghiệp vụ chính.

Đặc điểm cốt lõi

Trong JavaScript, chúng ta có thể sử dụng hàm khởi tạo hoặc class để tạo đối tượng. Tuy nhiên, khi quy trình tạo đối tượng trở nên phức tạp hoặc cần tạo nhiều đối tượng có chung giao diện nhưng khác nhau về hành vi, việc viết mã lặp đi lặp lại sẽ gây khó khăn cho bảo trì. Factory Pattern giải quyết vấn đề này bằng cách trừu tượng hóa quá trình khởi tạo.

Ưu điểm và Nhược điểm

  • Ưu điểm:
    • Tính đóng gói cao: Người dùng không cần biết chi tiết logic bên trong của việc tạo đối tượng.
    • Dễ dàng mở rộng: Tuân thủ nguyên tắc Open/Closed (Mở để mở rộng, đóng để sửa đổi).
    • Giảm sự phụ thuộc: Tách biệt mã nguồn cấp cao khỏi các lớp thực thi chi tiết.
  • Nhược điểm: Làm tăng độ phức tạp của hệ thống và thêm các lớp trừu tượng trung gian.

Ví dụ về Simple Factory

Dưới đây là một ví dụ đơn giản về việc tạo các loại thông báo khác nhau trong một ứng dụng:


class EmailProvider {
    send(message) {
        console.log(`Gửi Email với nội dung: ${message}`);
    }
}

class SMSProvider {
    send(message) {
        console.log(`Gửi SMS với nội dung: ${message}`);
    }
}

// Factory đơn giản
function NotificationFactory(type) {
    switch (type) {
        case 'email':
            return new EmailProvider();
        case 'sms':
            return new SMSProvider();
        default:
            throw new Error('Phương thức gửi không hỗ trợ');
    }
}

const messenger = NotificationFactory('email');
messenger.send('Chào mừng bạn đến với hệ thống!');

Cách tiếp cận trên được gọi là Simple Factory. Mặc dù dễ hiểu, nhưng nó có nhược điểm là mỗi khi cần thêm một loại thông báo mới, chúng ta phải sửa đổi trực tiếp hàm NotificationFactory, điều này vi phạm nguyên tắc Open/Closed.

Tối ưu hóa Factory Pattern

Để tăng tính linh hoạt, chúng ta có thể xây dựng một hệ thống đăng ký (Registry) cho Factory, cho phép thêm mới các loại đối tượng mà không cần can thiệp vào logic lõi của Factory.


class MessengerService {
    constructor() {
        this.providers = {};
    }

    // Đăng ký dịch vụ mới vào hệ thống
    registerProvider(name, ProviderClass) {
        this.providers[name] = ProviderClass;
    }

    // Khởi tạo dịch vụ dựa trên tên đã đăng ký
    create(name, ...args) {
        const Provider = this.providers[name];
        if (!Provider) {
            console.error('Dịch vụ chưa được đăng ký');
            return null;
        }
        return new Provider(...args);
    }
}

class PushNotification {
    constructor(appId) {
        this.appId = appId;
    }
    send(msg) {
        console.log(`Push App ${this.appId}: ${msg}`);
    }
}

const factory = new MessengerService();
factory.registerProvider('push', PushNotification);

const push = factory.create('push', 'APP_123');
push.send('Bạn có thông báo mới!');

Ứng dụng trong các thư viện thực tế

Factory Pattern xuất hiện rất phổ biến trong các framework JavaScript. Một ví dụ điển hình là hàm $() của jQuery. Thay vì yêu cầu người dùng sử dụng từ khóa new để tạo đối tượng jQuery, thư viện cung cấp một hàm Factory giúp xử lý nhiều loại đầu vào khác nhau (string, DOM element, function) và trả về một instance phù hợp.


// Mô phỏng tư duy Factory của jQuery
const MyQuery = function(selector) {
    return new MyQuery.fn.init(selector);
};

MyQuery.fn = MyQuery.prototype = {
    init: function(selector) {
        if (!selector) return this;
        
        if (typeof selector === "string") {
            // Logic tìm kiếm element theo string
            const nodes = document.querySelectorAll(selector);
            for(let i = 0; i < nodes.length; i++) this[i] = nodes[i];
            this.length = nodes.length;
        }
        return this;
    }
};
MyQuery.fn.init.prototype = MyQuery.fn;

Tổng kết

Nên sử dụng Factory Pattern khi bạn cần quản lý việc tạo ra các đối tượng có cấu trúc tương tự nhau nhưng khác nhau về thuộc tính hoặc cách thực thi. Nó giúp mã nguồn sạch hơn, giảm thiểu sự phụ thuộc lẫn nhau giữa các thành phần (decoupling). Tuy nhiên, nếu ứng dụng của bạn đơn giản và không có xu hướng mở rộng thêm nhiều loại đối tượng, việc sử dụng new trực tiếp sẽ giúp mã nguồn ngắn gọn và dễ đọc hơn.

Thẻ: JavaScript design-patterns factory-pattern oop

Đăng vào ngày 31 tháng 7 lúc 01:24