# Design pattern trong Odoo: Chain of Responsibility

> Mẫu thiết kế Chain of Responsibility thay những khối if/else dài bằng một chuỗi handler như thế nào, qua ví dụ trong odoo-addons-path, kiểm tra chất lượng của Odoo và phê duyệt nhiều cấp của OCA.

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

---


Hãy hình dung việc xây dựng một quy trình kiểm tra chất lượng cho nhà máy, nơi mỗi sản phẩm phải đi qua một chuỗi bước kiểm tra. Ban đầu, các bước này được viết cứng trong một khối code tuyến tính. Theo thời gian, bước kiểm tra mới được thêm vào, một số bước cần đổi thứ tự và các trường hợp đặc biệt xuất hiện. Khối `if/else` lớn hay hàm nguyên khối trở nên khó sửa, và việc thêm hoặc đổi thứ tự các bước kiểm tra trở thành thao tác rủi ro.

Đây là lúc mẫu Chain of Responsibility phát huy tác dụng. Bài viết này giới thiệu cách mẫu thiết kế vận hành qua ba ví dụ thực tế.

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

Mẫu Chain of Responsibility (CoR) cho phép nhiều đối tượng lần lượt có cơ hội xử lý một yêu cầu. Bên gửi không cần biết handler nào sẽ xử lý: yêu cầu đi dọc theo chuỗi cho đến khi có một handler nhận xử lý.

Về mặt kỹ thuật, mẫu thiết kế này:

- thay các khối `if/else` lớn, dễ vỡ bằng những handler nhỏ, tập trung vào một việc;
- giúp việc thêm hành vi mới đơn giản như viết một handler mới và đặt nó vào chuỗi;
- tách bạch trách nhiệm: client khởi phát yêu cầu, còn mỗi handler biết cách xử lý những trường hợp cụ thể.

## Cách triển khai

Một cách triển khai điển hình trong Python gồm:

- một handler cơ sở trừu tượng, lưu tham chiếu đến handler kế tiếp và định nghĩa interface;
- các handler cụ thể, cài đặt phần xử lý thực tế;
- code phía client, nối các handler thành chuỗi và gửi yêu cầu.

### Ví dụ 1: chuỗi detector trong odoo-addons-path

