Junedayday Blog

六月天天的个人博客

0%

【每周小结】2023-Week4

新的一年已经到来,祝各位读者2023年身体健康、家庭美满。

回到本篇的主题,我继续来聊聊本周的一些心得。

Go技巧 - 接口实现下的三个代码复用技巧

在面向对象开发的场景下,我们经常会写高度重复的Go代码。为了帮助大家形成一定的方法论,这里以一个具体场景为例,分享我的三个技巧。

示例:一个接口 + 三个实现

我们以上图为例,看看示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 接口定义 - 订单
// 方法定义:创建订单Create与关闭订单Close
type Order interface {
Create() error
Close() error
}

// 三种具体的订单类型 Order1 Order2 Order3
// 为了实现接口Order,这三个结构都需要实现 Create与Close 方法

// Order1部分
type Order1 struct {}

func (o *Order1) Create() error {
}

func (o *Order1) Close() error {
}

// Order2部分
type Order2 struct {}

func (o *Order2) Create() error {
}

func (o *Order2) Close() error {
}

// Order3部分
type Order3 struct {}

func (o *Order3) Create() error {
}

func (o *Order3) Close() error {
}

在实际场景中,Order1/Order2/Order3的逻辑、数据高度相似,出现大量的重复性代码。如何提升这部分代码的开发效率呢?下面给出三个途径:

方法1:快刀斩乱麻 - 函数复用

最直接的方法就是抛开面向对象的一堆概念,单纯地用函数复用来解决问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func createOrder(ctx context.Context, data interface{}) error {
}

func closeOrder(ctx context.Context, data interface{}) error {
}

// 以Order1为例,Order2/Order3类似
type Order1 struct {}

func (o *Order1) Create() error {
// 调用 createOrder
}

func (o *Order1) Close() error {
// 调用 closeOrder
}

这种编程思维是面向过程的,虽然不够抽象,但它确实是 最便捷的代码复用方式。而且,在很多情况下,我们不会对这块代码有大更新,函数复用是一个 高性价比 的选择。

但我们的追求不仅限于此:如果这块代码涉及业务核心,高频迭代,会出现什么样的现象呢?举3个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 示例1 - 入参不断增加
// 某些订单的需要一些额外的数据,那么就必须增加入参(并且这个参数很难通用!)
func createOrder(ctx context.Context, data interface{}, other, more interface{}) error {
}

// 示例2 - 大量的if-else
// 一个函数适配多种逻辑,只能增加判断逻辑
func createOrder(ctx context.Context, data interface{}, otherData interface{}) error {
if orderType == 1 {
} else if price > 1000 {
} else {
}
}

// 示例3 - 创建多个函数
func createOrder2() error {}
func createOrder3() error {}

以上这些代码是不整洁的,相信大部分人不愿在自己开发过程中看到。这种过程性代码复用的思路,见效虽快,但在复杂场景下弊端愈发明显。下面,我们引入第二个方法:

方法2:对象抽象的妙用 - 嵌套+Overwrite

Go并不是一门完全面向对象的语言,但对于复杂场景,会用嵌套来支持一定的代码复用。代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 一个基础对象,实现了接口 Order
type CommonOrder struct {}

func (o *CommonOrder) Create() error {
}

func (o *CommonOrder) Close() error {
}

// Order1 利用嵌套,直接实现了Create和Close两个方法
type Order1 struct {
*CommonOrder
}

// Order2 也利用了嵌套,Close方法会复用,但Create方法会被覆盖
type Order2 struct {
*CommonOrder
}

// Overwrite
func (o *Order2) Create() error {
}

// Order3 两个方法都会被覆盖
type Order3 struct {
*CommonOrder
}

// Overwrite
func (o *Order3) Create() error {
}

// Overwrite
func (o *Order3) Close() error {
}

嵌套+Overwrite的组合能力,支撑了Go语言面向对象的很多特性。与之前的函数复用对比,这种方法的可读性会更棒(这也非常依赖开发者面向对象的抽象能力)。

