设计模式踩坑记录
那时候的回答基本是"了解单例、工厂、观察者",背完定义就开始在脑子里搜索项目里用过没有——当然是没有。
后来进了一家所谓的"架构驱动"公司,项目里到处都是设计模式的痕迹:抽象工厂工厂出接口,装饰器装饰着装饰器,代理套着代理。
为什么需要设计模式?
教科书会告诉你:设计模式是解决特定问题的成熟方案,提高代码复用性、可维护性、扩展性。
但实际项目里,设计模式更多时候解决的是沟通成本。团队里10个人,对"怎么解耦"、“怎么扩展"有10种理解。设计模式给出了一个共享词汇表,你一说"这里用策略模式”,大家就知道大概是什么结构,不用再从零开始解释。
还有一个现实情况:代码写出来不是给人看的,是给人改的。需求永远在变,如果每次改需求都要重构整个模块,那开发效率就是问题。设计模式本质上是在预判变化——预判哪些地方可能会变,提前做隔离和扩展点。
但预判本身就是风险。过度设计是设计模式最大的坑,后面详细说。
常见模式在项目里的实际样子
这里只挑几个在项目里真正用得上、也容易用烂的模式说说。每个模式配一个真实场景和一段能跑的代码。
工厂方法:别让外部决定怎么创建对象
场景:项目里有多种支付渠道(支付宝、微信、银联),每个渠道的初始化参数、签名方式、回调处理都不一样。如果每次用的时候直接 new AlipayPayment(config),代码会到处散满创建逻辑,改个渠道配置要改一堆文件。
反模式:在业务代码里直接new对象,散落各处。
// 订单服务里
func CreateOrder(order *Order) error {
payment := &AlipayPayment{
AppID: "alipay-app-123",
PrivateKey: "...",
}
return payment.Process(order)
}
// 退款服务里又重复一遍
func RefundOrder(orderID string) error {
payment := &AlipayPayment{
AppID: "alipay-app-123",
PrivateKey: "...",
}
return payment.Refund(orderID)
}
合理做法:用工厂方法,把创建逻辑集中到一处。
type PaymentFactory interface {
CreatePayment() Payment
}
type AlipayFactory struct {
Config AlipayConfig
}
func (f *AlipayFactory) CreatePayment() Payment {
return &AlipayPayment{
AppID: f.Config.AppID,
PrivateKey: f.Config.PrivateKey,
}
}
type WechatFactory struct {
Config WechatConfig
}
func (f *WechatFactory) CreatePayment() Payment {
return &WechatPayment{
AppID: f.Config.AppID,
Secret: f.Config.Secret,
}
}
// 使用时
factory := getPaymentFactory("alipay") // 配置中心或DI容器里拿
payment := factory.CreatePayment()
payment.Process(order)
踩坑:工厂本身也可能被滥用。如果工厂只有一个实现,那就不用工厂,直接new就行。工厂是为了应对"多种实现"或者"创建过程复杂"的场景,不是为了让代码看起来"架构化"。
单例:全局状态最脆弱
场景:数据库连接池、日志器、配置加载器这些确实只需要一个实例的东西。
教科书写法:懒加载+双重检查锁。
type DatabasePool struct {
connections []*sql.DB
}
var (
instance *DatabasePool
once sync.Once
)
func GetDatabasePool() *DatabasePool {
once.Do(func() {
instance = &DatabasePool{
connections: initializeConnections(),
}
})
return instance
}
现实问题:单例会让代码变得不可测。单元测试时需要mock数据库,但单例直接返回真实对象,没法替换。更糟的是,单例会隐藏依赖关系——一个函数调用 Logger.GetLogger(),你从函数签名看不出它依赖日志系统。
更好的做法:通过依赖注入(DI)框架或者构造函数传入单例,让依赖关系显式化。
// 构造函数注入
type OrderService struct {
db *DatabasePool
logger Logger
}
func NewOrderService(db *DatabasePool, logger Logger) *OrderService {
return &OrderService{
db: db,
logger: logger,
}
}
// 测试时可以传入mock对象
func TestOrderService_Create(t *testing.T) {
mockDB := &MockDatabasePool{}
mockLogger := &MockLogger{}
service := NewOrderService(mockDB, mockLogger)
// 测试代码...
}
观察者模式:解耦但也可能变成事件地狱
场景:订单状态变更后,需要通知多个下游系统:库存系统扣减、物流系统创建运单、用户系统发送通知、数据分析系统记录埋点。如果把这些调用都写在订单服务里,代码会越来越臃肿。
简单实现:事件订阅。
type OrderEvent struct {
OrderID string
Status string
Timestamp time.Time
}
type Observer interface {
OnOrderEvent(event OrderEvent)
}
type OrderEventBus struct {
observers []Observer
}
func (bus *OrderEventBus) Subscribe(observer Observer) {
bus.observers = append(bus.observers, observer)
}
func (bus *OrderEventBus) Publish(event OrderEvent) {
for _, observer := range bus.observers {
go observer.OnOrderEvent(event) // 异步处理,避免阻塞
}
}
// 订单服务发布事件
func (s *OrderService) UpdateStatus(orderID string, status string) {
// 更新订单状态...
event := OrderEvent{
OrderID: orderID,
Status: status,
Timestamp: time.Now(),
}
s.eventBus.Publish(event)
}
踩坑:事件流转会变得难以追踪。一个订单状态变更,可能触发十几个事件,每个事件又触发其他事件,最后都不知道状态是怎么变成这样的。排查问题时,需要画一张完整的事件流转图。
调试建议:
- 记录事件发布日志,包括事件内容、订阅者列表
- 给每个事件加trace ID,全链路可追踪
- 关键业务事件同步处理,非关键业务异步处理
策略模式:消除if-else,但也别过度
场景:用户认证方式有多种:密码、手机验证码、第三方登录(微信、GitHub)。如果用if-else写,代码会像这样:
func Authenticate(user *User, authType string, credential interface{}) error {
if authType == "password" {
return authenticateByPassword(user, credential.(string))
} else if authType == "sms" {
return authenticateBySMS(user, credential.(string))
} else if authType == "wechat" {
return authenticateByWechat(user, credential.(WechatAuth))
} else if authType == "github" {
return authenticateByGithub(user, credential.(GithubAuth))
}
return errors.New("unsupported auth type")
}
每次新增一种认证方式,都要修改这个函数,违反了开闭原则。
策略模式改造:
type AuthStrategy interface {
Authenticate(user *User, credential interface{}) error
}
type PasswordAuthStrategy struct {
// 可能需要的依赖...
}
func (s *PasswordAuthStrategy) Authenticate(user *User, credential interface{}) error {
password, ok := credential.(string)
if !ok {
return errors.New("invalid credential type")
}
// 密码验证逻辑...
return nil
}
type SMSAuthStrategy struct {
smsService SMSService
}
func (s *SMSAuthStrategy) Authenticate(user *User, credential interface{}) error {
code, ok := credential.(string)
if !ok {
return errors.New("invalid credential type")
}
// 短信验证码验证逻辑...
return nil
}
type AuthService struct {
strategies map[string]AuthStrategy
}
func (s *AuthService) RegisterStrategy(authType string, strategy AuthStrategy) {
s.strategies[authType] = strategy
}
func (s *AuthService) Authenticate(user *User, authType string, credential interface{}) error {
strategy, ok := s.strategies[authType]
if !ok {
return fmt.Errorf("unsupported auth type: %s", authType)
}
return strategy.Authenticate(user, credential)
}
踩坑:策略模式用多了会变"策略爆炸"。一个简单的if-else拆成5个类,文件数量激增,新人看代码要跳来跳去。如果策略只有2-3个,而且不太可能扩展,那就直接用if-else,别强行上模式。
实践里踩过的那些坑
坑1:过度设计,为了用模式而用模式
刚接触设计模式那段时间,看什么都想套模式。一个简单的CRUD模块,硬生生拆成了Repository、Factory、Builder、Strategy、Observer,每个类几行代码,调用链七拐八弯。
结果是:
- 新同事看不懂,老同事一段时间后也看不懂
- 改个小功能要动好几个文件
- 调试时要在脑子里画出整个调用图
后来学会了一个简单原则:如果不用模式也能解决问题,那就不用模式。模式是为了解决复杂度,不是为了制造复杂度。
坑2:模式选择错误,越改越复杂
有次做缓存系统,用了装饰器模式来叠加缓存逻辑:先加本地缓存装饰器,再加Redis缓存装饰器,再加预热装饰器。结果运行时发现调用链太长,性能有问题,调试也困难。
后来改成了责任链模式,每个缓存节点独立判断是否命中、是否传递给下一个节点,逻辑清晰多了。
选择模式前先问自己:
- 这个模式能解决什么具体问题?
- 有没有更简单的方案?
- 团队成员都熟悉这个模式吗?
坑3:模式僵化,需求变了不好改
用了模板方法模式实现支付流程:下单→验证→支付→回调→完成。后来业务发展,需要支持"预授权→消费→完成"的流程,模板方法改起来很麻烦,因为骨架已经定死了。
如果预判到流程可能大变,那用策略模式或者组合模式会更灵活。模板方法适合流程稳定、细节变化的场景。
什么时候该用,什么时候不用
这个判断比"怎么用"更重要。
该用模式的场景:
- 重复的代码结构:同一套逻辑在多个地方出现,用模式可以复用
- 明确的变化点:某些地方未来肯定会变,提前做隔离
- 团队有共识:团队都熟悉这个模式,沟通成本低
- 复杂度足够高:简单的逻辑直接写,复杂的逻辑才需要模式来理清关系
不该用模式的场景:
- 只有一次的使用:一个类只有一个地方用,没必要抽象
- 未来不会变:逻辑很稳定,不太可能扩展
- 团队不熟悉:用了反而增加理解成本
- 性能敏感:抽象层会影响性能,且优化难度大
判断标准:
# 在心里问这三个问题
1. 这个模式能解决什么问题?——如果答不上来,就别用
2. 不用这个模式会怎么样?——如果也没什么影响,就别用
3. 六个月后回头看,这个模式还能看懂吗?——如果不确定,就简化
写在最后
设计模式这东西,说到底是一种"经验封装"。前人踩过坑、总结出方案,后人可以避免重复踩坑。但经验不是教条,具体问题具体分析。
代码写得再"优雅",如果没人看得懂、没法维护,那就是失败。设计模式是工具,不是目的。真正的高手,是知道什么时候该用、什么时候该克制的那个人。
参考的资源和实践环境:
- Go语言项目,主要在微服务架构下使用
- 依赖注入框架:Wire(Google出品的编译时DI)
- 项目规模:5-10人团队,代码量约10万行
- 主要模式:工厂、策略、观察者、装饰器、责任链
如果你想深入了解某个模式的细节,建议直接看Go标准库里是怎么用的。标准库的代码质量高、注释详尽,比教科书里的示例代码有参考价值得多。
可用性说明:本文发布于 2021 年 3 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-设计模式踩坑记录(https://blog.thinkmoon.cn/post/108-design-patterns-theory-practice-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。