[odoo-addons-path](https://github.com/trobz/odoo-addons-path) là công cụ dòng lệnh do Trobz phát triển, dùng để tính `addons_path` cuối cùng của Odoo bằng cách nhận diện cấu trúc thư mục của dự án. Cách làm đơn giản nhất là một khối `if/else` khổng lồ thử lần lượt mọi cấu trúc. Thay vào đó, mỗi cấu trúc có một detector riêng, tức một handler kiểm tra xem dự án có khớp với cấu trúc đó không và trả về các đường dẫn tương ứng.

Lớp cơ sở lưu detector kế tiếp và chuyển tiếp yêu cầu khi một detector không khớp:

```python
class CodeBaseDetector(ABC):
    _next_detector: Optional["CodeBaseDetector"] = None

    def set_next(self, detector: "CodeBaseDetector") -> "CodeBaseDetector":
        self._next_detector = detector
        return detector

    @abstractmethod
    def detect(self, codebase: Path) -> tuple[str, dict[str, Any]] | None:
        if self._next_detector:
            return self._next_detector.detect(codebase)
        return None
```

Một handler cụ thể, ở đây dành cho cấu trúc dự án của Trobz, trông như sau:

```python
class TrobzDetector(CodeBaseDetector):
    def detect(self, codebase: Path) -> tuple[str, dict[str, Any]] | None:
        if (codebase / ".trobz").is_dir():
            addons_dirs = []
            for item in (codebase / "addons").iterdir():
                if item.is_dir():
                    addons_dirs.append(item)
            return (
                "Trobz",
                {
                    "addons_dirs": addons_dirs,
                    "addons_dir": [codebase / "project"],
                    "odoo_dir": [
                        codebase / "odoo/addons",
                        codebase / "odoo/odoo/addons",
                    ],
                },
            )
        return super().detect(codebase)
```

Sau đó, công cụ nối các detector thành chuỗi theo thứ tự ưu tiên:

```python
trobz = TrobzDetector()
c2c = C2CDetector()
odoo_sh = OdooShDetector()
doodba = DoodbaDetector()
fallback = GenericDetector()
trobz.set_next(c2c).set_next(odoo_sh).set_next(doodba).set_next(fallback)
res = trobz.detect(codebase)
```

Mỗi detector hoặc trả về kết quả, hoặc chuyển yêu cầu cho detector kế tiếp, với một detector tổng quát ở cuối chuỗi làm phương án dự phòng.

### Ví dụ 2: kiểm tra chất lượng trên lệnh công việc trong Odoo MRP

Trong Odoo Manufacturing, việc sản xuất một sản phẩm có thể kích hoạt một hoặc nhiều [bước kiểm tra chất lượng trên lệnh công việc (work order)](https://www.odoo.com/documentation/19.0/applications/inventory_and_mrp/quality/quality_management/quality_checks.html#quality-check-on-work-order). Quality là tính năng thuộc Odoo Enterprise.

Thay vì viết cứng một trình tự cố định, mỗi bước kiểm tra được liên kết với bước kế tiếp và bước trước đó qua các field `next_check_id` và `previous_check_id`, nhờ vậy các bước kiểm tra tạo thành một chuỗi lưu trong cơ sở dữ liệu, giống như một [danh sách liên kết đôi](https://en.wikipedia.org/wiki/Doubly_linked_list).

Tại mỗi mắt xích, bước kiểm tra hoặc thực hiện logic của mình và cho phép sản xuất tiếp tục, hoặc chuyển quyền điều khiển cho bước kiểm tra kế tiếp trong chuỗi. Một phương thức nội bộ kiểu `_next()` thực hiện bước kiểm tra hiện tại rồi chuyển lệnh công việc sang bước kiểm tra được liên kết tiếp theo. Phác thảo đơn giản:

```python
def _next(self, continue_production=False):
    # perform the current check logic...
    # when finished, move to the next check in the chain:
    self.workorder_id._change_quality_check(position='next')
```

### Ví dụ 3: phê duyệt nhiều cấp trong OCA

Module [base_tier_validation](https://github.com/OCA/server-ux/tree/18.0/base_tier_validation) của OCA triển khai cơ chế phê duyệt nhiều cấp lấy cảm hứng từ Chain of Responsibility. Module này quản lý các quy trình phê duyệt, trong đó một bản ghi phải đi qua nhiều cấp duyệt (tier) trước khi được chấp thuận.

Mỗi cấp duyệt là một handler được định nghĩa trong model `tier.definition`, gồm:

- số thứ tự (sequence) quy định thứ tự xử lý;
- người duyệt được phân công;
- bộ lọc domain tùy chọn, quyết định cấp duyệt có áp dụng hay không;
- tùy chọn duyệt các cấp theo đúng thứ tự.

Trích đoạn đơn giản hóa của model:

```python
class TierDefinition(models.Model):
    _name = "tier.definition"

    # Handler properties
    sequence = fields.Integer(default=30)  # order in the chain
    definition_domain = fields.Char()  # condition for applying this handler

    # Reviewer assignment: a user, a group or a field on the record
    review_type = fields.Selection([
        ("individual", "Specific user"),
        ("group", "Any user in a specific group"),
        ("field", "Field in related record"),
    ])
    reviewer_id = fields.Many2one(comodel_name="res.users")
    reviewer_group_id = fields.Many2one(comodel_name="res.groups")
    reviewer_field_id = fields.Many2one(comodel_name="ir.model.fields")

    # Chain ordering
    approve_sequence = fields.Boolean(default=False)  # approve tiers in sequence order
```

Khi có yêu cầu phê duyệt, `request_validation` tìm các cấp duyệt ứng với model và công ty của bản ghi, chỉ giữ những cấp có domain khớp với bản ghi, tạo một tier review cho mỗi cấp và thông báo cho người duyệt:

```python
def request_validation(self):
    td_obj = self.env["tier.definition"]
    tr_obj = self.env["tier.review"]
    vals_list = []
    for rec in self:
        if rec._check_state_from_condition() and rec.need_validation:
            tier_definitions = td_obj.search(
                [
                    ("model", "=", self._name),
                    ("company_id", "in", [False] + rec._get_company().ids),
                ],
                order="sequence desc",
            )
            sequence = 0
            for td in tier_definitions:
                if rec.evaluate_tier(td):
                    sequence += 1
                    vals_list.append(rec._prepare_tier_review_vals(td, sequence))
    created_trs = tr_obj.create(vals_list)
    if any(self.mapped("can_review")):
        self._update_counter({"review_created": True})
    self._notify_review_requested(created_trs)
    return created_trs
```

Khi bật `approve_sequence`, mỗi cấp phải được duyệt theo thứ tự trước khi cấp tiếp theo được xem xét. Khi không bật, nhiều cấp có thể được duyệt song song. Module [purchase_tier_validation](https://github.com/OCA/purchase-workflow/tree/18.0/purchase_tier_validation) áp dụng cơ chế này cho đơn mua hàng.

## Khi nào nên dùng

Ba ví dụ trên cho thấy mẫu thiết kế này phù hợp khi:

- chương trình phải xử lý nhiều loại yêu cầu khác nhau và không biết trước handler cụ thể nào sẽ xử lý;
- nhiều handler cần lần lượt kiểm tra hoặc xử lý một yêu cầu theo một thứ tự nhất định;
- tập handler và thứ tự của chúng cần cấu hình hoặc mở rộng được mà không phải sửa code phía client.

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

**Ưu điểm:**

- **Nguyên tắc đóng/mở (Open/Closed)**: có thể thêm handler mới mà không phải sửa client.
- **Nguyên tắc đơn trách nhiệm (Single Responsibility)**: code gửi yêu cầu tách biệt khỏi code xử lý yêu cầu.
- **Thứ tự linh hoạt**: dễ kiểm soát hoặc thay đổi thứ tự xử lý.

**Nhược điểm:**

- **Hiệu năng**: trong trường hợp xấu nhất, một yêu cầu phải đi qua toàn bộ chuỗi.
- **Khó debug hơn**: logic phân tán ở nhiều handler nên luồng xử lý khó theo dõi hơn.
- **Yêu cầu không được xử lý**: nếu không có handler nào xử lý, yêu cầu có thể thất bại âm thầm, trừ khi có handler dự phòng hoặc báo lỗi rõ ràng.

## Kết luận

Chain of Responsibility là một mẫu thiết kế đơn giản, giúp tránh logic rẽ nhánh nguyên khối và làm việc xử lý yêu cầu linh hoạt, dễ mở rộng. Từ các detector trong công cụ dòng lệnh đến các bước kiểm tra chất lượng và các cấp duyệt lưu trong cơ sở dữ liệu Odoo, mẫu này phù hợp với những trường hợp mà trình tự xử lý cần được cấu hình, sắp xếp lại hoặc mở rộng theo thời gian.