到这一阶段,维护绝大多数的项目已经足够。但如果你是一个苛求细节的人,在继续开发的过程中会发现一个问题:即便代码的逻辑一致,我们却常常因为数据结构不同,而编写出高度重复性的代码。那么,我们再看第三个方法:

方法3:剥离数据结构的差异 - 泛型

我们先看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type OrderInfo1 struct{}

func (o *Order1) Create() error {
// 数据结构 OrderInfo1 保存的是 Order1 订单信息
var order *OrderInfo1
// 插入msyql
err := mysql.Insert(order)
if err != nil {
return err
}
// 序列化后打印
b,err := json.Marshal(order)
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}

type OrderInfo2 struct{}

func (o *Order2) Create() error {
// 数据结构 OrderInfo2 保存的是 Order2 订单信息
var order *OrderInfo2
// 后面操作同Order1
}

type OrderInfo3 struct{}

func (o *Order3) Create() error {
// 数据结构 OrderInfo3 保存的是 Order3 订单信息
var order *OrderInfo3
// 后面操作同Order1
}

这部分代码很难通过上述两个方法解决。而如果利用泛型,会变得非常巧妙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 将公共逻辑抽象到这个泛型函数中
func Create[OrderInfo interface{}](order OrderInfo) error {
err := mysql.Insert(order)
if err != nil {
return err
}
b,err := json.Marshal(order)
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}

// 三个Order的Create方法就非常清晰了
func (o *Order1) Create() error {
var order *OrderInfo1
return Create[OrderInfo1](order)
}

func (o *Order2) Create() error {
var order *OrderInfo2
return Create[OrderInfo2](order)
}

func (o *Order3) Create() error {
var order *OrderInfo3
return Create[OrderInfo3](order)
}

泛型特性的引入,往往出现在数据处理层,即和基础库、工具库相关的地方,而在业务层很少出现。我们可以从如下两点进行分析:

  • 业务层主要的特点在与 逻辑差异大,对数据结构也有各种校验等,不适用泛型;
  • 数据处理层则往往逻辑一致,仅仅只有数据结构的差异,泛型非常适配。

小结

函数复用、嵌套+Overwrite、泛型,是三种非常有效的代码复用技巧。希望大家能够循序渐进,在工程中找到属于自己的最佳实践。

编程思考 - 开发前的三个文档

在开发一个项目前,有三个文档是必备的,我们称为 - BRDPRD技术方案,它们在项目流程中依次编写。

  1. BRD(商业需求文档):这个文档有一个关键词 - 商业价值,不仅要了解用户痛点,更要结合市场,发掘价值
  2. PRD(产品需求文档):与产品经理角色相关,设计功能交互,体现出两种重要的思维:产品思维与用户思维
  3. 技术方案:开发者最熟悉的文档,最主要的是设计,但更重要的是评估能力,如排期、风险

编写技术方案不难,普通开发者工作两三年就能有一个很棒的呈现;而PRD则须要 视野转换,从用户与产品的角度来思考功能的开发;BRD则最为复杂,往往要多年行业经验积累以及深刻的用户洞察。

大家可以在日常开发中多主动地接触优秀的PRD、BRD,不仅能拓宽视野,更能提升个人认知。

工作生活 - 学会聚焦,才能做好取舍

不同人、在不同的阶段,对工作和生活的平衡点都有不同的理解。所以,我认为没有必要去过多地从他人经验里去探求 最佳平衡点,也没有必要把工作和生活当作对立面,而是在日常反复问自己:我究竟想要什么?

有取,往往就需要舍弃,这时就会犹豫代价是否过大。我总是过多地担忧所失去的,就扭曲了原问题:不再关注自己最想要的,而转过头去关注可能失去的,情绪上出现焦虑,甚至恐慌。简而言之,就是要认清自己,学会聚焦。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码