曹耘豪的博客

Don't Repeat Yourself

  1. 初见端倪
  2. 方法的DRY
  3. 变量的DRY
  4. 类的DRY
  5. 功能的DRY——组件化、微服务、DDD、框架化
  6. 服务的DRY——平台化
  7. 程序的DRY
  8. 日常运维DRY——开发自动化运维工具
  9. DRY的一些问题
  10. 生活上的DRY
  11. 总结

初见端倪

DRY,第一次看到这个词,是在《程序员修炼之道》里,一本在大学时就听说的书,但一直没有读,直到工作两年再次拾起,仿佛遇到故友。

方法的DRY

事实上,从大一开始的时候我就一直执行DRY原则,那个时候学习面向过程(C语言)。因为我很懒,所以有的代码不想写两边,便抽成一个公共函数。

变量的DRY

直到毕业,我还会看到有的同学会写出下面的代码。

1
2
3
if (isValid(getFromDB())) {
process(getFromDB())
}

显然,查询了2次数据库,这是没有必要的。更好的写法应该是:

1
2
3
4
Object dbValue = getFromDB()
if (isValid(dbValue)) {
process(dbValue)
}

以前我也认为这很简单,事实上还是有很多人做不到。

复用变量可以说是复用的最小粒度。

有时候,我们也可以使用懒加载工具来更好的控制非必要执行:

1
2
3
4
5
6
7
8
9
10
Lazy<Object> dbValueLazy = Lazy.of(() -> getFromDB())
if (otherCondition && isValid(dbValue.get())) {
process(dbValue.get())
}

// 或者下面写法,jdk内部很多地方用到该写法
Object dbValue;
if (otherCondition && isValid(dbValue = getFromDB())) {
process(dbValue)
}

实际工作中,我还没见过有人这么写。

类的DRY

模板方法设计模式非常直观执行DRY原则,比如我们常用的继承。

功能的DRY——组件化、微服务、DDD、框架化

一个功能可以是一个方法,比如一个工具方法,也可以是一个类或一组类。在Spring里,可以是一套Controller、Service、Repository的组合,提供一套能力,看起来更像是一个领域(domain)。

原则上,能力越抽象,可复用性就会越强,但过度抽象也会导致代码复杂度的提升和可读性的降低。

我认为,理想的项目应该存在大量的开箱即用的工具,这些工具可以降低业务代码量。

比如下面一段非常常见的代码:判断不合法然后抛出异常。

1
2
3
if (!isValid(value)) {
throws new BusinessException("invalid value: " + value);
}

如果我们封装了工具方法,我们在业务代码里只需1行即可。函数实现参考了guava。

1
2
3
4
5
6
7
8
checkArgument(isValid(value), "invalid value: %s", value)

// checkArgument实现
void checkArgument(boolean b, String messageTemplate, Object ...args) {
if (!b) {
throws new BusinessException(format(messageTemplate, args))
}
}

这种做法还统一了抛出异常的类型,更重要的是隐藏了抛出的具体异常实现,仅表达了我们的目的:校验参数,至于以后抛出的是BusinessException还是IllegalArgumentException,仅需改一个地方即可。

对于上面抛出的异常,Spring可以使用统一的全局异常处理器和ResponseBodyAdvice,但如果没有相关工具,我们每个Controller的代码可能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping
public Response api(@RequestBody Object value) {
try {
if (!isValid(value)) {
throws new BusinessException("invalid value: " + value);
}
// other code
return Response.success();
} catch (BusinessException e) {
log.warn("warn log");
return Response.fail(-1, e.getMessage);
} catch (Excetion e) {
log.warn("error log");
return Response.fail(-2, "server error");
}
}

如果有,那么我们的业务代码更简单:

1
2
3
4
5
6
@PostMapping
public Object api(@RequestBody Object value) {
checkArgument(isValid(value), "invalid value: %s", value);
// other code
return "success";
}

然后在全局异常处理器中分别处理BusinessException异常和其他异常。

服务的DRY——平台化

在微服务架构里,我们希望我们的服务提供单一能力,但不是一蹴而就的。

当一个业务成熟后,我们也会希望把业务能力通用化、标准化、平台化。

比如之前做人群标签,项目初期仅考虑人群,项目成熟之后,我们后来会想做一个通用标签平台,不仅仅对于用户,我们提前约定好标签数据的格式。然后将之前的人群标签,按照新的平台标准格式接入到标签平台。

比如对于支付能力,在我们作完充值、购买后,我们希望支付能力是通用的,所以做了支付中台,隐藏每种支付渠道的实现,对外提供标准的下单、查询、回调等能力,然后将充值、购买都接入支付中台。

再比如各种云服务商、各种XaaS平台。

程序的DRY

java开始的口号便是“Write once, run anywhere”,也是一种DRY。

日常运维DRY——开发自动化运维工具

现在,排查问题总免不了查询MySQL,使用查询工具Navicat、DataGrip等,但我们总是需要写很多重复的SQL,并且DB的结果并不直观,除非你记住表里的1、2、3分别代表什么。

为此,我在业余时间开发了一个查询工具网站,预设了一些我常用的表,以及每个表的筛选字段。也对一些表增加了跳转,比如一个表有user_id字段,增加一个链接,链接到对应的用户详情表。这样,以前需要写模板SQL的,现在只需要点点点即可。也可以在一个页面同时展示所有用户相关的表,一览无余。后来该工具在部门内做了推广,被评价“一个朴实而又不失优雅,简单而又不失自然的小工具”。

DRY的一些问题

生活上的DRY

生活上,有时我们需要重复,比如学习一项技能、坚持读书健身,这些重复是有意义的。

但我们也能看到一些DRY的场景,比如一些大而全的APP、聚合网站等,尤其是聚合社交网络

比如我们要在社交网络上发表动态,有微博、X、知乎等很多平台,如果我们每个都发一边也是很麻烦,于是就有类似的聚合平台,关联上每个平台的账号,发一次即可同步到其他平台。

又比如现在的智能家居,开门自动化等操作,都是为了减少生活中的重复。

重复的生活也会对人的心理健康产生负面影响。

所以有一种说法便是,懒是社会进步的推动力,人会想办法让自己可以懒,因为我们想懒,所以我们必须创造各种工具,提高效率。

总结

无论我们写代码、还是系统设计、日常生活,DRY无处不在。时刻提醒自己,尽量不要重复你自己!

   /