Mẫu thiết kế Proxy kiểm soát việc truy cập một đối tượng như thế nào, qua hai ví dụ trong JavaScript của Odoo: đối tượng localization và các bản ghi reactive trong store của module mail.

Khi làm việc với code JavaScript trong Odoo, lập trình viên có thể gặp Proxy. Proxy là một mẫu thiết kế cấu trúc (structural design pattern), cung cấp một đối tượng thay thế cho một đối tượng khác. Proxy kiểm soát việc truy cập vào đối tượng gốc và có thể chạy thêm logic trước hoặc sau khi yêu cầu đến được đối tượng đó. Có thể hình dung proxy như nhân viên bảo vệ ở cửa tòa nhà, kiểm tra giấy tờ trước khi cho người vào.

Tổng quan về mẫu thiết kế

Đôi khi, việc truy cập một đối tượng cần thêm logic: trì hoãn khởi tạo, hạn chế quyền truy cập, lưu cache kết quả hoặc xử lý các vấn đề khác. Sửa trực tiếp đối tượng gốc không phải lúc nào cũng hợp lý, vì điều đó làm code phức tạp hơn và vi phạm nguyên tắc đơn trách nhiệm (single responsibility principle).

Mẫu Proxy giải quyết vấn đề này bằng một đối tượng proxy có cùng interface với đối tượng gốc. Client làm việc với proxy mà không biết đó không phải đối tượng thật, còn proxy quản lý việc truy cập đối tượng gốc, thêm logic trước hoặc sau khi chuyển giao yêu cầu.

Cách triển khai trong JavaScript

JavaScript có sẵn đối tượng Proxy để triển khai mẫu thiết kế này. Proxy nhận hai tham số:

  • Target: đối tượng gốc.
  • Handler: đối tượng định nghĩa hành vi tùy chỉnh, thông qua các trap như getset.

Ví dụ 1: kiểm soát truy cập trong localization

Trong module web, localization là một đối tượng proxy chứa các thiết lập bản địa hóa của người dùng:

export const localization = new Proxy(
    {},
    {
        get: (target, p) => {
            // "then" can be called implicitly if the object is returned in an
            // `async` function, so we need to allow it.
            if (p in target || p === "then") {
                return Reflect.get(target, p);
            }
            throw new Error(
                `could not access localization parameter "${p}": parameters are not ready yet. Maybe add 'localization' to your dependencies?`
            );
        },
    }
);

Các thuộc tính của đối tượng này được gán sau đó trong localization service :

Object.assign(localization, {
    dateFormat,
    timeFormat,
    dateTimeFormat: `${dateFormat} ${timeFormat}`,
    decimalPoint: userLocalization.decimal_point,
    direction: userLocalization.direction,
    grouping: JSON.parse(userLocalization.grouping),
    multiLang: result.multi_lang,
    thousandsSep: userLocalization.thousands_sep,
    weekStart: userLocalization.week_start,
});

Khi code đọc một thuộc tính như localization.dateFormat, proxy sẽ chặn lại truy cập đó. Nếu thuộc tính chưa được gán, proxy ném lỗi thay vì âm thầm trả về undefined, nhờ vậy lỗi do dữ liệu chưa khởi tạo lộ ra sớm. API Reflect có sẵn giúp chuyển tiếp việc truy cập đến đối tượng target một cách đơn giản.

Ví dụ 2: reactivity và tính toán lười trong mail

Trong module mail, các bản ghi trong store được bọc trong proxy. Trap get xử lý việc tính toán lười (lazy computation):

get(record, name, recordFullProxy) {
    // ...
    if (Model._.fieldsCompute.get(name) && !Model._.fieldsEager.get(name)) {
        record._.fieldsComputeInNeed.set(name, true);
        if (record._.fieldsComputeOnNeed.get(name)) {
            record._.compute(record, name);
        }
    }
    // ...
    return Reflect.get(record, name, recordFullProxy);
},

Khi một field computed không phải eager, trap đánh dấu field đó là cần tính và chỉ tính giá trị khi field được đọc, giúp tránh xử lý thừa.

Phần đáng chú ý là trap set:

set(record, name, val, receiver) {
    // ...
    return store.MAKE_UPDATE(function recordSet() {
        // ...
        store._.updateFields(record, { [name]: val });
        // ...
        return true;
    });
},

Thay vì gán giá trị trực tiếp, trap chạy việc cập nhật bên trong store.MAKE_UPDATE , gom nhiều thay đổi vào một chu kỳ cập nhật duy nhất để giao diện phản ứng với thay đổi dữ liệu hiệu quả hơn.

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

Ưu điểm:

  • Bổ sung chức năng: có thể thêm kiểm tra dữ liệu, kiểm tra bảo mật hoặc cache mà không sửa code gốc.
  • Khởi tạo lười: đối tượng hoặc giá trị tốn kém chỉ được tạo khi thực sự cần.
  • Tách biệt trách nhiệm: hành vi bổ sung tách riêng khỏi logic nghiệp vụ cốt lõi.

Nhược điểm:

  • Phức tạp hơn: lớp trung gian có thể khiến việc debug khó hơn.
  • Tốn thêm hiệu năng: mỗi thao tác bị chặn đều tốn thêm một chút chi phí.

Kết luận

Mẫu Proxy giúp code JavaScript của Odoo kiểm soát và mở rộng hành vi của đối tượng mà không cần sửa chính đối tượng đó. Dù dùng cho tính toán lười, kiểm tra dữ liệu hay bảo vệ dữ liệu chưa sẵn sàng như với localization, proxy giúp code vững chắc và dễ bảo trì hơn.

Tài liệu tham khảo