资讯详情

从应用间 API 约定到强制隔离:Open edX learning_sequences 应用的可维护性 ADR 实践指南

📅 2026/9/17 22:50:44 | 华诺云谱 👁 阅读
从应用间 API 约定到强制隔离:Open edX learning_sequences 应用的可维护性 ADR 实践指南
从应用间 API 约定到强制隔离Open edX learning_sequences 应用的可维护性 ADR 实践指南【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform在 Open edX 单体仓库中数十个 Django app 长期互相侵入对方的 Python 内部实现导致模块边界模糊、改动牵一发动全身。docs/decisions/0002-inter-app-apis.rst确立了“每个 app 通过顶层api.py暴露唯一 API 入口”的约定而 0001-extensions-to-inter-app-apis.rst 则在此基础上针对多年实践暴露出的问题API 返回与输入不透明、兼容性破坏难以察觉、视图所依赖的关键功能在 API 中缺失或行为不一致等提出了 7 条更严格的扩展约定并以 learning_sequences 应用为试验场。读完本文你将理解这套扩展约定的每条决策的动机、在仓库中的落地方式以及如何在自己的 app 中复制这一模式。背景从api.py到api包约定既有约定ADR 00020002-inter-app-apis.rst 定义了应用间 API 的基础约定每个 Django app 应明确定义一套暴露给其他 app 的 Python APIAPI 定义在 app 最顶层目录的api.py模块中API 命名应良好、自洽且贴近自身领域不暴露技术实现细节app 的 Django models 及其他内部数据结构不得通过 Python API 暴露测试应优先只使用其他 app 的api.py中声明的 API若某 API 仅用于测试则应定义在专门的api_for_tests.py中。该文档以 grades app 的lms/djangoapps/grades/api.py作为参考示例强调显式 API 能阻止单体中 app 间意外的纠缠并迫使开发者考虑良好的 SOLID 抽象设计。多年实践暴露的问题ADR 0001 开篇即指出即使有了api.py约定实践多年后仍出现四类典型问题难以判断 API 究竟返回什么——返回裸 dict 或 Model 实例时调用方只能靠读源码猜字段难以知道 API 的合法输入是什么——参数缺少类型约束与校验传错只能运行时才发现难以察觉兼容性破坏——修改 API 内部行为时没有显式的“契约”来提示破坏视图所承载的关键功能在 API 中缺失或行为不一致——开发者遇到问题时绕过 API 直接写视图导致两套逻辑漂移。决策一不可变 attrs 数据类 独立data.py所有 API 数据结构必须声明为独立data.py文件中的不可变 attrs 类所有属性必须带类型注解。在 learning_sequences 中这一约定的落地文件是 data.py。其模块 docstring 给出了四条硬性规则尽可能使用frozenTrue不可变以简化调试——对象一旦创建不可修改杜绝“某个字段被谁改了”的排查难题data.py不得 import 本 app 的任何其他部分依赖被严格限制为 Python 标准库、attr、opaque keys 及少量 Django 原语——它是整个 app 的依赖底部而不是被依赖方数据类保持“愚笨”——业务逻辑应放在操作这些数据的api包模块中不要在数据类上挂复杂对象作为属性否则难以 mock、难以对行为做保证数据类可以做校验但只限于完全自包含的校验——禁止数据库调用、网络请求、调用其他 app 的 API 函数也不能触发昂贵的计算。从data.py看数据结构的实际形态以核心的 CourseOutlineData 为例attr.s(frozenTrue) class CourseOutlineData: Course Outline information without any user-specific data. MAX_SEQUENCE_COUNT 1000 class DoesNotExist(ObjectDoesNotExist): pass course_key attr.ib(typeCourseKey) course_key.validator def not_deprecated(self, _attribute, value): Only non-deprecated course keys (e.g. course-v1:) are supported. if value.deprecated: raise ValueError(course_key cannot be a slash-separated course key (.deprecatedTrue)) title attr.ib(typestr) published_at attr.ib(typedatetime) published_version attr.ib(typestr) days_early_for_beta attr.ib(typeOptional[int]) sections attr.ib(typeList[CourseSectionData]) self_paced attr.ib(typebool) # 由 sections 推导而来禁止直接设置 sequences attr.ib(typeDict[UsageKey, CourseLearningSequenceData], initFalse) course_visibility: CourseVisibility attr.ib(validatorattr.validators.in_(CourseVisibility)) entrance_exam_id attr.ib(typeOptional[str]) def __attrs_post_init__(self): ...可以看到类型注解覆盖所有字段frozenTrue保证不可变性attr.validators.in_限制枚举取值__attrs_post_init__在初始化后自动校验“同一 Sequence 不得出现在多个 Section”并推导sequences索引超过MAX_SEQUENCE_COUNT 1000会抛ValueError。这些校验都不需要访问数据库完全符合“自包含校验”的约束。数据类同样实现了“哑容器”哲学UserCourseOutlineData 继承CourseOutlineData仅追加base_outline、user、at_time、accessible_sequences四个字段其 docstring 明确说明“它不知道如何推导裁剪后的状态如何实例化它由learning_sequences.api包中的函数负责”。ObjectDoesNotExist基类则“模仿 Django 模型约定”让数据类可以内嵌DoesNotExist子类如CourseOutlineData.DoesNotExist被视图捕获后转换为 HTTP 404。决策二与决策三类型注解 顶层api包唯一出口所有公共 API 函数的参数与返回值必须使用类型注解所有公共 API 函数必须在顶层api包中导出其他应用只允许从该顶层包 import。类型注解让契约在签名上显式化这一条直接回应“难以知道合法输入”的问题。以 outlines.py 中的公共函数为例def key_supports_outlines(opaque_key: OpaqueKey) - bool: ... def get_course_keys_with_outlines() - QuerySet: ... def get_course_outline(course_key: CourseKey) - CourseOutlineData: ...get_course_outline的返回类型直接指向data.py中的数据类调用方无需猜测返回结构参数CourseKey也限定了合法输入范围。key_supports_outlines的 docstring 详细解释了边界允许除 v1 LibrariesLibraryLocator是CourseKey的子类但不应支持之外的所有非废弃 CourseKey——即正常 SplitMongo 课程与 CCX 课程可用而 libraries、pathways 和旧式 Mongo 课程不可用。顶层api包唯一的 import 通道api/__init__.py是唯一允许外部 import 的出口# pylint: disablemissing-module-docstring from .outlines import ( get_content_errors, get_course_keys_with_outlines, get_course_outline, get_user_course_outline, get_user_course_outline_details, key_supports_outlines, replace_course_outline, )而outlines.py模块 docstring 明确写道“不要直接 import 本模块请使用openedx.core.djangoapps.content.learning_sequences.api——那个__init__.py从这导入是更稳定的 import 位置。”仓库中的实际调用严格遵循了这一约定views.py 使用from .api import get_user_course_outline_details和from .data import CourseOutlineData——本 app 的视图也只从api顶层包与data.py获取数据绝不直接触碰 modelsadmin.py 使用from .api import get_content_errors, get_course_outline。这种“单一出口”设计意味着只要顶层api/__init__.py的导出签名不变api包内部的模块重组、models 结构调整都不会破坏外部调用方。决策四与决策五视图与任务自我约束 内联 Serializer视图、任务等所有不在api包内的部分必须遵守与外部 app 相同的规则——即只从api导入不直接导入 modelsREST API 的 Serializer 定义为视图的内嵌类显式反对跨用例复用。视图与任务遵守“外部规则”这条规则把“单一出口”约束扩展到 app 内部视图和任务不能因为“就在同一个 app 里”就绕过 API 直接操作 models。learning_sequences 的 views.py 开篇 docstring 就是宣言“本 app 的 views.py 刻意保持单薄只负责在用户输入/输出与api包中的业务逻辑之间做翻译。”Serializer 内嵌视图防止“修改涟漪”CourseOutlineView 中内嵌了UserCourseOutlineDataSerializer其 docstring 解释了动机该 Serializer 刻意声明在CourseOutlineView内部以阻止复用/魔法。我们的目标是让序列化方式极其显眼避免共享 Serializer 在另一个模块中被修改以修复某个用例时意外破坏另外两个用例。这回应了“视图行为与 API 不一致”的问题——序列化逻辑与具体视图绑定REST 输出结构变更时直接修改对应视图即可不影响任何其他调用方。此外该 Serializer 还有一个值得注意的设计在序列化层把UsageKey翻译成字符串id。UsageKey是 edx-platform 内部的临界数据结构进程内 API 使用它但 REST 客户端只应看到id: block-v1:...形式的字符串。决策六不 mock 内部的 API 级测试尽可能编写不 mock 内部实现、不用模型操作预置数据库的 API 级测试使 API 测试只有在 API 真正变化时才失败可以 mock 对其他服务的调用如 grades。这条规则的目的是让测试成为 API 兼容性的“哨兵”如果某次改动让 API 测试失败那一定是 API 本身发生了真实变化而不是测试耦合了内部实现细节。可以 mock 的是跨 app 的外部服务调用如 grades被禁止的是 mock 本 app 的内部实现、绕开 API 直接操作模型来构造数据。落地方案OutlineProcessor 扩展机制API 的“扩展点”是 api/processors/ 目录下的 OutlineProcessor 体系它体现了“把业务规则挂在数据流上”的约定。基类 base.py 定义了四个可扩展方法__init__(self, course_key, user, at_time)只做初始化不做真实工作数据库访问、昂贵计算都不允许load_data(self, full_course_outline)加载课程与用户所需数据明确禁止使用 modulestore 或 block structures因为其前置性能开销正是本 app 存在的原因即便在含数百个 learning sequence 的课程上该方法也应在几十毫秒内完成inaccessible_sequences(...)返回不可访问的 Sequence UsageKey 集合不会为 staff 用户运行usage_keys_to_remove(...)返回需要整体移除的 UsageKey 集合不会为 staff 用户运行。目前仓库已内置 10 个处理器覆盖典型规则enrollment.py选课、schedule.py日程、milestones.py里程碑、content_gating.py内容门控、special_exams.py特殊考试、visibility.py可见性、cohort_partition_groups.py、enrollment_track_partition_groups.py、team_partition_groups.py各类用户分组等。处理器在请求期间同步按序执行__init__→load_data→inaccessible_sequences/usage_keys_to_remove且不保证与任何其他处理器的相对顺序接口目前尚不可插拔但设计上已为未来插拔化做好准备见 README.rst。在代码库中观察 ADR 的效果可验证的调用链从 views.py 的CourseOutlineView.get可以看到完整调用链validate_course_key(course_key_str)校验课程 key非法 key 返回 HTTP 400 而非 404_determine_user(request, course_key)处理目标用户支持 session masquerade 与user查询参数staff 可模拟任意用户或匿名用户get_user_course_outline_details(course_key, request.user, at_time)——只从api顶层包导入捕获CourseOutlineData.DoesNotExist匿名用户得到NotAuthenticated避免爬虫制造大量 404 噪音登录用户得到NotFound通过内嵌UserCourseOutlineDataSerializer序列化返回。约定失效时的“防火墙”CourseOutlineData.remove()data.py演示了不可变数据结构如何安全支持“派生”它使用attr.evolve基于现有 outline 生成删除了指定 UsageKey 的新实例而非原地修改——这正是frozenTrue带来的安全演进模式。性能与日志约定outlines.py 使用function_trace装饰器跟踪 API 调用频率与所在事务get_course_outline中通过set_custom_attribute单独记录course_id确保从管理命令等场景调用时也能获得有效的监控信息。总结与推广ADR 0001 的 7 条扩展约定本质上是把“API 即契约”从理念变成可执行规则约定解决的问题关键落点不可变 attrs 数据类 data.py返回不透明、难以调试data.py数据类自包含校验合法输入不明确各类字段 validator 与__attrs_post_init__函数类型注解输入/输出不透明outlines.py 公共函数顶层api包唯一出口内部实现泄漏api/init.py视图/任务遵守外部规则视图行为与 API 漂移views.py 的 docstring 与导入方式内嵌 Serializer共享序列化被意外破坏CourseOutlineView.UserCourseOutlineDataSerializer不 mock 内部的 API 测试兼容性破坏难以察觉api/tests/目录下的测试策略作为“试验场”该 ADR 明确说明若这些扩展约定在实践中被证明有效将推广到 edx-platform 顶层定义的全部 Inter-app API 中。对希望在自己的 Django app 中复制这套模式的开发者README.rst 给出了三条实践指引想给公共 API 增加数据扩展api/data.py中的数据类业务逻辑放进api包模块并在顶层api/__init__.py重新导出想增加影响 outline 展示的新规则创建或修改api/processors/下的 OutlineProcessor先读 base.py 的 docstring需要 modulestore / block structures 的数据不要在 API 调用中同步拉取而应在课程发布时把数据推入更小的模型中——这是保持 API 性能目标的底线。【免费下载链接】openedx-platformThe Open edX LMS Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。