Hello, world!
让我们从一个什么正经事都不做的 Action 开始——它只打印一行,然后返回一个空的占位结果。它的价值不在于此:它展示的是,在 AOA 里要让任何代码跑起来,最少需要声明哪些东西。
$ uv run python examples/step_01_Action_and_pipeline/01_hello_world.py对于一句简单的问候来说,这行数不算少——但没有一行是走过场:每一行都在声明一样东西,少了它,AOA 中的 Action 就不算完整。一切始于一个Domain。@meta 是 Action 的身份护照;@check_roles(GuestRole) 把访问权限明确声明出来,而不是默认开放。@summary_aspect 则是唯一的出口——这三者只要少一个,机器就会在第一次调用之前拒绝运行这个 Action。
Params、Result 与 box
占位代码适合用来入门,但真正的 Action 要处理数据。我们给它加上一个输入和一个输出——顺便把 print 换成 AOA 中 Action 与外部世界对话时真正会用的工具。
$ uv run python examples/step_01_Action_and_pipeline/02_params_result_and_box.pybox 比 print 有意思得多——它是绑定到当前步骤的结构化日志器,携带 channel、level、domain、action 与 aspect 名称等信息,因此天然适合过滤、路由与导出。事件最终流向哪里并不由 Action 决定——那是机器的事,由外部接入。
多个 aspect
一个步骤往往是不够的。AOA 不允许中间数据藏身于局部变量或对象字段中——它以显式的 state 流经整条流水线,而 Action 本身在两次调用之间始终保持为空。
$ uv run python examples/step_01_Action_and_pipeline/03_multiple_aspects.py返回的字典会整体替换 state——而不是在其基础上扩展。@result_string("cleaned", required=True) 是一个checker:对 aspect 输出的可验证契约。一旦违反承诺,机器会当场终止该 aspect,绝不让被污染的 state 继续向下传递。
继承
Action 的继承方式和普通类没什么两样——但有一个刻意为之的例外:aspect 不会被继承进流水线。机器只根据类自身声明的内容来构建流水线。
$ uv run python examples/step_01_Action_and_pipeline/04_inheritance.pyChildOrderAction 能正常运行,但它的流水线里只有 child_summary——父类的 validate_aspect 永远不会执行。ExtendedOrderAction 的做法才对:它重新声明该 aspect,并调用 super() 在父类逻辑的基础上继续构建。
实验
这一章积累了不少规则——但它们有一个共同点:几乎所有规则都是在类声明时、也就是模块导入阶段被检查的,而不是在 Action 调用时。这里的代码就是可执行的规范;违反契约会在第一次运行之前暴露出来,而不是在生产环境的某个深夜。动手试试——从第一个例子中选一种方式破坏 SayHelloAction,然后点击运行。
$ uv run python examples/step_01_Action_and_pipeline/01_hello_world.py小结
模型的核心你已经掌握了:一次操作就是一个 Action,拥有带类型的边界(Params 与 Result)、一条线性的 aspect 流水线,以及大多在初始化阶段就被检查的契约。行为体现在代码结构本身,而不是躺在旁边的文档里。
复习题
- 哪一条不变量保证了 Action “单一类可读”的特性?它又是在什么时刻被检查的?
- 为什么缺少
@check_roles会被视为错误,而不是默默地“对所有人开放”? - AOA 的流水线是线性的——没有分支,也没有旁路出口。这个约束换来了什么?又付出了什么代价?
- 为什么 aspect 不会自动被继承进流水线?对比一下 OOP 中普通的方法继承。
- “代码即可执行的规范”是什么意思?为什么大多数契约是在初始化阶段被检查,而不是在运行时?
