该用facade当调用方需写5行以上初始化、按序调用3个以上包且手动处理中间状态时;应传接口而非具体类型,提供3~5个核心方法并强制实现shutdown()资源清理。

什么时候该用 Facade 而不是直接暴露子系统?
当你发现调用方要写 5 行以上初始化代码、按固定顺序调用 3 个以上包、还要手动处理中间状态时,Facade 就不是“设计模式炫技”,而是防错刚需。比如启动一个带日志、配置加载、数据库连接、HTTP 服务的模块,如果每次都要重复写 log.New() → config.Load() → db.Open() → http.ListenAndServe(),那封装一层 NewApp() 就是减少出错面。
- 典型信号:测试里出现大量
defer db.Close()、if err != nil { ... }套娃 - 别硬套:单个函数、无依赖链、不跨包的逻辑,加
Facade只会多一层跳转 - 关键判断点:是否有多处调用方重复写相同组合逻辑?有,就值得抽
NewApp() 该接收哪些参数?为什么不能全塞进一个 Config 结构体?
参数设计本质是控制权划分。把所有东西塞进 Config 看似干净,但会让 Facade 失去灵活性——比如测试时想 mock DB 实例,却发现它被锁死在 Config.DBURL 字符串里,只能改连接字符串,没法换实现。
- 必须传接口:如
logger Logger、db DBer,而非具体类型或配置字符串 - 可选传配置结构体:仅限真正只读、无行为的字段,如
HTTPAddr string、Timeout time.Duration - 禁止传指针或全局变量:如
*sql.DB或os.Stdout,这会让Facade无法控制生命周期 - 示例合理签名:
func NewApp(logger Logger, db DBer, cfg AppConfig) *App
如何避免 Facade 变成“上帝对象”?
常见陷阱是把所有功能都挂到 Facade 实例上,结果 app.DoX()、app.HandleY()、app.CleanupZ() 堆出 20 个方法,违背“简化”初衷。真正的 Facade 应只提供顶层动作,内部细节下沉。
- 只暴露 3~5 个核心方法:如
Start()、Shutdown()、HealthCheck(),其余能力通过返回的子对象调用 - 不代理子系统方法:不要写
func (a *App) Query(sql string) {... return a.db.Query(sql) },这等于没封装 - 允许获取子系统实例:提供
App.DB() DBer、App.Logger() Logger,让调用方按需深入,而不是拦住去路 - 错误信息要透出源头:比如
app.Start() error返回的错误应包含"failed to open db: dial timeout",而不是笼统的"app start failed"
Go 的接口机制让 Facade 实现比其他语言更轻量,但也更容易漏掉什么?
Go 没有抽象类或强制继承,所以 Facade 很容易变成一堆零散函数,缺少统一入口和生命周期管理。最常被忽略的是资源清理契约——Java/C# 有 AutoCloseable 或 IDisposable 强约束,Go 全靠约定。
立即学习“go语言免费学习笔记(深入)”;
- 必须提供明确的关闭方法:如
Shutdown() error,且文档写清它是否阻塞、超时多久、能否多次调用 - 初始化失败时,已分配的资源要自动释放:比如
db.Open()成功但http.Listen()失败,得立刻db.Close() - 别依赖
runtime.SetFinalizer:它不保证执行时机,Shutdown()才是唯一可信出口 - 测试时记得验证:调用
Shutdown()后,net.Listener是否真关了,*sql.DB的连接数是否归零










