[想法] 重构代码的一些想法
重构代码的一些想法
最近需要新写一个业务模块,这个业务模块和两年前自己写的一个业务功能高度类似,就想着能不能拿过来改改就行。这个业务模块使用 golang 实现的,是我写的第一个 golang 代码。
以上为背景,但是代码拿过来后发现通用性太差,虽然业务有相似的地方,但是小改达不到自己的期望,于是做了一下几个部分的重构。
函数尽量选择非对象化实现
很多功能函数的实现都强行面向对象,多余的维护了一些不必要的状态信息;
直接拿 golang 举个例子,一个调用 docker sdk 来启动容器的接口 Start 调用如下
err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
cli 的构造可能会发生错误
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
获取容器 id 也可能发生错误
resp, err := cli.ContainerInspect(context.Background(), name)
也就是说,如果需要实现一个 Start,需要额外处理两个错误。
假设提供的对象化实现,表面上看有一个好处,可以复用基于对象的一些其他变量,例如 cli 指针,这样我不仅 Start 可以用,Stop 也能够用这个变量。
带来的问题也是显而易见的,对于构造函数而言,cli指针不管是从外部传入还是直接在内部进行构造都有失败的风险。
type DockerManager struct {
cli *client.Client
ctx context.Context
name string
id string
}
// New...
func (d *DockerManager) Start() error {
return d.cli.ContainerStart(d.ctx, d.id, types.ContainerStartOptions{})
}
对于一个容器的启动,调用是这个样子的,d.Start(),d 有可能不是一个正常的对象,直接调用可能有panic的风险,又需要一些其他的措施来预防。
也带来一个传染性的问题,对于外部调用者来说,构造一个 DockerManager 对象有可能是直接在函数的执行过程中完成的,也有可能是外部构造函数中,这样的影响面又变大了,一层一层往外传染,失败该怎么办。
当然对于 golang 而言,一般构造函数都不会选择失败的做法,所以我们这里换一种实现,构造函数只构造无状态的 name
type DockerManager struct {
name string
}
// func NewDockerManager(name string)
func (d *DockerManager) Start() error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
ctx := context.Background()
resp, err := cli.ContainerInspect(ctx, d.name)
if err != nil {
return err
}
return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
}
这样构造函数永远不失败,对于对象而言,不会影响到外部的调用,但是这样做基本就退化成非面向对象的实现了,
去掉对象的语言,name 从参数传进来,在有些场景之下,构造函数再调用,不如直接这样调用来的方便一些,错误于外部而言只需要处理一次。
func Start(name string) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
ctx := context.Background()
resp, err := cli.ContainerInspect(ctx, name)
if err != nil {
return err
}
return cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
}