三个士兵在打完仗回家乡的途中,他们又累又饿。当他们看到不远处的村庄时,情绪高涨,希望村民们会好心给他们一些饭吃。但在到达了村庄后,他们发现所有的门窗都关紧了。多年战乱,村民们尝过食物短缺的苦后,都十分珍惜自己的食物。

饥肠辘辘的士兵们没有放弃,他们煮了一锅水,并小心翼翼地放了三个石头进去。好奇的村民们走过来围观。

“这叫石头汤”,士兵解释。”这些石头就能做汤?“村民们好奇。”当然可以,不过加一些胡萝卜会更鲜甜“。一个村民跑开了,没过一会带着胡萝卜回来了。

汤继续煮了几分钟,又有村民问道:“靠这些东西就能做汤?”。“嗯”,士兵回答,”如果有土豆会更好喝“。于是另一个村民跑回家里。

在接下来的一个小时,士兵们继续列了更多佐料:盐、胡椒、葱、牛肉。每次都有村民跑回家去拿自己的储藏。

最后,一锅热气腾腾的、美味的汤做好了,士兵们把石头去掉,跟村民一起分享了这锅汤,在场所有人都喝到了他们过去几个月内尝到过的最好的汤。

《The Pragmatic Programmer》第四章讲了石头汤的故事,让我想起之前一个工作经历,是关于优化代码的。

Geek式的美梦

当时的后端Golang团队一直用一个自研、简单的 ORM 库,并出于某些原因近期没有更换它的打算,当时访问数据库的代码大概长下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// in server1/dao.go
// 全局函数,没有方便扩展的接口!!!
func GetProviderByCode(o *Orm, code string) (*ProviderEntity, error){
// ...
if len(rows) <= 0 {
return nil, nil
}
return rows[0], nil
}

// in server2/dao.go
// 每个服务自己有一份重复代码!!!
func GetProviderByCode(o *Orm, code string) (*ProviderEntity, error){
// ...
if len(rows) <= 0 {
// 甚至不同服务的函数返回都不一样!!!
return nil, errors2.ErrNoRows
}
return rows[0], nil
}

// 每个不同的数据库访问都要**开发手写**一份无聊的代码!!!
func GetProviderById(o *Orm, id int64) (*ProviderEntity, error){}
func ListProviderByStatus(o *Orm, status int32) (*ProviderEntity, error){}

救世主

我基于“渐进式”原则,结合了Spring JPAgo generate,写了一个减少开发工作量的工具 GPA,你只要写一个 go interface

1
2
3
4
5
6
7
package provider

//go:generate gpagen

type ProviderRepository interface {
FirstByCode(o *Orm, code string) (*ProviderEntity, error)
}

就可以生成固定的实现代码,跟开发手写的代码一样,除了它“言行一致”:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Code generated by gpa-gen. DO NOT EDIT.
package provider

type OrmProviderRepository struct {
}

func (r *OrmProviderRepository) FirstByCode(o *Orm, code string) (*ProviderEntity, error) {
// ...
if len(rows) <= 0 {
return nil, nil
}
return rows[0], nil
}

群众路线

我花了几天假期写完它,它像是一个手工艺者做出来的好东西:小巧、实用。我打算逐步在项目中使用,推销给其他开发,而不走向Leader汇报、请求推广的方式,因为

  • 我觉得好的东西是会吸引人主动使用的,酒香不怕巷子深
  • 我没把握说服Leader相信这个工具有价值
  • 自顶向下推广让我觉得专制
  • 我对该团队的技术优化项目没有多少好印象

我对它很有信心,甚至在想什么情况下会推广不出去时,也只想到了被辞退才会没推广出去。

在回去工作后,我先是自己在项目里使用(eat your own dog food),改善了一些地方后,开始私下推销给其他开发。

现实

遇冷

我线下找团队的开发们,推荐他们使用GPA工具减少工作量,效果非常差,他们都没记住这个工具。

命悬一线

有个“优化数据存储和访问”的技术专项,专项负责人在了解了GPA后觉得目标不一致,打算重新造轮子,统一DB和Cache,名为数据访问层。

我以为GPA将要英年早逝,结果造轮子这事被领导叫停,专项只能专注于给核心链路加Cache和拆分数据库。

柳暗花明

新来的同事对GPA接受度较高,有人实际使用了GPA。

招安

一位资深开发A也对那些“访问数据库的重复代码”很有意见。他写了优化设计文档,找Leader审批,开会给开发者们宣讲,我心灰意冷没有去参加。

会后我了解到,一个GPA使用者在会上提出,这个优化要兼容旧方案,他不想改已经用了GPA的代码,于是Leader安排我做这个“优化”的实现,我需要跟开发A确定实现方案。

经过一番激烈的争论、开会征求意见,我实现了这个优化项目,在我眼中,它包含了:

  • 一个不完整的GPA,它被要求依赖一个Base层
  • 一个Base层,设想是简单和便于扩展
  • 一个Table Struct Generator,只是用于生成表对应的那几行 go struct 定义代码

我不喜欢这个优化,但它至少包含了GPA。

结局

在招安没过几周,部门要求统一基础库,团队需要用XORM换掉自研的ORM,而之前的所有数据库优化都是基于自研ORM做的,这带来两种可能:

  1. 改造工具以兼容XORM,顺势把代码库里的数据库访问都统一成用GPA
  2. 弃用工具,只追求直接的切换XORM方案

我由于个人原因不再参与这些事情,也还没看到结果。

回顾

它符合石头汤的精神吗?

符合的部分:

  • 我提出一个美好愿景,这个工具将减少团队里开发者的工作量
  • 参加建设是自愿的,没有人在推广中被要求使用GAP

不符合的部分:

  • GPA不够吸引人。团队日常需求里会频繁新增数据库表和字段的大多是Admin需求(不受重视);当开发者可以复用前人写过的代码时,他们的工作量,就像使用了GPA一样,也被减少了。
    吸引到第一个好奇的村民花了不少时间,连锁效应也被拖累了
  • 我是有“食物”的。GPA这个工具已经解决了我工作上遇到的问题,我已经自给自足,只有虚荣和代码洁癖驱动我推广GPA

石头汤有用吗?

《The Pragmatic Programmer》里推荐的理由简单来说是:

每个人都在乎自己的资源,如果你能让事情显现出对他们的好处,他们会主动加入。

而我从这次经历得到的经验有:

好处要足够明显

每个人有惰性,没有触碰痛点是很难让大家积极改变的。

需要扁平的环境

石头汤策略更适合扁平的环境,如果一个环境有权重较大的权威,那你需要考虑更多的是权威的利益。

如果有村长收到命令,要求统一食物储藏管理,或者不得向未登记士兵提供食物,那石头汤就做不成了。