1. 外观模式的 Go 示例:Facade 到底在解什么问题
原文用 Go 写的外观模式示例非常典型:客户端不想分别了解 A、B 两个子系统的接口,于是引入 Facade,把NewAModuleAPI()和NewBModuleAPI()封装到NewAPI()里,客户端只需调用api.Test()。这次我用 TaoToken 让 Codex 读这段代码并核对运行结果,TaoToken 官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= ,先到那里创建 Key,再把 Codex 的 Base URL 填成 https://taotoken.net/api(别加 /v1)。
这就是外观模式的核心:为子系统中的一组接口提供一个一致的界面,让复杂的东西看起来简单。放到日常写代码的场景,外观模式解决的是“依赖混乱”的问题。子系统内部怎么改,客户端不需要知道;只要 Facade 的签名不变,两边就可以各自演进。真正运行main()的仍然是你本地终端,Codex 负责读代码、给命令、核对输出。
1.1 外观模式的三个角色
外观模式通常包含三个角色:Facade(门面)、子系统、客户端。Facade 知道哪些子系统负责处理哪些请求,把客户端请求代理给合适的子系统对象。子系统本身不感知 Facade 的存在,它们各自独立工作。客户端只跟 Facade 打交道,不直接创建或调用子系统。
在原文示例里,API接口就是 Facade,apiImpl是 Facade 实现,AModuleAPI和BModuleAPI是两个子系统。apiImpl.Test()内部依次调用TestA()和TestB(),把结果拼成字符串返回。客户端在main()里只写了api := NewAPI()和api.Test(),完全没有直接接触aModuleImpl或bModuleImpl。
1.2 为什么 Facade 能让“复杂的东西看起来简单”
“简单”不是说代码量变少,而是调用方的认知负担变轻。在没有 Facade 时,客户端要知道AModuleAPI和BModuleAPI的创建方式、方法签名、调用顺序,甚至还要处理两者之间的初始化依赖。有了 Facade,客户端只需要知道一件事:调用Test(),得到想要的结果。这种“把复杂藏起来”的能力,正是外观模式在实际项目中被大量使用的原因。
1.3 什么时候该用外观模式
当客户端需要与多个子系统协作,而且这些子系统经常一起出现时,就该考虑引入 Facade。比如一个下单流程里,需要同时操作库存、优惠券、支付三个模块,如果客户端挨个调用,任何一个模块的接口变化都会波及所有调用方。用一个OrderFacade把三个模块的调用统一收口,客户端的代码就只依赖OrderFacade。
当然,外观模式也不是万能的。如果 Facade 变成了所有逻辑的“垃圾桶”,里面塞了太多不属于子系统的业务规则,那它反而会变成一个超级类,增加维护成本。所以使用 Facade 的度是:只做编排,不做业务决策。这一点,也是后面让 Codex 审代码时可以重点检查的地方。
2. 复现原文的 main.go:把 A、B 两个子系统收进 NewFacade()
在配 Codex 之前,先把这段 Go 代码存成main.go。为了不直接照搬原文,我调整了命名,但结构保持一致:
package main import "fmt" type AModule interface { RunA() string } type aModule struct{} func (*aModule) RunA() string { return "A module running" } type BModule interface { RunB() string } type bModule struct{} func (*bModule) RunB() string { return "B module running" } type Facade interface { Test() string } type facade struct { a AModule b BModule } func NewFacade() Facade { return &facade{ a: &aModule{}, b: &bModule{}, } } func (f *facade) Test() string { return fmt.Sprintf("%s\n%s", f.a.RunA(), f.b.RunB()) } func main() { f := NewFacade() fmt.Println(f.Test()) }这段代码和原文想表达的东西完全一致:facade同时持有AModule和BModule,Test()把两个子系统的返回值拼在一起。你在本地跑go run main.go,会得到:
A module running B module running如果输出不是这样,说明某个子系统的实现或者 Facade 的组装出了问题。
2.1 接口和实现拆开的 Go 写法
注意Facade接口只声明了Test() string,而facade结构体内部持有AModule和BModule两个接口。这样设计的好处是:客户端依赖的是接口而非具体类型,将来替换aModule的实现时,只要新的实现仍然满足AModule接口,facade.Test()的调用方式就不用变。这也是“面向接口编程”在 Go 里的落地方式。
原文中NewAModuleAPI()返回的是AModuleAPI接口,而不是aModuleImpl指针,这一点非常重要。如果返回的是具体类型,客户端就可能绕过接口去访问具体实现,Facade 的边界就被破坏了。
2.2 运行结果的顺序由 Test() 的编排逻辑决定
Test()里的fmt.Sprintf("%s\n%s", f.a.RunA(), f.b.RunB())决定了A module running一定在第一行,B module running在第二行。这个顺序由 Facade 内部的编排逻辑决定,客户端不需要关心。Codex 在核对输出时也会检查顺序,因为它反映了 Facade 是否按预期先调用 A 再调用 B。如果你的子系统之间有先后依赖,那么顺序就显得更加关键。
2.3 用 Codex 验证设计模式代码,重点看结构而非只跑结果
让 Codex 读这段代码时,你可以这样提问:“请检查这个 Facade 是否正确地封装了 A 和 B 两个模块,并给出运行命令。”它除了给命令,还会提醒你:facade结构体里直接 new 了aModule{}和bModule{},这在演示环境没问题,但在工程中可能想改用工厂或依赖注入。这种反馈比单纯跑一次程序更有价值,也是我们选择用 Codex 辅助验证的原因。
3. 在 TaoToken 创建 Key,把 Codex 指向 https://taotoken.net/api
打开 TaoToken,注册后创建 API Key。注意官网地址和接口地址的区别:浏览器访问的是官网(带 UTM),Codex 填的 Base URL 是https://taotoken.net/api,结尾不要加/v1。
3.1 创建 Key 的位置
在 TaoToken 控制台的 API Keys 页面点“创建 Key”,复制sk-开头的字符串。这个 Key 只在创建时完整显示一次,最好马上存到环境变量里。如果之后忘了 Key,只能重新创建,旧的 Key 会失效。
3.2 两个地址各司其职
官网地址 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 用于注册、查模型、看用量。接口地址https://taotoken.net/api是代码里填的,两者不要混用。把官网地址填到 Codex 的base_url,会得到一推 HTML 而不是 JSON 响应;把接口地址贴到浏览器,也看不到正常的操作页面。
4. 配置 Codex:config.toml 里加 model_provider
编辑~/.codex/config.toml,写入:
model = "<从TaoToken模型广场复制的模型ID>" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"模型 ID 以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 模型广场为准。不同时期模型列表会变化,不要照搬别人文章里写死的模型名。然后导出环境变量:
export TAOTOKEN_API_KEY=YOUR_API_KEY4.1 环境变量持久化
在~/.zshrc里加一行export TAOTOKEN_API_KEY=YOUR_API_KEY,然后执行source ~/.zshrc,避免每次开终端都重复设置。
4.2 首次跑通的最小提问
运行codex "请读取 main.go 并解释外观模式,给出 go run 命令"。Codex 会通过 TaoToken 读取 main.go,返回结构分析和运行命令。注意它不会替你执行程序,你要在本地终端自己跑go run main.go。
5. 本地执行 main.go,把输出贴回 Codex 核对
在终端运行go run main.go,得到:
A module running B module running把这两行复制回 Codex 对话,确认“输出是否符合预期”。Codex 会逐行核对,并说明两个子系统的调用都正常。
5.1 贴输出时不要带多余内容
只粘贴原始两行输出,不要带终端提示符和当前目录。Codex 对多余字符很敏感,一旦看到不在预期内的内容,可能会怀疑你的运行环境有问题。
5.2 在 TaoToken 控制台看这次调用
用同一把 Key 发起对话后,登录 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= ,在用量页面能看到这次请求的记录,包括模型、时间和 token 消耗。这一步确认了 Key 是通的,Codex 的模型接入也配置正确。
6. 401、404 和模型 ID:三个容易卡住的点
6.1 401:Key 没传对
先执行echo $TAOTOKEN_API_KEY,看输出是否为空。空就重新 export,再检查 Key 是否复制完整。有时候复制会把末尾换行带进去,可以用wc -c对比字符数。
6.2 404:Base URL 填多了 /v1
正确地址是https://taotoken.net/api,不是https://taotoken.net/api/v1。检查 config.toml 的base_url,同时确认没有把带 UTM 的官网链接当成接口地址。
6.3 模型 ID 不存在
返回 400 或“model not found”时,去模型广场复制一个当前有效的 ID。模型列表会变化,以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 当时显示为准。
跑通之后,可以顺手到模型对话里用同一把 Key 再发一条消息,或者在 Coding Plan 看套餐是否够用;Key 管理在控制台 API Keys。之后接 Claude Code 时,TaoToken 的 Claude Code 接入文档 也可以参考,Base URL 的配置思路是一样的。