前几天读了《代码整洁之道》,写点读后感。

读完差不多半个月才想起来写文章,当时也没有做笔记,只能凭记忆,能写多少是多少。。

S.R.P

Single Response Principle 单一功能原则

这本书在很多地方都提到了 SRP 原则,足以证明作者对 SRP 的认可和重视。

作者提到了几个方法,来让我们的代码能够满足单一功能原则

简单

定义的方法应该足够简单,当定义的方法足够简单的时候,也做不了太多的事情了。

要做到简单,那就要简短。一个方法不应该有太多的代码,可能几行,或者十几行。

太多的代码会增加我们的阅读成本。之前有看到统计,开发者大多时间是在阅读和理解以前的代码,真正 coding 的时间只有不到 10%…

所以,代码的可读性很重要

命名

我们都知道命名的重要性,要做到见名知义。当你发现很难给这个方法命名,也许是因为这个方法做的事情太多了。

1
2
3
4
5
6
func eat(){
  if me.notWashHand(){
    me.washHand()
  }
  me.eat()
}

上面这段伪代码片段的方法名为 eat,但是实际上在执行 eat 这个动作前会检查有没有洗手。对于上层调用者来说,如果没有阅读过这个方法的实现而直接调用这个方法,也许会很 confuse 吧。我明明没有洗手,为什么执行完 eat 后手就干净了?

我们给这个方法重新命名,叫做 washHandAndEat,清晰多了,对吧。

但是随之而来的问题是,这明显违背了 S.R.P 原则。从命名就可以看出来,这个方法做了两件事: 洗手吃饭

作者提到,方法的名字不该出现 and 或者 or 之类的词,因为这意味着这个方法做了不止一件事。

重构

经常回头重构已有的代码。没有人能够一次就写出完美的代码,只能通过不断的优化、改进。

还是上面的例子,当我们写完上面例子的代码以后,我们可以先停下来,重新审视已有的代码,并试着重构。

很明显上面的代码可以拆成两个独立的方法,washHandeat

1
2
3
4
5
6
7
func washHand(){
  me.washHand()
}

func eat(){
  me.eat()
}

至于如何使用,就交给上层的调用者来决定吧,我们只提供最原子的方法。

参数

一个方法不应该有太多的参数。方法需要的参数越多,调用者的使用成本就越高,出错的概率就越大。如果不小心把两个参数的位置放错了,或者根本就忘记了准备某个参数 etc。

作者觉得一个参数最好,两个可以接受,三个也能捏着鼻子用。超过三个,就需要考虑重新封装了(凭记忆。。。好像是这样)

whatever,我个人也觉得,方法的参数很多的时候,调用起来会很麻烦。

比如这样

1
2
3
func createUser(name str, age int, weight: int, height: int, address: str, phone: str ...) User{
  ...
}

我们要是不小心把身高和体重颠倒了位置呢,那么李雷就成了一个身高 70cm 体重 180kg 的长方体了。

我们可以先定义一个 user 的对象

1
2
3
4
5
6
7
8
9
type createUserParam struct{
  name str
  age int
  ...
}

func createUser(user createUserParam) User{
  ...
}

测试

如果你坐视测试腐坏,那么代码也会跟着腐坏。保持测试整洁吧 ——《代码整洁之道》

这句话我当时记在小本本上了。

S.R.P 一样,作者也在书中体现了对测试的重要性。

是的,有了测试用例的约束,我们可以放心大胆的修改我们的代码,只要最后能通过测试用例的验证。

对于测试,作者提出了 F.I.R.S.T概念:

F(Fast)

测试应该够快,你总不希望你花一分钟改一个逻辑,结果测试要跑十分钟吧

I(Independent)

测试之间应该相互独立

R(Repeatable)

测试应该可以在任务地方重复通过

S(Self-validating)

测试应该有bool 值输出

T(Timely)

测试应该及时编写。

前面的几个概念已经记不清了,但是 Timely 印象深刻,因为在读这本书之前我就已经在一些博客上看到关于提前写测试用例的思想了,而且我很同意。

在我们定义好方法的签名以后,方法的入参和返回值就已经确定了。我们就已经有了写测试用例的条件了。

1
func max(a, b int) int{}

我们定义了一个 max 方法,此时我们还没有具体的实现,但是只看签名,我们已经知道了这个方法要做的事,我们来写测试用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func TestMax(t testing.T){
  a := max(1, 2)
  if a != 2{
    t.Fatal(...)
  }

  a = max(-1, -4)
  if a != -1{
    t.Fatal(...)
  }

  // ...
  // 其他 cases
}

好了,我们已经有了测试用例来约束我们的 max 方法,现在来写具体实现

1
2
3
4
5
6
func max(a, b int) int{
  if a > b{
    return b
  }
  return a
}

如果我们运行测试用例的话,肯定会发现没有通过,让我们来检查一下我们的代码。。。原来是比较以后返回的值有误,我们修改一些,再次运行测试用例

1
2
3
4
5
6
func max(a, b int) int{
  if a > b{
    return a
  }
  return b
}

搞定。此时我们可以信心满满的将这个方法公开给其他调用者了

零散的东西

S.O.L.I.D 原则

有看过一篇反 solid 原则的文章,也看到过一个大佬的反驳反 solid 文章的文章(禁止套娃)。solid 原则还是很有实用意义的

interface

使用接口作为参数,使用结构体作为函数的返回值

在调用方定义接口

repo.go 中定义了 repository对象,实现和数据库的交互

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// repo.go

type repository struct{
  db
}

func NewRepository() *repository{
  ...
}

func (r repository) GetUserById(id int) User {}
func (r repository) CreateUser(user User) User {}
func (r repository) DeleteUser(user User) User {}
func (r repository) UpdateUser(user User) User {}

UserService 对外提供了查找和创建用户的功能,我们在 user.go 中创建了一个 Repository 的接口,需要实现 GetUserById(id int) UserCreateUser(user User) User 的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// user.go

type Repository interface{
  GetUserById(id int) User
  CreateUser(user User) User
}

type UserService struct{
  repo Repository
}

func NewUserService(repo Repository) *UserService {
  ...
}

func (u *UserService) GetUser(id int) User{}

func (u *UserService) CreateUser(user User) User{}

我们还有另外一个 svc,UserServicePlus. 提供删除和更新的功能,同时也定义了一个 RepositoryPlus 的接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// user_plus.go

type RepositoryPlus interface{
	DeleteUser(user User) User {}
  UpdateUser(user User) User {}
}

type UserServicePlus struct{
  repo RepositoryPlus
}

func NewUserServicePlus(repo RepositoryPlus) *UserServicePlus {
  ...
}

func (u *UserService) DeleteUser(user User) User {}

func (u *UserService) UpdateUser(user User) User {}

我们可以看到 repository 对象同时实现了 RepositoryRepositoryPlus 两个接口,所以我们可以把 repository 对象分别交给 UserServiceUserServicePlus 的构造函数中。同时,这两个 SVC 又不会有他们不该有的功能,比如在 UserService 删除用户。