第四十五章 模块设计模式
篇别:第十三篇 最佳实践
本章学习目标
- 运用 低耦合、高内聚 原则划分 模块边界 与
depends。 - 通过 AbstractModel、Mixin、工具方法 复用逻辑,避免复制粘贴。
- 正确使用
UserError、ValidationError与 异常链,区分 用户可见错误与程序缺陷。 - 建立 结构化日志 习惯,服务 排障与审计。
- 处理 多公司、多货币 的 数据隔离与金额一致性。
导读:模式是「可维护性的复利」
Odoo 模块常在 数年 内持续迭代。过早把销售/库存/会计绑死在一个自定义模块里,后续 分包、复用、开源 都会痛。本章把 最常见 的 解耦、复用、错误、日志、多公司/货币 固定成 可教可练 的条目;与 第十二章(继承)、第七章(安全)、第四十六章(高级主题) 交叉。
45.1 模块解耦
45.1.1 知识要点
- depends 最小化:仅依赖 真正调用的模块;可选扩展 用
try/except ImportError或_inherit条件(谨慎)。 - 扩展优于复制:
_inherit标准模型;勿 fork 整份sale进私有模块。 - 接口层:跨模块调用 稳定方法名
_prepare_*、_get_*_values;避免 深钻对方private方法。 - 数据耦合:XML ID 引用 上游模块 时 注明版本;升级前读 changelog。
45.1.2 案例
差:复制 sale.order.action_confirm 200 行到自定义模块。
好:_inherit 中 super 前后 各 插入 5 行 钩子调用 _library_validate_order()。
45.1.3 截图占位

45.1.4 本节练习
- 简答:「依赖过多」 的 三个负面后果?
- 判断:为避免依赖,应用
eval动态 import 调其他模块。( )
参考答案提示:2. 错;难测、难审计、升级炸裂。
45.2 代码复用
45.2.1 知识要点
AbstractModel:_name = 'my.abstract.mixin'_abstract = True;共用的 compute、check、格式化。- Python 工具:纯函数
def format_isbn(code):放tools/;无 ORM 依赖 易测。 - Domain 工厂:
@api.model def _domain_open_loans(self):返回 list,列表/报表/规则 共用。 - 反模式:在 XML 里堆巨型 domain 复制 十次 → 改一处漏九处。
45.2.2 案例
class LibraryDiscountMixin(models.AbstractModel):
_name = "library.discount.mixin"
_description = ...
def _apply_discount(self, lines, percent):
for line in lines:
line.discount = max(line.discount, percent)
45.2.3 本节练习
- 实操:把 重复 domain 抽为
@api.model方法 并 两处调用。 - 简答:Mixin 与
_inherits委托继承 选型一句?
参考答案提示:2. 行为复用 用 Mixin;「是一条记录的组成部分」 用委托继承。
45.3 错误处理
45.3.1 知识要点
UserError:用户可理解、可恢复(填错字段、业务规则)。ValidationError:数据约束(Python + SQL)。AccessError:权限;勿 用UserError冒充。- 程序错误:
KeyError等 应 记录日志 并 500(或 Odoo 标准错误页),勿 吞掉。 raise ... from e:保留 异常链 便于 排障。
45.3.2 案例
import requests
from odoo.exceptions import UserError
try:
external_api.call()
except requests.Timeout as e:
raise UserError(_("同步超时,请稍后重试")) from e
45.3.3 本节练习
- 判断:所有异常 都应转成
UserError。( ) - 简答:何时应
_logger.exception再 re-raise?
参考答案提示:1. 错。2. 未预期缺陷、需要栈追踪 时。
45.4 日志记录
45.4.1 知识要点
_logger = logging.getLogger(__name__):模块级 logger。- 结构化
extra:{'integration': 'dhl', 'picking_id': picking.id},便于 ELK 检索。 - 级别:INFO 业务流程、WARNING 可恢复异常、ERROR 失败需人看。
- 隐私:勿 记录 完整卡号、密码;GDPR 下 日志保留策略。
45.4.2 案例
_logger.warning("DHL rate limit, backing off", extra={"partner_ref": partner.ref})
45.4.3 截图占位

45.4.4 本节练习
- 实操:为 集成失败 记录
extra={'partner_ref': ...}一行日志。 - 简答:生产 为何慎用
DEBUG全量开?
参考答案提示:2. 体积、PII、I/O、性能。
45.5 多公司架构(新增)
45.5.1 知识要点
company_id/company_ids:记录归属;规则('company_id', 'in', company_ids + [False])常见。- 共享主数据:
company_id空 表示 全局 或 规则显式允许;须测试 切换公司 菜单。 sudo+with_company:极危险;仅 在 知悉后果 的 内部封装 使用,并文档化。- 报表:默认公司上下文 与 用户所选公司 易混;显式
with_company。
45.5.2 案例
错误:sudo().create({...}) 未设 company_id → 新记录落在错误公司。
45.5.3 截图占位

45.5.4 本节练习
- 简答:
sudo+with_company联用 风险? - 实操:切换公司 后 列表 domain 是否仍 正确过滤(任选一模型 验证)。
参考答案提示:1. 绕过规则 + 写错公司 + 审计缺口。
45.6 多货币处理(新增)
45.6.1 知识要点
Monetary:currency_field与company_id.currency_id区分 单据币别 / 公司本位币。- 汇率:
res.currency.rate按日期;历史单据 应用 当时汇率 而非 今日。 - 报表:统一公司币 时 折算;勿 简单
sum外币金额。 - 舍入:
decimal.precision与 会计规则 对齐;浮点 勿 直接 == 比较。
45.6.2 案例
报表行:amount_foreign * rate_at(date) → amount_company;rate 来自 _convert 或 currency._convert(以版本 API 为准)。
45.6.3 截图占位

45.6.4 本节练习
- 实操:用
Monetary展示 外币单据 的 公司币折算(参考account模块 字段设计)。 - 简答:「用今天汇率重算去年报表」 何时 错误?
参考答案提示:2. 历史业绩分析 应 时点汇率;仅「当前估值」 可用现价。
本章综合练习
- 设计:插件式定价规则(策略模式草图:上下文、策略接口、注册表)。
- 文档:模块 README 必备章节(不少于 6 项:依赖、配置、已知问题 等)。
- 综合:同一 mixin 被 销售与采购 同时继承时 如何避免方法名冲突?
- 实操:列出你维护模块中 一处 可抽成 mixin 的 重复代码 并 草拟
_name。
本章对应白皮书目录:第四十五章 模块设计模式。