# Design pattern trong Odoo: Proxy

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

**Source:** <https://trobz.com/vi/insights/design-patterns-odoo-proxy/>

---


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ư `get` và `set`.

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

Trong module web, [`localization`](https://github.com/odoo/odoo/blob/209f9ea327d59c2583edf7399549dab66dc23d4b/addons/web/static/src/core/l10n/localization.js#L26) 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:

```javascript
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](https://github.com/odoo/odoo/blob/209f9ea327d59c2583edf7399549dab66dc23d4b/addons/web/static/src/core/l10n/localization_service.js#L70):

```javascript
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](https://github.com/odoo/odoo/blob/209f9ea327d59c2583edf7399549dab66dc23d4b/addons/mail/static/src/model/make_store.js#L57) được bọc trong proxy. Trap `get` xử lý việc tính toán lười (lazy computation):

```javascript
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`:

```javascript
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`](https://github.com/odoo/odoo/blob/209f9ea327d59c2583edf7399549dab66dc23d4b/addons/mail/static/src/model/store.js#L37), 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

- Patterns.dev, [Proxy Pattern](https://www.patterns.dev/vanilla/proxy-pattern/).

